/*
 * Copyright (c) 2020-2026 XMLmind Software. All rights reserved.
 *
 * Author: Hussein Shafie
 * Version: 1.8.1
 *
 * This file is part of the XMLmind XML Editor project.
 * For conditions of distribution and use, see the accompanying legal.txt file.
 */

import * as FSAccess from "./fsaccess.js";

import * as XUI from "./xui.js";

const PLATFORM_IS_MAC_OS = /mac/i.test(window.navigator.platform);
const PLATFORM_IS_WINDOWS = /win/i.test(window.navigator.platform);
const PLATFORM_IS_LINUX = /linux/i.test(window.navigator.platform);
const BROWSER_ENGINE_IS_GECKO = /Gecko\/\S+/.test(window.navigator.userAgent);
function browserEngineIsSupported() {
const appleWebKitMinVersion = 605000;
let supported = false;
if (window.navigator) {
const userAgent = window.navigator.userAgent;
if (userAgent) {
let match = /Chrome\/([0-9.]+)/.exec(userAgent);
if (match !== null) {
let version = parseInt(match[1]);
if (!isNaN(version) && version >= 107) {
supported = true;
}
} else {
let match = /AppleWebKit\/([0-9.]+)/.exec(userAgent);
if (match !== null) {
let version = parseInt(match[1]);
if (!isNaN(version) && version >= appleWebKitMinVersion) {
supported = true;
}
} else if (/Gecko\/\S+/.test(userAgent)) {
let match = /rv:([0-9.]+)/.exec(userAgent);
if (match !== null) {
let version = parseInt(match[1]);
if (!isNaN(version) && version >= 102) {
supported = true;
}
}
}
}
}
if (supported && ("ontouchstart" in window)) {
supported = false;
}
}
return supported;
}
function assertOrError(assertion, message=null) {
if (!assertion) {
if (!message) {
message = "ASSERTION FAILED!";
}
let error = new Error(message);
console.error(`assertOrError
---
${error.stack}
---`);
throw error;
}
}
function splitTrimString(s, separ) {
return (s === null || (s = s.trim()).length === 0)? [] : s.split(separ);
}
const CSRI_SEPARATOR_NO_AUTHORITY = ")@no-authority/";
const CSRI_SEPARATOR_OPAQUE_URI = ")@opaque-uri/";
const CSRI_SEPARATORS = [
CSRI_SEPARATOR_NO_AUTHORITY,
CSRI_SEPARATOR_OPAQUE_URI,
")@",
")"
];
const WINDOWS_FILE_PATH_SEPARATOR = '\\';
export const FILE_PATH_SEPARATOR =
PLATFORM_IS_WINDOWS? WINDOWS_FILE_PATH_SEPARATOR : '/';
export class URIUtil {
static csriURLToURI(url) {
if (url === null || !url.startsWith("csri://(")) {
return url;
}
let uri = url;
for (let separ of CSRI_SEPARATORS) {
let pos = uri.indexOf(separ, 8);
if (pos > 8) {
let scheme = uri.substring(8, pos).trim();
if (scheme.length > 0) {
let tail;
if (CSRI_SEPARATOR_NO_AUTHORITY === separ) {
tail = "///" + uri.substring(pos + separ.length);
} else if (CSRI_SEPARATOR_OPAQUE_URI === separ) {
tail = uri.substring(pos + separ.length);
} else {
tail = "//" + uri.substring(pos + separ.length);
}
uri = scheme + ":" + tail;
}
break;
}
}
return uri;
}
static uriBasename(uri) {
const basename = URIUtil.uriRawBasename(uri);
return !basename? null : decodeURIComponent(basename);
}
static uriRawBasename(uri) {
let [scheme, authority, path, query, fragment] = URIUtil.splitURI(uri);
if (!path) {
return null;
}
let basename = URIUtil.pathBasename(path, '/');
if (!basename) {
basename = null;
}
return basename;
}
static uriSetBasename(uri, basename) {
return URIUtil.uriSetRawBasename(uri, encodeURIComponent(basename));
}
static uriSetRawBasename(uri, basename) {
let [scheme, authority, path, query, fragment] = URIUtil.splitURI(uri);
if (!path) {
return null;
}
let parent = URIUtil.pathParent(path, '/',  true);
if (!parent) {
path = basename;
} else {
path = parent + basename;
}
return URIUtil.joinURI(scheme, authority, path, query, fragment);
}
static splitURI(uri) {
if (!uri) {
return [ null, null, null, null, null ];
}
let scheme = null;
let authority = null;
let path = null;
let query = null;
let fragment = null;
let pos = uri.indexOf("://");
if (pos < 0) {
pos = uri.indexOf(":");
if (pos < 0) {
path = uri;
} else {
scheme = uri.substring(0, pos);
path = uri.substring(pos+1);
}
} else {
scheme = uri.substring(0, uri.indexOf(":"));
pos += 3;
let slash = uri.indexOf("/", pos);
if (slash < 0) {
authority = uri.substring(pos);
path = "";
} else {
authority = uri.substring(pos, slash);
path = uri.substring(slash);
}
}
if (path !== null) {
pos = path.lastIndexOf("#");
if (pos >= 0) {
fragment = path.substring(pos+1);
path = path.substring(0, pos);
}
}
if (path !== null) {
pos = path.lastIndexOf("?");
if (pos >= 0) {
query = path.substring(pos+1);
path = path.substring(0, pos);
}
}
return [ scheme, authority, path, query, fragment ];
}
static uriExtension(uri) {
let [scheme, authority, path, query, fragment] = URIUtil.splitURI(uri);
if (!path) {
return null;
}
let ext = URIUtil.pathExtension(path, '/');
return !ext? null : decodeURIComponent(ext);
}
static uriParent(uri, trailingSepar=true) {
let [scheme, authority, path, query, fragment] = URIUtil.splitURI(uri);
if (!path) {
return null;
}
let parent = URIUtil.pathParent(path, '/', trailingSepar);
if (!parent) {
return null;
}
return URIUtil.joinURI(scheme, authority, parent);
}
static joinURI(scheme, authority, path, query=null, fragment=null) {
let uri = "";
if (scheme) {
uri += scheme;
uri += ':';
}
if (authority) {
uri += "//";
uri += authority;
}
if (path) {
uri += path;
}
if (query) {
uri += '?';
uri += query;
}
if (fragment) {
uri += '#';
uri += fragment;
}
return uri;
}
static isAbsoluteURI(uri) {
let [scheme, authority, path, query, fragment] = URIUtil.splitURI(uri);
return (scheme && path && path.startsWith('/'))? true : false;
}
static relativizeURI(uri, baseURI) {
let [scheme1, authority1, path1, query1, fragment1] =
URIUtil.splitURI(uri);
if (scheme1) {
scheme1 = scheme1.toLowerCase();
}
let auth1 = authority1;
if (!auth1) {
auth1 = null;
} else {
auth1 = auth1.toLowerCase();
if (scheme1 === "file" && auth1 === "localhost") {
auth1 = null;
}
}
let [scheme2, authority2, path2, query2, fragment2] =
URIUtil.splitURI(baseURI);
if (scheme2) {
scheme2 = scheme2.toLowerCase();
}
let auth2 = authority2;
if (!auth2) {
auth2 = null;
} else {
auth2 = auth2.toLowerCase();
if (scheme2 === "file" && auth2 === "localhost") {
auth2 = null;
}
}
if (!scheme1 || !path1 || !path1.startsWith('/') ||
!scheme2 || !path2 || !path2.startsWith('/') ||
scheme1 !== scheme2 || auth1 !== auth2) {
return uri;
}
if (PLATFORM_IS_WINDOWS && scheme1 === "file") {
let drive1 = null;
let match = path1.match(/^\/([a-zA-Z]:)\//);
if (match !== null) {
drive1 = match[1].toUpperCase();
}
let drive2 = null;
match = path2.match(/^\/([a-zA-Z]:)\//);
if (match !== null) {
drive2 = match[1].toUpperCase();
}
if (drive1 !== drive2) {
return uri;
}
}
let relPath = URIUtil.relativizePath(path1, path2, '/');
return URIUtil.joinURI( null,  null,
relPath, query1, fragment1);
}
static resolveURI(uri, baseURI) {
let [scheme2, authority2, path2, query2, fragment2] =
URIUtil.splitURI(baseURI);
if (!scheme2 || !path2 || !path2.startsWith('/')) {
return uri;
}
let [scheme1, authority1, path1, query1, fragment1] =
URIUtil.splitURI(uri);
if (scheme1 && path1 && path1.startsWith('/')) {
return uri;
}
let absPath = !path1? "/" : URIUtil.resolvePath(path1, path2, '/');
return URIUtil.joinURI(scheme2, authority2,
absPath, query1, fragment1);
}
static normalizeFileURI(uri) {
let uri2;
if (uri && (uri2 = uri.toLowerCase()).startsWith("file:/")) {
let matches = (new RegExp("^file://([^/]*)/")).exec(uri2);
if (matches === null) {
uri = "file:///" + uri.substring(6);
} else {
if (matches[1] === "localhost") {
uri = "file:///" + uri.substring(17);
}
}
}
return uri;
}
static uriToFilePath(uri, pathSepar=FILE_PATH_SEPARATOR) {
let [scheme, authority, path, query, fragment] = URIUtil.splitURI(uri);
if (!scheme || scheme !== "file") {
return null;
}
if (!path) {
path = "/";
}
let segments = path.split('/');
for (let i = segments.length-1; i >= 0; --i) {
segments[i] = decodeURIComponent(segments[i]);
}
let file = segments.join(pathSepar);
if (pathSepar === WINDOWS_FILE_PATH_SEPARATOR) {
if (authority && authority !== "localhost") {
file = "\\\\" + authority + file;
} else {
if (file.match(/^\\[a-zA-Z]:/)) {
if (file.length === 3) {
file = file.substring(1) + "\\";
} else if (file.charAt(3) === "\\") {
file = file.substring(1);
}
}
}
}
return URIUtil.normalizePath(file, pathSepar);
}
static pathToFileURI(path, pathSepar=FILE_PATH_SEPARATOR) {
let [drive, checkedPath] = URIUtil.checkPath(path, pathSepar);
let uri;
if (URIUtil.testIsAbsolute(drive, checkedPath, pathSepar)) {
uri = "file://";
if (drive !== null) {
if (drive.match(/^[a-zA-Z]:/)) {
uri += "/" + drive;
} else {
uri += drive.substring(2);
}
}
} else {
uri = "";
}
let parts = checkedPath.split(pathSepar).map((part) => {
if (part.length > 0) {
part = encodeURIComponent(part);
}
return part;
});
uri += parts.join('/');
return uri;
}
static checkPath(path, pathSepar) {
let drive = null;
if (pathSepar === WINDOWS_FILE_PATH_SEPARATOR) {
let match = path.match(/^(\\\\[^\\]+)\\/);
if (match !== null) {
drive = match[1].toUpperCase();
path = path.substring(drive.length);
} else {
let match = path.match(/^([a-zA-Z]:)\\/);
if (match !== null) {
drive = match[1].toUpperCase();
path = path.substring(drive.length);
}
}
}
path = URIUtil.doNormalizePath(path, pathSepar);
return [drive, path];
}
static doNormalizePath(path, pathSepar) {
const originalPath = path;
const prependSlash = path.startsWith(pathSepar);
const appendSlash = path.endsWith(pathSepar);
if (prependSlash || appendSlash) {
let start = 0;
let end = path.length-1;
while (start <= end && path.charAt(start) === pathSepar) {
++start;
}
while (end >= start && path.charAt(end) === pathSepar) {
--end;
}
if (start > end) {
return pathSepar;
}
path = path.substring(start, end+1);
}
let twoPasses = false;
let buffer = "";
let parts = path.split(pathSepar);
for (let part of parts) {
if (part.length === 0 ||
"." === part) {
continue;
}
if (part.trim().length === 0) {
return originalPath;
}
if (buffer.length > 0) {
buffer += pathSepar;
}
buffer += part;
if (".." === part) {
twoPasses = true;
}
}
path = buffer;
if (twoPasses) {
parts = path.split(pathSepar);
buffer = "";
for (let i = parts.length-1; i >= 0; --i) {
let part = parts[i];
if (".." === part) {
let j = i;
while (j >= 0 && ".." === parts[j]) {
--j;
}
i = i - 2*(i-j) + 1;
if (i < 0) {
return originalPath;
}
} else {
if (buffer.length > 0) {
buffer = pathSepar + buffer;
}
buffer = part + buffer;
}
}
}
path = buffer;
if (prependSlash) {
path = pathSepar + path;
}
if (appendSlash) {
path = path + pathSepar;
}
return path;
}
static testIsAbsolute(drive, checkedPath, pathSepar) {
if (pathSepar === WINDOWS_FILE_PATH_SEPARATOR && drive === null) {
return false;
}
return checkedPath.startsWith(pathSepar);
}
static isAbsolutePath(path, pathSepar) {
let [drive, checkedPath] = URIUtil.checkPath(path, pathSepar);
return URIUtil.testIsAbsolute(drive, checkedPath, pathSepar);
}
static resolvePath(path, basePath, pathSepar) {
if (URIUtil.isAbsolutePath(path, pathSepar) ||
!basePath) {
return path;
}
let parentPath;
if (basePath.endsWith(pathSepar)) {
parentPath = basePath;
} else {
parentPath = URIUtil.pathParent(basePath, pathSepar,
true);
}
return URIUtil.normalizePath(!parentPath? path : parentPath + path,
pathSepar);
}
static normalizePath(path, pathSepar) {
let [drive, checkedPath] = URIUtil.checkPath(path, pathSepar);
if (drive !== null) {
checkedPath = drive + checkedPath;
}
return checkedPath;
}
static pathExtension(path, pathSepar) {
let [drive, checkedPath] = URIUtil.checkPath(path, pathSepar);
let dot = URIUtil.indexOfDot(checkedPath, pathSepar);
if (dot < 0) {
return null;
} else {
return checkedPath.substring(dot+1);
}
}
static indexOfDot(checkedPath, pathSepar) {
let dot = checkedPath.lastIndexOf('.');
if (dot >= 0) {
let baseNameStart = checkedPath.lastIndexOf(pathSepar);
if (baseNameStart < 0) {
baseNameStart = 0;
} else {
++baseNameStart;
}
if (dot <= baseNameStart) {
dot = -1;
}
}
return dot;
}
static pathBasename(path, pathSepar) {
let [drive, checkedPath] = URIUtil.checkPath(path, pathSepar);
if (checkedPath === pathSepar) {
return "";
}
if (checkedPath.endsWith(pathSepar)) {
checkedPath = checkedPath.substring(0, checkedPath.length-1);
}
let slash = checkedPath.lastIndexOf(pathSepar);
if (slash < 0) {
return checkedPath;
}
return checkedPath.substring(slash+1);
}
static pathParent(path, pathSepar, trailingSepar) {
let [drive, checkedPath] = URIUtil.checkPath(path, pathSepar);
if (checkedPath === pathSepar) {
return null;
}
if (checkedPath.endsWith(pathSepar)) {
checkedPath = checkedPath.substring(0, checkedPath.length-1);
}
let slash = checkedPath.lastIndexOf(pathSepar);
if (slash < 0) {
return null;
}
let parent;
if (slash === 0) {
parent = pathSepar;
} else {
parent = checkedPath.substring(0, trailingSepar? slash+1 : slash);
}
if (drive !== null) {
parent = drive + parent;
}
return parent;
}
static relativizePath(path, basePath, pathSepar) {
let [drive1, checkedPath1] = URIUtil.checkPath(path, pathSepar);
let [drive2, checkedPath2] = URIUtil.checkPath(basePath, pathSepar);
if (!URIUtil.testIsAbsolute(drive1, checkedPath1, pathSepar) ||
!URIUtil.testIsAbsolute(drive2, checkedPath2, pathSepar) ||
drive1 !== drive2) {
return path;
}
path = checkedPath1;
basePath = checkedPath2;
if (pathSepar === path) {
return pathSepar;
}
if (pathSepar === basePath) {
return path.substring(1);
}
if (!basePath.endsWith(pathSepar)) {
basePath = URIUtil.pathParent(basePath, pathSepar,
true);
}
const onWindows = (pathSepar === WINDOWS_FILE_PATH_SEPARATOR);
const upSegment = ".." + pathSepar;
let relPath = "";
while (basePath !== null) {
let start = basePath;
if (path.startsWith(start) ||
(onWindows &&
path.toLowerCase().startsWith(start.toLowerCase()))) {
relPath += path.substring(start.length);
break;
}
relPath += upSegment;
basePath = URIUtil.pathParent(basePath, pathSepar,
true);
}
return relPath;
}
static formatFileSize(byteCount, fallback=null) {
if (!Number.isInteger(byteCount) || byteCount < 0) {
return fallback;
}
let unit = 0;
while (byteCount >= 1024) {
byteCount /= 1024;
++unit;
if (unit === 8) {
break;
}
}
if (unit > 0) {
byteCount = byteCount.toFixed(1);
}
return `${byteCount} ${" KMGTPEZY"[unit]}B`;
}
static formatFileDate(millis, fallback=null) {
if (!Number.isInteger(millis) || millis < 0) {
return fallback;
}
const date = new Date(millis);
let text = date.getFullYear();
text += "-";
text += URIUtil.formatFileDateField(1 + date.getMonth());
text += "-";
text += URIUtil.formatFileDateField(date.getDate());
text += " ";
text += URIUtil.formatFileDateField(date.getHours());
text += ":";
text += URIUtil.formatFileDateField(date.getMinutes());
return text;
}
static formatFileDateField(value) {
let text = String(value);
while (text.length < 2) {
text = "0" + text;
}
return text;
}
}
export class Resource {
constructor(uri, data) {
this._uri = uri;
this._data = data;
}
get uri() {
return this._uri;
}
get data() {
return this._data;
}
toString() {
const size = !this.data? -1 : this.data.size;
return `${this.constructor.name}(uri='${this.uri}', \
data=<${size} bytes>)`;
}
toJSON(key) {
const size = !this.data? -1 : this.data.size;
return { type: this.constructor.name, uri: this.uri, size: size };
}
}
export class ResourceStorage {
constructor(xmlEditor) {
this._xmlEditor = xmlEditor;
}
get xmlEditor() {
return this._xmlEditor;
}
loadResource(uri) {
return Promise.reject(new Error("'loadResource' not implemented"));
}
storeResource(data, uri) {
return Promise.reject(new Error("'storeResource' not implemented"));
}
openResource(options) {
return Promise.reject(new Error("'openResource' not implemented"));
}
}
export class LocalFileDialog extends XUI.Dialog {
static showDialog(xmlEditor, options={}, savedData=null) {
return new Promise((resolve, reject) => {
let dialog = new LocalFileDialog(xmlEditor, options, savedData,
resolve);
dialog.open("center", xmlEditor);
});
}
constructor(xmlEditor, options, savedData, resolve) {
super({ title: LocalFileDialog.titleDefaulted(options, savedData),
movable: true, resizable: true, closeable: true,
template: LocalFileDialog.TEMPLATE,
buttons: [ { label: "Cancel", action: "cancelAction" },
{ label: "OK", action: "okAction",
default: true } ] });
this._xmlEditor = xmlEditor;
this._options = options;
this._savedData = savedData;
this._resolve = resolve;
let pane = this.contentPane.firstElementChild;
this._step1 = pane.querySelector(".xxe-lfd-step1");
this._browseLabel = pane.querySelector(".xxe-lfd-browse-label");
this._browseButton = pane.querySelector(".xxe-lfd-browse");
this._browseButton.onclick = this.chooseFileAction.bind(this);
this._step2 = pane.querySelector(".xxe-lfd-step2");
this._dirPathText = pane.querySelector(".xxe-lfd-dir-path");
let pathSepar = pane.querySelector(".xxe-lfd-path-separ");
pathSepar.textContent = FILE_PATH_SEPARATOR;
this._fileNameText = pane.querySelector(".xxe-lfd-file-name");
const openMode = !this._savedData;
if (FSAccess.isAvailable() || openMode) {
this._fileNameText.setAttribute("readonly", "readonly");
}
this._optPane = pane.querySelector(".xxe-lfd-opt-pane");
this._optLabel = pane.querySelector(".xxe-lfd-opt-label");
this._optToggle = pane.querySelector(".xxe-lfd-opt-toggle");
if (openMode) {
this._browseButton.textContent = XUI.StockIcon["folder-open"];
} else {
this._browseLabel.textContent = "Save file:";
this._browseButton.textContent = XUI.StockIcon["floppy"];
}
let suggestedDirPath = null;
let suggestedFileName = null;
let templatePath = !options.templateURI? null :
URIUtil.uriToFilePath(options.templateURI);
if (templatePath !== null) {
suggestedDirPath =
URIUtil.pathParent(templatePath, FILE_PATH_SEPARATOR,
false);
suggestedFileName =
URIUtil.pathBasename(templatePath, FILE_PATH_SEPARATOR);
}
this._suggestedFileName = null;
if (!openMode && suggestedFileName) {
this._suggestedFileName = suggestedFileName;
}
this.rememberDirPath(suggestedDirPath,  true);
if (!FSAccess.isAvailable()) {
this.rememberFileName(suggestedFileName,  !openMode);
}
if (!options.option ||
!Array.isArray(options.option) || options.option.length !== 3) {
this._optionName = null;
this._optPane.style.display = "none";
} else {
this._optionName = options.option[0];
this._optToggle.checked = options.option[1];
this._optLabel.appendChild(
document.createTextNode(options.option[2]));
}
this._openedFile = null;
this._savedFileHandle = null;
}
static titleDefaulted(options, savedData) {
if (!options.title) {
if (!savedData) {
return "Open";
} else {
return "Save";
}
} else {
return options.title;
}
}
rememberDirPath(dirPath, addDataList) {
let valueList =
XUI.Util.rememberDatalistItem("XXE.LocalFileDialog.lastDirPaths",
dirPath);
if (addDataList) {
XUI.Util.attachDatalist(this._dirPathText, valueList,
this.contentPane.firstElementChild);
}
}
rememberFileName(fileName, addDataList) {
let valueList =
XUI.Util.rememberDatalistItem("XXE.LocalFileDialog.lastFileNames",
fileName);
if (addDataList) {
XUI.Util.attachDatalist(this._fileNameText, valueList,
this.contentPane.firstElementChild);
}
}
chooseFileAction(event) {
let opts = { id: "XXE_localFileDialog", multiple: false,
startIn: "documents", excludeAcceptAllOption: false };
const openMode = !this._savedData;
if (openMode) {
LocalFileDialog.addExtensions(this._options.extensions, opts);
} else {
if (this._suggestedFileName !== null) {
opts.fileName = this._suggestedFileName;
}
}
if (openMode) {
FSAccess.fileOpen(opts)
.then((file) => {
if (file) {
this._openedFile = file;
this._fileNameText.value = file.name;
this.proceedToStep2();
}
},
(error) => {
this.chooseFileError(error);
});
} else {
FSAccess.fileSave(this._savedData, opts,  null,
false)
.then((handle) => {
if (FSAccess.isAvailable()) {
if (handle) {
this._savedFileHandle = handle;
this._fileNameText.value = handle.name;
this.proceedToStep2();
}
} else {
this.proceedToStep2();
}
},
(error) => {
this.chooseFileError(error);
});
}
}
static addExtensions(exts, opts) {
if (FSAccess.isAvailable()) {
return;
}
if (Array.isArray(exts) && exts.length > 0) {
opts.extensions = [];
for (let ext of exts) {
if (Array.isArray(ext) && ext.length >= 3) {
for (let i = 2; i < ext.length; ++i) {
let suffix = ext[i].trim().toLowerCase();
if (suffix.length > 0) {
if (!suffix.startsWith(".")) {
suffix = "." + suffix;
}
opts.extensions.push(suffix);
}
}
}
}
}
}
chooseFileError(error) {
if (error.name !== "AbortError") {
this._openedFile = null;
this._savedFileHandle = null;
XUI.Alert.showError(
`Cannot display the file chooser dialog:\n${error}`,
this._xmlEditor);
}
}
proceedToStep2() {
this._step1.classList.remove("xxe-lfd-curstep");
this._step2.classList.add("xxe-lfd-curstep");
this._dirPathText.removeAttribute("disabled");
this._fileNameText.removeAttribute("disabled");
}
dialogClosed(result) {
this._resolve(result);
}
cancelAction() {
this.close(null);
}
okAction() {
let choice = {};
const openMode = !this._savedData;
if (openMode) {
if (this._openedFile === null) {
this._browseButton.focus();
return;
}
choice.file = this._openedFile;
} else {
if (FSAccess.isAvailable()) {
if (this._savedFileHandle === null) {
this._browseButton.focus();
return;
}
choice.fileHandle = this._savedFileHandle;
} else {
choice.fileHandle = null;
}
}
let dirPath = this._dirPathText.value.trim();
if (dirPath.length === 0) {
XUI.Util.badTextField(this._dirPathText);
return;
}
let fileName = this._fileNameText.value.trim();
if (fileName.length === 0 ||
fileName.indexOf(FILE_PATH_SEPARATOR) >= 0) {
XUI.Util.badTextField(this._fileNameText);
return;
}
let filePath = dirPath;
if (!filePath.endsWith(FILE_PATH_SEPARATOR)) {
filePath += FILE_PATH_SEPARATOR;
}
filePath += fileName;
if (!URIUtil.isAbsolutePath(filePath, FILE_PATH_SEPARATOR)) {
XUI.Util.badTextField(this._dirPathText);
return;
}
dirPath = URIUtil.pathParent(filePath, FILE_PATH_SEPARATOR,
false);
this.rememberDirPath(dirPath,  false);
if (!FSAccess.isAvailable()) {
this.rememberFileName(fileName, false);
}
choice.uri = URIUtil.pathToFileURI(filePath);
if (this._optionName !== null) {
choice[this._optionName] = this._optToggle.checked;
}
this.close(choice);
}
}
LocalFileDialog.TEMPLATE = document.createElement("template");
LocalFileDialog.TEMPLATE.innerHTML = `
<div class="xxe-lfd-pane">
  <div class="xxe-lfd-browse-pane"><span class="xxe-lfd-step1
    xxe-lfd-curstep">1.</span>
    <span class="xxe-lfd-browse-label">Open file:</span>
    <button type="button" title="Choose file..."
            class="xui-control xui-small-icon xxe-lfd-browse"></button>
  </div>
  <div class="xxe-lfd-path-label"><span class="xxe-lfd-step2">2.</span>
  Specify the <strong>absolute</strong> path of chosen file:</div>
  <div class="xxe-lfd-path-pane">
    <input type="text" size="50" class="xui-control xxe-lfd-dir-path"
           spellcheck="false" autocomplete="off" disabled="disabled" />
    <span class="xxe-lfd-path-separ"></span>
    <input type="text" size="20" class="xui-control xxe-lfd-file-name"
           spellcheck="false" autocomplete="off" disabled="disabled" />
  </div>
  <div class="xxe-lfd-path-hint"><strong>Required.</strong>
  For security reasons, this application has no way<br />
  to automatically determine the absolute path of chosen file.</div>
  <div class="xxe-lfd-opt-pane">
    <label class="xxe-lfd-opt-label">
      <input type="checkbox" class="xxe-lfd-opt-toggle"/>
    </label>
  </div>
</div>
`;
export class LocalFile extends Resource {
constructor(uri, data, fileHandle) {
super(uri, data);
this._fileHandle = !fileHandle? null : fileHandle;
}
get fileHandle() {
return this._fileHandle;
}
toJSON(key) {
let obj = super.toJSON(key);
const handle = this.fileHandle;
obj.fileHandle =
(handle === null)? null : `<${handle.kind} "${handle.name}>"`;
return obj;
}
}
export class LocalFiles extends ResourceStorage {
constructor(xmlEditor) {
super(xmlEditor);
}
loadResource(uri) {
if (!uri.startsWith("file:/")) {
return Promise.resolve(null);
} else {
return Promise.resolve(new LocalFile(uri, null, null));
}
}
storeResource(data, uri) {
if (!uri.startsWith("file:/")) {
return Promise.resolve(null);
} else {
return Promise.resolve(new LocalFile(uri, data, data.handle));
}
}
openResource(options) {
return LocalFileDialog.showDialog(this._xmlEditor, options)
.then((choice) => {
if (choice === null) {
return null;
} else {
return new LocalFile(choice.uri, choice.file,
choice.file.handle);
}
});
}
}
export class RemoteFileDialog extends XUI.Dialog {
static showDialog(xmlEditor, options={}, saveMode=false) {
return new Promise((resolve, reject) => {
let dialog = new RemoteFileDialog(xmlEditor, options, saveMode,
resolve);
dialog.open("center", xmlEditor);
});
}
constructor(xmlEditor, options, saveMode, resolve) {
super({ title: LocalFileDialog.titleDefaulted(options, saveMode),
movable: true, resizable: true, closeable: true,
template: RemoteFileDialog.TEMPLATE,
buttons: [ { label: "Cancel", action: "cancelAction" },
{ label: "OK", action: "okAction",
default: true } ] });
this._xmlEditor = xmlEditor;
this._resolve = resolve;
this._saveMode = saveMode;
this._isLastSelectedURI = false;
this._initialURI = !options.templateURI? null : options.templateURI;
if (this._initialURI === null) {
this._initialURI = window.localStorage.getItem(
"XXE.RemoteFileDialog.lastSelectedURI");
if (this._initialURI !== null) {
this._initialURI = URIUtil.uriSetBasename(this._initialURI,
"Untitled.xml");
this._isLastSelectedURI = (this._initialURI !== null);
}
}
this._initialPath = null;
let content = this.contentPane.firstElementChild;
this._drivePane = content.firstElementChild;
let chooserPane = content.lastElementChild;
let folderPane = chooserPane.firstElementChild;
this._folderSelector = folderPane.firstElementChild
.nextElementSibling;
this._folderSelector.onchange = this.folderSelected.bind(this);
this._folderUpButton = this._folderSelector.nextElementSibling;
this._folderUpButton.firstElementChild.textContent =
XUI.StockIcon["folder"];
this._folderUpButton.lastElementChild.textContent =
XUI.StockIcon["up-bold"];
this._folderUpButton.onclick = this.folderUpAction.bind(this);
this._folderNewButton = this._folderUpButton.nextElementSibling;
this._folderNewButton.firstElementChild.textContent =
XUI.StockIcon["folder"];
this._folderNewButton.lastElementChild.textContent =
XUI.StockIcon["plus"];
this._folderNewButton.onclick = this.folderNewAction.bind(this);
let fileScroll = folderPane.nextElementSibling;
this._fileTable =  fileScroll.firstElementChild;
this._fileRows = this._fileTable.lastElementChild;
this._fileRows.onclick = this.fileRowClicked.bind(this);
let fileForm = fileScroll.nextElementSibling;
let fileFormChildren = fileForm.children;
this._fileNameText = fileFormChildren.item(1);
this._fileNameText.onfocus = this.fileNameTextFocused.bind(this);
this._selectingFileType = false;
this._fileTypeSelector = fileFormChildren.item(3);
RemoteFileDialog.addFileTypes(this._fileTypeSelector,
options.extensions);
this._fileTypeSelector.onchange = this.fileTypeSelected.bind(this);
this._optPane = fileForm.nextElementSibling;
this._optLabel = this._optPane.firstElementChild;
this._optToggle = this._optLabel.firstElementChild;
if (!options.option ||
!Array.isArray(options.option) || options.option.length !== 3) {
this._optionName = null;
this._optPane.style.display = "none";
} else {
this._optionName = options.option[0];
this._optToggle.checked = options.option[1];
this._optLabel.appendChild(
document.createTextNode(options.option[2]));
}
this.initDrives();
}
static addFileTypes(select, extensions) {
select.appendChild(RemoteFileDialog.createOption("*", "All files",
true));
if (!Array.isArray(extensions)) {
return;
}
for (let extension of extensions) {
if (Array.isArray(extension) && extension.length >= 3) {
let list = [];
for (let i = 2; i < extension.length; ++i) {
let ext = extension[i].trim();
if (ext.startsWith(".")) {
ext = ext.substring(1);
}
if (ext.length > 0) {
list.push(ext);
}
}
if (list.length > 0) {
select.appendChild(
RemoteFileDialog.createOption(list.join(';').toLowerCase(),
extension[0],
false));
}
}
}
}
static createOption(value, text, selected) {
let option = document.createElement("option");
option.setAttribute("value", value);
if (selected) {
option.setAttribute("selected", "selected");
}
option.appendChild(document.createTextNode(text));
return option;
}
selectFileType(extension) {
this._selectingFileType = true;
let selectedIndex = -1;
if (extension === null || extension.length === 0 ||
extension === "*") {
selectedIndex = 0;
} else {
extension = extension.toLowerCase();
for (let option of this._fileTypeSelector.options) {
let exts = option.value;
if (exts.indexOf(';') >= 0) {
exts = exts.split(';');
} else {
exts = [ exts ];
}
if (exts.indexOf(extension) >= 0) {
selectedIndex = option.index;
break;
}
}
}
if (selectedIndex < 0) {
selectedIndex = this._fileTypeSelector.length;
this._fileTypeSelector.appendChild(
RemoteFileDialog.createOption(extension, "*." + extension,
false));
}
this._fileTypeSelector.options[selectedIndex].selected = true;
this._selectingFileType = false;
}
initDrives() {
this._drives = [];
this._selectedDrive = null;
return this._xmlEditor.listRootDirectories()
.then((drives) => {
this.setDrives(drives);
return drives;
},
(error) => {
XUI.Alert.showError(
`Cannot list root directories:\n${error}`,
this._xmlEditor);
return null;
});
}
setDrives(drives) {
this._drives = drives;
this._selectedDrive = null;
XUI.Util.removeAllChildren(this._drivePane);
let initialURI = this._initialURI;
this._initialURI = null;
this._initialPath = null;
if (drives.length > 0) {
let selectedDrive = null;
if (initialURI !== null) {
selectedDrive = RemoteFileDialog.findDrive(drives, initialURI);
if (selectedDrive === null) {
this._isLastSelectedURI = false;
} else {
this._initialPath =
"/" + initialURI.substring(selectedDrive.uri.length);
this._initialPath =
URIUtil.normalizePath(this._initialPath, '/');
if (this._initialPath.endsWith("/")) {
this._initialPath += "Untitled.xml";
}
}
}
let selectedDriveDiv = null;
for (let drive of drives) {
let driveDiv = this.appendDrive(drive, this._drivePane);
if (drive === selectedDrive) {
selectedDriveDiv = driveDiv;
}
}
if (selectedDrive === null) {
selectedDrive = drives[0];
selectedDriveDiv = this._drivePane.firstElementChild;
}
this.selectDrive(selectedDrive, selectedDriveDiv);
const maxDrives = 6;
if (drives.length > maxDrives) {
const padding =
XUI.Util.getPxProperty(this._drivePane, "padding-top");
const driveDiv = this._drivePane.lastElementChild;
const margin = XUI.Util.getPxProperty(driveDiv, "margin-top");
const maxHeight = (2 * padding) +
(maxDrives * driveDiv.offsetHeight) +
((maxDrives - 1) * margin);
if (!isNaN(maxHeight)) {
this._drivePane.style.maxHeight = String(maxHeight) + "px";
}
}
}
}
static findDrive(drives, uri) {
uri = uri.toLowerCase();
let found = null;
for (let drive of drives) {
if (uri.startsWith(drive.uri.toLowerCase())) {
if (found === null ||
found.uri.length < drive.uri.length) {
found = drive;
}
}
}
return found;
}
appendDrive(drive, drivePane) {
let driveDiv = document.createElement("div");
driveDiv.setAttribute("class", "xui-control xxe-rfd-drive");
let tooltip = drive.uri;
if (drive.readonly) {
tooltip += "\n(read-only)";
}
driveDiv.setAttribute("title", tooltip);
drivePane.appendChild(driveDiv);
driveDiv.onclick = (e) => {
XUI.Util.consumeEvent(e);
if (this._selectedDrive !== drive) {
this.selectDrive(drive, driveDiv);
}
};
if (drive.readonly) {
let badge = document.createElement("div");
badge.setAttribute("class", "xui-small-icon xxe-rfd-drive-badge");
badge.textContent = XUI.StockIcon["eye"];
driveDiv.appendChild(badge);
}
let icon = document.createElement("div");
icon.setAttribute("class", "xui-small-icon xxe-rfd-drive-icon");
let iconName;
let iconColor;
let iconLabel = drive.label.toLowerCase();
if (iconLabel === "home") {
iconName = iconLabel;
iconColor = "#318CE7";
} else if (iconLabel === "computer") {
iconName = iconLabel;
iconColor = "#D0417E";
} else {
if (!drive.uri.startsWith("file:")) {
iconName = "server";
iconColor = "#63B76C";
} else {
iconName = "drive";
iconColor = "#6CB4EE";
}
}
icon.style.color = iconColor;
icon.textContent = XUI.StockIcon[iconName];
driveDiv.appendChild(icon);
let label = document.createElement("div");
label.setAttribute("class", "xxe-rfd-drive-label");
label.textContent = XUI.Util.shortenText(drive.label, 12);
driveDiv.appendChild(label);
return driveDiv;
}
selectDrive(drive, driveDiv) {
if (this._selectedDrive !== null) {
let sel = this._drivePane.querySelector(".xxe-rfd-sel-drive");
if (sel !== null) {
sel.classList.remove("xxe-rfd-sel-drive");
}
this._selectedDrive = null;
}
if (drive !== null) {
driveDiv.classList.add("xxe-rfd-sel-drive");
this._selectedDrive = drive;
let initialPath = this._initialPath;
this._initialURI = null;
this._initialPath = null;
let folderPath = null;
let fileName = null;
if (initialPath !== null) {
folderPath = URIUtil.pathParent(initialPath, '/',
false);
if (this._saveMode) {
fileName = URIUtil.pathBasename(initialPath, '/');
}
}
if (folderPath === null) {
folderPath =
!drive.selectedFolder? "/" : drive.selectedFolder.path;
this._isLastSelectedURI = false;
}
this.selectFolder(drive, folderPath, fileName);
}
}
selectFolder(drive, folderPath, fileName=null) {
this.updateFileTable(drive, folderPath)
.then((result) => {
if (result !== null) {
let [actualPath, listing] = result;
drive.selectedFolder = { path: actualPath,
listing: listing };
this.updateFolderSelector(actualPath);
this.enableFolderButtons(drive, actualPath);
if (fileName !== null && actualPath === folderPath) {
this.selectRow(fileName);
}
}
});
}
updateFileTable(drive, folderPath) {
let folderPathFromLastSelectedURI = this._isLastSelectedURI;
this._isLastSelectedURI = false;
const folderInfo = {
path: folderPath,
uri: RemoteFileDialog.joinURI(drive, folderPath)
};
return this._xmlEditor.listFiles(folderInfo.uri)
.then((listing) => {
if (Array.isArray(listing)) {
return listing;
} else {
if (folderPath === "/") {
return null;
} else {
if (!folderPathFromLastSelectedURI) {
this.reportListFolderError(folderInfo.path,
"not a folder");
}
folderInfo.path = "/";
folderInfo.uri = RemoteFileDialog.joinURI(drive, "/");
return this._xmlEditor.listFiles(folderInfo.uri);
}
}
})
.then((listing) => {
if (!Array.isArray(listing)) {
this.reportListFolderError(folderInfo.path, "not a folder");
return null;
}
this.updateFileRows(listing, this.getAcceptedExtensions());
return [folderInfo.path, listing];
})
.catch((error) => {
this.reportListFolderError(folderInfo.path, error);
return null;
});
}
static joinURI(drive, filePath, fileName=null) {
if (fileName !== null) {
filePath = RemoteFileDialog.joinPath(filePath, fileName);
}
let uri = drive.uri;
if (!uri.endsWith("/")) {
uri += "/";
}
const parts = RemoteFileDialog.splitPath(filePath);
for (let i = 0; i < parts.length; ++i) {
if (i > 0) {
uri += "/";
}
uri += encodeURIComponent(parts[i]);
}
return uri;
}
static joinPath(folderPath, fileName) {
let path = folderPath;
if (!path.endsWith("/")) {
path += "/";
}
path += fileName;
return path;
}
static splitPath(path) {
if (path.startsWith("/")) {
path = path.substring(1);
}
if (path.endsWith("/")) {
path = path.substring(0, path.length - 1);
}
if (path.length > 0) {
return path.split("/");
} else {
return [];
}
}
reportListFolderError(folderPath, error) {
XUI.Alert.showError(`Cannot list folder\n"${folderPath}":\n${error}`,
this._xmlEditor);
}
updateFolderSelector(folderPath) {
XUI.Util.removeAllChildren(this._folderSelector);
const parts = RemoteFileDialog.splitPath(folderPath);
const lastPart = parts.length-1;
folderPath = "/";
let prevOption =
RemoteFileDialog.createOption(folderPath, folderPath,
(-1 === lastPart));
this._folderSelector.appendChild(prevOption);
for (let i = 0; i <= lastPart; ++i) {
if (i > 0) {
folderPath += "/";
}
folderPath += parts[i];
let option =
RemoteFileDialog.createOption(folderPath, folderPath,
(i === lastPart));
this._folderSelector.insertBefore(option, prevOption);
prevOption = option;
}
}
folderSelected(event) {
this.selectFolder(this._selectedDrive, event.target.value);
}
enableFolderButtons(drive, folderPath) {
RemoteFileDialog.enableButton(this._folderUpButton, folderPath !== "/");
RemoteFileDialog.enableButton(this._folderNewButton, !drive.readonly);
}
static enableButton(button, enable) {
if (enable) {
button.removeAttribute("disabled");
button.classList.remove("xui-control-disabled");
} else {
button.setAttribute("disabled", "disabled");
button.classList.add("xui-control-disabled");
}
}
folderUpAction(event) {
let selectedFolder = this.getSelectedFolder();
if (selectedFolder === null) {
return;
}
let folderPath = selectedFolder.path;
if (folderPath === "/") {
return;
}
folderPath = URIUtil.pathParent(folderPath, '/',
false);
this.selectFolder(this._selectedDrive, folderPath);
}
getSelectedFolder() {
const selectedDrive = this._selectedDrive;
if (!selectedDrive) {
return null;
}
const selectedFolder = selectedDrive.selectedFolder;
if (!selectedFolder ||
!selectedFolder.path ||
!Array.isArray(selectedFolder.listing)) {
return null;
}
return selectedFolder;
}
folderNewAction(event) {
let selectedFolder = this.getSelectedFolder();
if (selectedFolder === null) {
return;
}
let folderName = [];
XUI.Prompt.showPrompt("Create New Folder", "Folder name:",
30,  null,
null,  null,
this._xmlEditor)
.then((answer) => {
if (answer === null) {
return false;
} else {
let error = null;
if (answer.indexOf("/") >= 0) {
error = `"${answer}" is not a file basename.`;
} else if (RemoteFileDialog.getFileInfo(selectedFolder,
answer) !== null) {
error = `File "${answer}" already exists.`;
}
if (error !== null) {
XUI.Alert.showError(error, this._xmlEditor);
return false;
}
folderName.push(answer);
return this._xmlEditor.createDirectory(
RemoteFileDialog.joinURI(this._selectedDrive,
selectedFolder.path,
folderName[0]));
}
})
.then((created) => {
if (created) {
this.selectFolder(this._selectedDrive, selectedFolder.path,
folderName[0]);
}
})
.catch((error) => {
XUI.Alert.showError(
`Cannot create folder "${folderName[0]}":\n${error}`,
this._xmlEditor);
});
}
static getFileInfo(selectedFolder, fileName) {
let fileInfo = selectedFolder.listing.find((entry) => {
return (entry.name === fileName);
});
return !fileInfo? null : fileInfo;
}
updateFileRows(listing, acceptedExts) {
XUI.Util.removeAllChildren(this._fileRows);
for (let file of listing) {
if (!file.directory &&
!RemoteFileDialog.acceptFileName(file.name, acceptedExts)) {
continue;
}
let row = document.createElement("tr");
row.xxeFileInfo = file;
let cell = document.createElement("td");
let fileIcon = document.createElement("span");
let fileIconClass, fileIconName;
if (file.directory) {
fileIconClass = "xxe-rfd-dir-icon";
fileIconName = "folder";
} else {
fileIconClass = "xxe-rfd-file-icon";
fileIconName = "file";
}
fileIcon.setAttribute("class", "xui-small-icon " + fileIconClass);
fileIcon.textContent = XUI.StockIcon[fileIconName];
cell.appendChild(fileIcon);
cell.appendChild(document.createTextNode(file.name));
row.appendChild(cell);
cell = document.createElement("td");
cell.textContent = URIUtil.formatFileSize(file.size, "\u00A0");
row.appendChild(cell);
cell = document.createElement("td");
cell.textContent = URIUtil.formatFileDate(file.date, "\u00A0");
row.appendChild(cell);
this._fileRows.appendChild(row);
}
this._fileTable.scrollIntoView({ block: "start", inline: "start" });
}
static acceptFileName(fileName, acceptedExts) {
if (acceptedExts === null || acceptedExts.length === 0) {
return true;
}
let ext = URIUtil.pathExtension(fileName, '/');
if (ext === null || (ext = ext.trim()).length === 0) {
return false;
}
ext = ext.toLowerCase();
return (acceptedExts.indexOf(ext) >= 0);
}
fileRowClicked(event) {
if (event.button === 0) {
XUI.Util.consumeEvent(event);
switch (event.detail) {
case 1:
this.fileRowSelected(event);
break;
case 2:
this.okAction();
break;
}
}
}
fileRowSelected(event) {
let elem = event.target;
while (elem !== null) {
if ("xxeFileInfo" in elem) {
this.selectRow(elem.xxeFileInfo.name);
break;
}
if (elem.localName === "tbody") {
break;
}
elem = elem.parentElement;
}
}
selectRow(fileName) {
this.unselectRow();
let row = null;
for (let r of this._fileRows.children) {
if (r.xxeFileInfo && r.xxeFileInfo.name === fileName) {
row = r;
break;
}
}
if (row !== null) {
row.classList.add("xxe-rfd-sel-file");
row.scrollIntoViewIfNeeded( true);
if (!row.xxeFileInfo.directory) {
this._fileNameText.value = fileName;
}
}
}
unselectRow() {
let sel = this._fileRows.querySelector(".xxe-rfd-sel-file");
if (sel !== null) {
sel.classList.remove("xxe-rfd-sel-file");
}
}
fileNameTextFocused(event) {
this.unselectRow();
}
fileTypeSelected(event) {
if (this._selectingFileType) {
return;
}
let selectedFolder = this.getSelectedFolder();
if (selectedFolder === null) {
return null;
}
this.updateFileRows(selectedFolder.listing,
this.getAcceptedExtensions());
}
getAcceptedExtensions() {
let sel = this._fileTypeSelector.selectedIndex;
if (sel < 0) {
return null;
}
let extList = this._fileTypeSelector.options[sel].value;
if (extList === "*") {
return null;
}
return extList.split(';');
}
dialogClosed(result) {
this._resolve(result);
}
cancelAction() {
this.close(null);
}
getFileChoice() {
let selectedFolder = this.getSelectedFolder();
if (selectedFolder === null) {
return null;
}
let selectedFileInfo = null;
let sel = this._fileRows.querySelector(".xxe-rfd-sel-file");
if (sel !== null && sel.xxeFileInfo && sel.xxeFileInfo.name) {
selectedFileInfo = sel.xxeFileInfo;
}
if (selectedFileInfo !== null && selectedFileInfo.directory) {
const folderPath = RemoteFileDialog.joinPath(selectedFolder.path,
selectedFileInfo.name);
this.selectFolder(this._selectedDrive, folderPath);
return null;
}
let fileInfo = null;
let fileName = this._fileNameText.value.trim();
if (fileName.length === 0 && selectedFileInfo !== null) {
fileName = selectedFileInfo.name;
fileInfo = selectedFileInfo;
}
if (fileName.length === 0 ||
fileName.indexOf('/') >= 0) {
XUI.Util.badTextField(this._fileNameText);
return null;
}
if (fileInfo === null) {
fileInfo = RemoteFileDialog.getFileInfo(selectedFolder, fileName);
}
let choice = {};
if (fileInfo === null) {
if (fileName.indexOf('*') >= 0) {
let ext = null;
if (fileName === "*") {
fileName = "*.*";
}
if (fileName.startsWith("*.")) {
ext = URIUtil.pathExtension(fileName, '/');
if (ext === null || ext.length === 0) {
ext = null;
}
}
if (ext === null) {
XUI.Util.badTextField(this._fileNameText);
} else {
this._fileNameText.value = "";
this.selectFileType(ext);
this.fileTypeSelected( null);
}
return null;
}
if (!this._saveMode) {
XUI.Util.badTextField(this._fileNameText);
return null;
}
} else {
if (fileInfo.directory) {
this._fileNameText.value = "";
const folderPath =
RemoteFileDialog.joinPath(selectedFolder.path, fileName);
this.selectFolder(this._selectedDrive, folderPath);
return null;
}
if (this._saveMode) {
choice.confirmCanOverwrite = true;
}
}
choice.uri = RemoteFileDialog.joinURI(this._selectedDrive,
selectedFolder.path, fileName);
if (this._optionName !== null) {
choice[this._optionName] = this._optToggle.checked;
}
return choice;
}
okAction() {
let choice = this.getFileChoice();
if (choice === null) {
return;
}
window.localStorage.setItem("XXE.RemoteFileDialog.lastSelectedURI",
choice.uri);
if (choice.confirmCanOverwrite) {
delete choice.confirmCanOverwrite;
return XUI.Confirm.showConfirm(
`File\n${choice.uri}\nalready exists. Overwrite?`,
this._xmlEditor)
.then((confirmed) => {
if (confirmed) {
this.close(choice);
}
});
} else {
this.close(choice);
}
}
}
RemoteFileDialog.TEMPLATE = document.createElement("template");
RemoteFileDialog.TEMPLATE.innerHTML = `
<div class="xxe-rfd-pane">
  <div class="xui-control xxe-rfd-drive-pane">
  </div>
  <div class="xxe-rfd-chooser-pane">
    <div class="xxe-rfd-folder-pane">
      <span class="xxe-rfd-folder-label">Folder:</span>
      <select autocomplete="off"
        class="xui-control xxe-rfd-folder-select"></select>
      <button disabled="disabled" title="Up one level"
              class="xui-control xui-control-disabled
                     xui-small-icon xxe-rfd-folder-up">
        <span class="xxe-rfd-folder-icon"></span><span
          class="xxe-rfd-folder-badge"></span>
      </button>
      <button disabled="disabled" title="Create new folder"
              class="xui-control xui-control-disabled
                     xui-small-icon xxe-rfd-folder-new">
        <span class="xxe-rfd-folder-icon"></span><span
          class="xxe-rfd-folder-badge"></span>
      </button>
    </div>
    <div class="xxe-rfd-file-scroll">
      <table class="xxe-rfd-file-table">
        <colgroup class="xxe-rfd-ncol"></colgroup>
        <colgroup class="xxe-rfd-scol"></colgroup>
        <colgroup class="xxe-rfd-dcol"></colgroup>
        <thead><th>Name</th><th>Size</th><th>Last Modified</th></thead>
        <tbody></tbody>
      </table>
    </div>
    <div class="xxe-rfd-file-form">
      <span class="xxe-rfd-file-nlabel">File name:</span>
      <input type="text" class="xui-control xxe-rfd-file-name"
             spellcheck="false" autocomplete="off" />
      <span class="xxe-rfd-file-tlabel">File type:</span>
      <select autocomplete="off"
        class="xui-control xxe-rfd-file-type"></select>
    </div>
    <div class="xxe-rfd-opt-pane">
      <label class="xxe-rfd-opt-label">
        <input type="checkbox" class="xxe-rfd-opt-toggle"/>
      </label>
    </div>
  </div>
</div>
`;
export class RemoteFile extends Resource {
constructor(uri, data) {
super(uri, data);
}
}
export class RemoteFiles extends ResourceStorage {
constructor(xmlEditor) {
super(xmlEditor);
}
loadResource(uri) {
return this._xmlEditor.sendRequest("loadResource", uri)
.then((data) => {
if (data) {
return new RemoteFile(uri, data);
} else {
return null;
}
},
(error) => {
throw new Error(
`Cannot load resource "${uri}": ${error}`);
});
}
storeResource(data, uri) {
return this._xmlEditor.sendRequest("storeResource", data, uri)
.then((done) => {
if (done) {
return new RemoteFile(uri, data);
} else {
return null;
}
},
(error) => {
throw new Error(
`Cannot store resource '${uri}': ${error}`);
});
}
openResource(options) {
let uri = [ "???" ];
return RemoteFileDialog.showDialog(this._xmlEditor, options)
.then((choice) => {
if (choice === null) {
return null;
} else {
uri[0] = choice.uri;
return this._xmlEditor.sendRequest("loadResource", uri[0]);
}
})
.then((data) => {
if (data === null) {
return null;
} else {
return new RemoteFile(uri[0], data);
}
})
.catch((error) => {
throw new Error(`Cannot load resource "${uri[0]}": ${error}`);
});
}
}
const PLATFORM_HAS_SYSTEM_SELECTION = PLATFORM_IS_LINUX;
const USER_INPUT_MODIFIER_SHIFT = 0x1;
const USER_INPUT_MODIFIER_CTRL = 0x2;
const USER_INPUT_MODIFIER_ALT = 0x4;
const USER_INPUT_MODIFIER_META = 0x8;
const USER_INPUT_MODIFIER_MOD = (PLATFORM_IS_MAC_OS? USER_INPUT_MODIFIER_META :
USER_INPUT_MODIFIER_CTRL);
const USER_INPUT_MOUSE_DOWN = 1;
const USER_INPUT_MOUSE_DRAG = 2;
const USER_INPUT_MOUSE_UP = 3;
const USER_INPUT_MOUSE_CLICK = 4;
const USER_INPUT_CONTEXT_MENU = 5;
const USER_INPUT_KEY_DOWN = 10;
const USER_INPUT_KEY_PRESS = 11;
const USER_INPUT_MOUSE_BUTTON1 = 1;
const USER_INPUT_MOUSE_BUTTON2 = 4;
const USER_INPUT_MOUSE_BUTTON3 = 2;
const USER_INPUT_APP_EVENT = 100;
class UserInput {
constructor(type, modifiers) {
this._type = type;
this._modifiers = modifiers;
}
get type() {
return this._type;
}
get modifiers() {
return this._modifiers;
}
static toModifiers(shiftKey, ctrlKey,  altKey, metaKey) {
let modifiers = 0x0;
if (shiftKey) {
modifiers |= USER_INPUT_MODIFIER_SHIFT;
}
if (ctrlKey) {
modifiers |= USER_INPUT_MODIFIER_CTRL;
}
if (altKey) {
modifiers |= USER_INPUT_MODIFIER_ALT;
}
if (metaKey) {
modifiers |= USER_INPUT_MODIFIER_META;
}
return modifiers;
}
get id() {
throw new Error("Must be implemented by subclass");
}
getLabel() {
if (this._modifiers === 0x0) {
return this.doGetLabel();
} else {
let modifPrefix = "";
if ((this._modifiers & USER_INPUT_MODIFIER_META) !== 0) {
if (modifPrefix.length > 0) {
modifPrefix += "+";
}
modifPrefix += PLATFORM_IS_MAC_OS? "Cmd" : "Meta";
}
if ((this._modifiers & USER_INPUT_MODIFIER_CTRL) !== 0) {
if (modifPrefix.length > 0) {
modifPrefix += "+";
}
modifPrefix += "Ctrl";
}
if ((this._modifiers & USER_INPUT_MODIFIER_ALT) !== 0) {
if (modifPrefix.length > 0) {
modifPrefix += "+";
}
modifPrefix += PLATFORM_IS_MAC_OS? "Opt" : "Alt";
}
if ((this._modifiers & USER_INPUT_MODIFIER_SHIFT) !== 0) {
if (modifPrefix.length > 0) {
modifPrefix += "+";
}
modifPrefix += "Shift";
}
return modifPrefix + "-" + this.doGetLabel();
}
}
doGetLabel() {
return "???";
}
}
class MouseInput extends UserInput {
constructor(type, button, modifiers=0x0, clickCount=0) {
super(type, modifiers);
this._button = button;
if (type === USER_INPUT_MOUSE_CLICK) {
if (clickCount > 4) {
clickCount = 4;
} else if (clickCount <= 0) {
clickCount = 1;
}
} else {
clickCount = 0;
}
this._clickCount = clickCount;
this._id = MouseInput.computeId(type, button, modifiers, clickCount);
}
get button() {
return this._button;
}
get clickCount() {
return this._clickCount;
}
get id() {
return this._id;
}
static toId(type, button, shiftKey, ctrlKey,  altKey, metaKey,
clickCount) {
let modifiers =
UserInput.toModifiers(shiftKey, ctrlKey,  altKey, metaKey);
return MouseInput.computeId(type, button, modifiers, clickCount);
}
static computeId(type, button, modifiers, clickCount) {
if (type === USER_INPUT_MOUSE_CLICK) {
if (clickCount > 4) {
clickCount = 4;
} else if (clickCount <= 0) {
clickCount = 1;
}
} else {
clickCount = 0;
}
return String(type | (modifiers<<8) | (button<<16) | (clickCount<<24));
}
doGetLabel() {
let label;
switch (this.type) {
case USER_INPUT_MOUSE_DOWN:
label = "Down";
break;
case USER_INPUT_MOUSE_DRAG:
label = "Drag";
break;
case USER_INPUT_MOUSE_UP:
label = "Up";
break;
case USER_INPUT_MOUSE_CLICK:
switch (this._clickCount) {
case 1:
label = "Click";
break;
case 2:
label = "Double-Click";
break;
default:
label = String(this._clickCount) + "-Click";
break;
}
break;
case  USER_INPUT_CONTEXT_MENU:
label = "Context-Menu";
break;
default:
label = "?" + String(this.type) + "?";
break;
}
if (this.type !== USER_INPUT_CONTEXT_MENU &&
this._button !== USER_INPUT_MOUSE_BUTTON1) {
label = ((this._button === USER_INPUT_MOUSE_BUTTON3)?
"Right-" : "Middle-") + label;
}
return "Mouse-" + label;
}
}
class KeyDownInput extends UserInput {
constructor(key, modifiers=0x0) {
super(USER_INPUT_KEY_DOWN, modifiers);
this._key = key;
this._id = KeyDownInput.computeId(key, modifiers);
}
get key() {
return this._key;
}
get id() {
return this._id;
}
static toId(key, shiftKey, ctrlKey,  altKey, metaKey) {
let modifiers =
UserInput.toModifiers(shiftKey, ctrlKey,  altKey, metaKey);
return KeyDownInput.computeId(key, modifiers);
}
static computeId(key, modifiers) {
return String(USER_INPUT_KEY_DOWN | (modifiers<<8)) + key.toUpperCase();
}
doGetLabel() {
return (this._key === " ")? "Space" : this._key;
}
}
class KeyPressInput extends UserInput {
constructor(key) {
super(USER_INPUT_KEY_PRESS,  0x0);
this._key = key;
this._id = KeyPressInput.toId(key);
}
get key() {
return this._key;
}
get id() {
return this._id;
}
static toId(key) {
return String(USER_INPUT_KEY_PRESS) + key;
}
doGetLabel() {
return (this._key === "'")? "\"'\"" : ("'" + this._key + "'");
}
}
class AppEvent extends UserInput {
constructor(name) {
super(USER_INPUT_APP_EVENT,  0x0);
this._name = name;
}
get name() {
return this._name;
}
get id() {
return this._name;
}
doGetLabel() {
return this._name;
}
}
export const COMMAND_RESULT_DONE = 0;
export const COMMAND_RESULT_STOPPED = 1;
export const COMMAND_RESULT_CANCELED = 2;
export const COMMAND_RESULT_FAILED = 3;
export class CommandResult {
constructor(status, value=null) {
this._status = status;
this._value = value;
}
get status() {
return this._status;
}
get value() {
return this._value;
}
toString() {
let s = "CommandResult.";
switch (this._status) {
case COMMAND_RESULT_DONE:
s += "DONE";
break;
case COMMAND_RESULT_STOPPED:
s += "STOPPED";
break;
case COMMAND_RESULT_CANCELED:
s += "CANCELED";
break;
case COMMAND_RESULT_FAILED:
s += "FAILED";
break;
default:
s += "???";
break;
}
if (this._value !== null) {
s += '(' + this._value.toString() + ')';
}
return s;
}
static isDone(result) {
return (result !== null && result._status === COMMAND_RESULT_DONE);
}
static failed(value) {
return new CommandResult(COMMAND_RESULT_FAILED, value);
}
static deserialize(data) {
if (Array.isArray(data) &&
data.length === 2 &&
Number.isInteger(data[0])) {
switch (data[0]) {
case COMMAND_RESULT_DONE:
case COMMAND_RESULT_STOPPED:
case COMMAND_RESULT_CANCELED:
case COMMAND_RESULT_FAILED:
data = new CommandResult(data[0], data[1]);
break;
}
}
return data;
}
static formatCommandResult(cmd, result) {
return (result === null)?
`command\n"${cmd}"\ncould not be executed` :
`command\n"${cmd}"\nreturned\n${result}`;
}
}
CommandResult.DONE = new CommandResult(COMMAND_RESULT_DONE);
CommandResult.STOPPED = new CommandResult(COMMAND_RESULT_STOPPED);
CommandResult.CANCELED = new CommandResult(COMMAND_RESULT_CANCELED);
CommandResult.FAILED = new CommandResult(COMMAND_RESULT_FAILED);
export const EXECUTE_TEST = 0;
export const EXECUTE_HELPER = 1;
export const EXECUTE_NORMAL = 2;
export class Command {
constructor() {}
get commandName() {
let name = this.constructor.name;
if (name.endsWith("Cmd") && name.length > 3) {
name = name.charAt(0).toLowerCase() +
name.substring(1, name.length-3);
}
return name;
}
getCommandString(params) {
return Command.joinCmdString(this.commandName, params, ' ');
}
static joinCmdString(cmdName, cmdParam, separ) {
let cmdString = null;
if (cmdName) {
cmdString = cmdName;
if (cmdParam !== null) {
cmdString += separ + cmdParam;
}
}
return cmdString;
}
static splitCmdString(cmdString, separ) {
let cmd;
if (!cmdString) {
cmd = [null, null];
} else {
let cmdName = null;
let cmdParam = null;
let pos = cmdString.indexOf(separ);
if (pos > 0 && pos+1 < cmdString.length) {
cmdName = cmdString.substring(0, pos);
cmdParam = cmdString.substring(pos+1);
} else {
cmdName = cmdString;
}
cmd = [cmdName, cmdParam];
}
return cmd;
}
execute(mode, docView, params, event=null) {
if (mode === EXECUTE_TEST) {
return Promise.resolve(false);
}
Command.consumeEvent(event);
console.error(`Command.execute: NOT IMPLEMENTED: \
command "${this.commandName}" \
(mode=${mode}, params="${params}", event=${event})`);
return Promise.resolve(CommandResult.FAILED);
}
static consumeEvent(event) {
if (event !== null) {
event.preventDefault();
event.stopImmediatePropagation();
}
}
}
export class RemoteCommand extends Command {
constructor(commandName) {
super();
this._commandName = commandName;
}
get commandName() {
return this._commandName;
}
execute(mode, docView, params, event=null) {
if (mode === EXECUTE_TEST) {
return docView.sendExecuteCommand(mode, this._commandName, params)
}
Command.consumeEvent(event);
return docView.sendExecuteCommand(mode, this._commandName, params)
.then((result) => {
return this.commandExecuted(mode, docView, params, result);
});
}
commandExecuted(mode, docView, params, result) {
if (result === null) {
return null;
} else {
switch (result.status) {
case COMMAND_RESULT_DONE:
this.executionDone(mode, docView, params, result.value);
break;
case COMMAND_RESULT_STOPPED:
{
let resume = [null, null];
let stoppedValue = result.value;
if (stoppedValue !== null &&
stoppedValue.startsWith("xxeClientExecuteCommand")) {
stoppedValue = stoppedValue.substring(23).trim();
resume = Command.splitCmdString(stoppedValue, ' ');
}
if (resume[0]) {
return docView.executeCommand(EXECUTE_HELPER,
resume[0], resume[1]);
}
}
this.executionStopped(mode, docView, params, result.value);
break;
case COMMAND_RESULT_FAILED:
this.executionFailed(mode, docView, params, result.value);
break;
}
return result;
}
}
executionDone(mode, docView, params, resultValue) {}
executionStopped(mode, docView, params, resultValue) {
this.reportExecutionError(docView, params, resultValue,
true);
}
executionFailed(mode, docView, params, resultValue) {
this.reportExecutionError(docView, params, resultValue,
false);
}
reportExecutionError(docView, params, resultValue, stopped, quiet=true) {
let msg;
if (stopped || resultValue === null) {
if (quiet) {
return;
}
const cmdString = this.getCommandString(params);
msg = stopped? `Command "${cmdString}" was stopped` :
`Command "${cmdString}" has failed`;
if (resultValue === null) {
msg += ".";
} else {
msg += ":\n";
msg += resultValue;
}
} else {
msg = resultValue;
}
XUI.Alert.showError(msg, docView);
}
}
export class WaitScreen extends XUI.Dialog {
constructor(message, ownerCmd) {
super({ title: null, movable: true, resizable: false, closeable: false,
template: WaitScreen.TEMPLATE, buttons: null });
this._ownerCmd = ownerCmd;
let pane = this.contentPane.firstElementChild;
let icon = pane.firstElementChild;
icon.textContent = XUI.StockIcon["hourglass"];
let msg = pane.lastElementChild;
if (message.startsWith("<html>")) {
message = message.substring(6);
} else {
message = XUI.Util.escapeHTML(message);
if (message.indexOf('\n') >= 0) {
message = message.replaceAll('\n', "<br>");
}
}
msg.innerHTML = message;
}
open(position, reference) {
const dialog = super.open(position, reference);
dialog.removeEventListener("keydown", XUI.Dialogs.onKeydownDialog);
return dialog;
}
dialogClosed(result) {
this._ownerCmd._waitScreen = null;
}
}
WaitScreen.TEMPLATE = document.createElement("template");
WaitScreen.TEMPLATE.innerHTML = `
<div class="xui-alert-pane">
  <div class="xui-alert-icon"></div>
  <div class="xui-alert-message"></div>
</div>
`;
export class LengthyRemoteCommand extends RemoteCommand {
constructor(commandName, waitMessage=null) {
super(commandName);
if (!waitMessage) {
waitMessage = `Executing command "${commandName}".\nPlease wait...`;
}
this._waitMessage = waitMessage;
this._waitScreen = null;
}
execute(mode, docView, params, event=null) {
this.stopWaiting();
if (mode === EXECUTE_TEST) {
return super.execute(mode, docView, params, event);
}
Command.consumeEvent(event);
this._waitScreen = this.createWaitScreen(this._waitMessage);
this._waitScreen.open("center", docView);
let startWaiting = new Promise((resolve, reject) => {
setTimeout(() => { resolve(true); }, 1000 );
});
return startWaiting
.then((done) => {
return super.execute(mode, docView, params, event);
});
}
createWaitScreen(message) {
return new WaitScreen(message, this);
}
commandExecuted(mode, docView, params, result) {
this.stopWaiting();
return super.commandExecuted(mode, docView, params, result);
}
stopWaiting() {
if (this._waitScreen !== null) {
this._waitScreen.close();
this._waitScreen = null;
}
}
}
export class InteractiveRemoteCommand extends RemoteCommand {
constructor(commandName) {
super(commandName);
}
execute(mode, docView, params, event=null) {
if (mode === EXECUTE_TEST) {
return super.execute(mode, docView, params, event);
}
Command.consumeEvent(event);
return docView.sendExecuteCommand(mode, this.commandName, params)
.then((result) => {
if (result !== null &&
result.status === COMMAND_RESULT_STOPPED &&
result.value !== null) {
return new Promise((resolve, reject) => {
this.resumeExecution(mode, docView, params,
result.value, resolve, reject);
});
} else {
return this.commandExecuted(mode, docView, params, result);
}
});
}
resumeExecution(mode, docView, params,
stoppedCommandInfo, resolve, reject) {
resolve(new CommandResult(COMMAND_RESULT_STOPPED, stoppedCommandInfo));
}
}
class Binding {
constructor(userInput, commandName, commandParams, command=null) {
this._userInput = userInput;
this._commandName = commandName;
this._commandParams = commandParams;
this._command = command;
}
get userInput() {
return this._userInput;
}
get commandName() {
return this._commandName;
}
get commandParams() {
return this._commandParams;
}
get command() {
return this._command;
}
getUserInputId() {
let result;
if (Array.isArray(this._userInput)) {
const list = this._userInput;
const count = list.length;
result = "";
for (let i = 0; i < count; ++i) {
if (i > 0) {
result += " ";
}
result += list[i].id;
}
} else {
result = this._userInput.id;
}
return result;
}
getUserInputLabel() {
let result;
if (Array.isArray(this._userInput)) {
const list = this._userInput;
const count = list.length;
result = "";
for (let i = 0; i < count; ++i) {
if (i > 0) {
result += " ";
}
result += list[i].getLabel();
}
} else {
result = this._userInput.getLabel();
}
return result;
}
static fromJSON(data) {
let binding = null;
if (Array.isArray(data) && data.length === 3) {
const keyStrokes = data[0];
let userInput = null;
if (Array.isArray(keyStrokes)) {
const count = keyStrokes.length;
userInput = new Array(count);
for (let i = 0; i < count; ++i) {
userInput[i] = Binding.parseKeyStroke(keyStrokes[i]);
if (userInput[i] === null) {
userInput = null;
break;
}
}
} else {
if (keyStrokes.startsWith("appEvent:")) {
userInput = new AppEvent(keyStrokes.substring(9));
} else {
userInput = Binding.parseKeyStroke(keyStrokes);
}
}
if (userInput !== null &&  data[1] !== null) {
binding = new Binding(userInput, data[1],
data[2]);
}
}
return binding;
}
static parseKeyStroke(keyStroke) {
let userInput = null;
if (keyStroke.length === 1) {
userInput = new KeyPressInput(keyStroke);
} else {
let sep = keyStroke.indexOf('-');
if (sep >= 0 && sep+1 < keyStroke.length) {
let key = keyStroke.substring(sep+1);
let modifiers = 0x0;
if (sep > 0) {
const mods = keyStroke.substring(0, sep).split('+');
for (let mod of mods) {
switch (mod) {
case "Shift":
modifiers |= USER_INPUT_MODIFIER_SHIFT;
break;
case "Control":
modifiers |= USER_INPUT_MODIFIER_CTRL;
break;
case "Alt":
modifiers |= USER_INPUT_MODIFIER_ALT;
break;
case "Meta":
modifiers |= USER_INPUT_MODIFIER_META;
break;
case "Mod":
modifiers |= USER_INPUT_MODIFIER_MOD;
break;
default:
key = null;
break;
}
}
}
if (key !== null) {
userInput = new KeyDownInput(key, modifiers);
}
}
}
return userInput;
}
}
const COMMON_DEFAULT_BINDINGS = [
new Binding(new MouseInput(USER_INPUT_MOUSE_DOWN, USER_INPUT_MOUSE_BUTTON1),
"selectAt", "begin"),
new Binding(new MouseInput(USER_INPUT_MOUSE_DRAG, USER_INPUT_MOUSE_BUTTON1),
"selectAt", "extend"),
new Binding(new MouseInput(USER_INPUT_MOUSE_UP, USER_INPUT_MOUSE_BUTTON1),
"selectAt", "end"),
new Binding(new MouseInput(USER_INPUT_MOUSE_CLICK, USER_INPUT_MOUSE_BUTTON1,
0,  2),
"selectText", "word"),
new Binding(new KeyDownInput("Tab"),
"moveDotTo", "nextTextNode"),
new Binding(new KeyDownInput("Tab", USER_INPUT_MODIFIER_SHIFT),
"moveDotTo", "previousTextNode"),
new Binding(new KeyDownInput("ArrowLeft"),
"moveDotTo", "previousChar"),
new Binding(new KeyDownInput("ArrowLeft", USER_INPUT_MODIFIER_SHIFT),
"selectTo", "previousChar"),
new Binding(new KeyDownInput("ArrowRight"),
"moveDotTo", "nextChar"),
new Binding(new KeyDownInput("ArrowRight", USER_INPUT_MODIFIER_SHIFT),
"selectTo", "nextChar"),
new Binding(new KeyDownInput("ArrowUp"),
"moveDotTo", "previousLine"),
new Binding(new KeyDownInput("ArrowUp", USER_INPUT_MODIFIER_SHIFT),
"selectTo", "previousLine"),
new Binding(new KeyDownInput("ArrowDown"),
"moveDotTo", "nextLine"),
new Binding(new KeyDownInput("ArrowDown", USER_INPUT_MODIFIER_SHIFT),
"selectTo", "nextLine"),
new Binding(new KeyDownInput("Home"),
"moveDotTo", "lineBegin"),
new Binding(new KeyDownInput("Home", USER_INPUT_MODIFIER_SHIFT),
"selectTo", "lineBegin"),
new Binding(new KeyDownInput("End"),
"moveDotTo", "lineEnd"),
new Binding(new KeyDownInput("End", USER_INPUT_MODIFIER_SHIFT),
"selectTo", "lineEnd"),
new Binding(new KeyDownInput("ArrowLeft", USER_INPUT_MODIFIER_MOD),
"moveDotTo", "previousWord"),
new Binding(new KeyDownInput("ArrowLeft",
(USER_INPUT_MODIFIER_SHIFT |
USER_INPUT_MODIFIER_MOD)),
"selectTo", "previousWord"),
new Binding(new KeyDownInput("ArrowRight", USER_INPUT_MODIFIER_MOD),
"moveDotTo", "nextWord"),
new Binding(new KeyDownInput("ArrowRight",
(USER_INPUT_MODIFIER_SHIFT |
USER_INPUT_MODIFIER_MOD)),
"selectTo", "nextWord"),
new Binding(new MouseInput(USER_INPUT_MOUSE_CLICK, USER_INPUT_MOUSE_BUTTON1,
USER_INPUT_MODIFIER_MOD),
"selectNodeAt", "orParent"),
new Binding(new MouseInput(USER_INPUT_MOUSE_CLICK, USER_INPUT_MOUSE_BUTTON1,
USER_INPUT_MODIFIER_SHIFT),
"extendSelectionAt", null),
new Binding(new KeyDownInput("ArrowUp", USER_INPUT_MODIFIER_MOD),
"selectNode", "parentOrNode"),
new Binding(new KeyDownInput("ArrowDown", USER_INPUT_MODIFIER_MOD),
"selectNode", "childOrNone"),
new Binding(new KeyDownInput("ArrowUp",
(USER_INPUT_MODIFIER_SHIFT |
USER_INPUT_MODIFIER_MOD)),
"selectNode", "previousSibling[implicitElement]"),
new Binding(new KeyDownInput("ArrowDown",
(USER_INPUT_MODIFIER_SHIFT |
USER_INPUT_MODIFIER_MOD)),
"selectNode", "nextSibling[implicitElement]"),
new Binding([new KeyDownInput("Escape"),
new KeyDownInput("ArrowLeft")],
"selectNode", "extendToPreviousSiblingOrElement"),
new Binding([new KeyDownInput("Escape"),
new KeyDownInput("ArrowRight")],
"selectNode", "extendToNextSiblingOrElement"),
new Binding([new KeyDownInput("Escape"),
new KeyDownInput("ArrowDown")],
"selectNode", "children[implicitElement]"),
new Binding([new KeyDownInput("Escape"),
new KeyDownInput("Escape")],
"cancelSelection", null),
new Binding(new KeyDownInput("L", USER_INPUT_MODIFIER_MOD),
"center", null),
new Binding(new MouseInput(USER_INPUT_MOUSE_CLICK, USER_INPUT_MOUSE_BUTTON1,
(USER_INPUT_MODIFIER_MOD|
USER_INPUT_MODIFIER_ALT)),
"followLinkAt", null),
new Binding(new KeyDownInput("C", USER_INPUT_MODIFIER_MOD),
"copy", "[implicitElement]"),
new Binding(new KeyDownInput("C",
(USER_INPUT_MODIFIER_SHIFT |
USER_INPUT_MODIFIER_MOD)),
"copyChars", "[separateParagraphs]"),
new Binding(new KeyDownInput("Delete"),
"deleteSelectionOrDeleteChar", null),
new Binding(new KeyDownInput("Backspace"),
"deleteSelectionOrDeleteChar", "backwards"),
new Binding(new KeyDownInput("Delete",
USER_INPUT_MODIFIER_MOD),
"deleteWord", null),
new Binding(new KeyDownInput("Backspace",
USER_INPUT_MODIFIER_MOD),
"deleteWord", "backwards"),
new Binding(new KeyDownInput(" ", USER_INPUT_MODIFIER_CTRL),
"insertString", "\u00A0"),
new Binding(new KeyDownInput("Enter"),
"insertControlChar", "\n"),
new Binding([new KeyDownInput("Escape"),
new KeyDownInput("Tab")],
"insertControlChar", "\t"),
new Binding(new KeyDownInput("K", USER_INPUT_MODIFIER_MOD),
"delete", "[implicitElement]"),
new Binding(new KeyDownInput("X", USER_INPUT_MODIFIER_MOD),
"cut", "[implicitElement]"),
new Binding(new KeyDownInput("V", USER_INPUT_MODIFIER_MOD),
"paste", "toOrInto"),
new Binding(new KeyDownInput("Z", USER_INPUT_MODIFIER_MOD),
"undo", null),
new Binding(new KeyDownInput("Y", USER_INPUT_MODIFIER_MOD),
"redo", null),
new Binding(new KeyDownInput("A", USER_INPUT_MODIFIER_MOD),
"repeat", null),
new Binding(new KeyDownInput("A", (USER_INPUT_MODIFIER_SHIFT |
USER_INPUT_MODIFIER_MOD)),
"listRepeatable", null),
new Binding([new KeyDownInput("Escape"),
new KeyPressInput('x')],
"execute", null),
new Binding([new KeyDownInput("Escape"),
new KeyPressInput('l')],
"convertCase", "lower"),
new Binding([new KeyDownInput("Escape"),
new KeyPressInput('u')],
"convertCase", "upper"),
new Binding([new KeyDownInput("Escape"),
new KeyPressInput('c')],
"convertCase", "capital"),
new Binding([new KeyDownInput("Escape"),
new KeyPressInput('n')],
"insertCharByName", "[DocBookIfNone]"),
new Binding(new MouseInput(USER_INPUT_MOUSE_CLICK,USER_INPUT_MOUSE_BUTTON2),
"pastePrimarySelection", null),
new Binding(new MouseInput(USER_INPUT_CONTEXT_MENU,  0),
"contextualMenu", null),
];
const OTHER_DEFAULT_BINDINGS = [
new Binding(new KeyDownInput("Insert"),
"insertTextOrMoveDot", "after"),
new Binding(new KeyDownInput("Insert", USER_INPUT_MODIFIER_SHIFT),
"insertTextOrMoveDot", "before"),
new Binding(new KeyDownInput("Insert", USER_INPUT_MODIFIER_MOD),
"insertNode", "sameElementAfter[implicitElement]"),
new Binding(new KeyDownInput("Insert",
(USER_INPUT_MODIFIER_SHIFT |
USER_INPUT_MODIFIER_MOD)),
"insertNode", "sameElementBefore[implicitElement]"),
new Binding(new KeyDownInput("ArrowLeft",
(USER_INPUT_MODIFIER_ALT |
USER_INPUT_MODIFIER_CTRL)),
"go", "back"),
new Binding(new KeyDownInput("ArrowRight",
(USER_INPUT_MODIFIER_ALT |
USER_INPUT_MODIFIER_CTRL)),
"go", "forward"),
new Binding(new KeyDownInput("R", (USER_INPUT_MODIFIER_ALT |
USER_INPUT_MODIFIER_CTRL)),
"replace", "[implicitElement]"),
new Binding(new KeyDownInput("I", (USER_INPUT_MODIFIER_ALT |
USER_INPUT_MODIFIER_CTRL)),
"insert", "into"),
new Binding(new KeyDownInput("H", (USER_INPUT_MODIFIER_ALT |
USER_INPUT_MODIFIER_CTRL)),
"insert", "before[implicitElement]"),
new Binding(new KeyDownInput("J", (USER_INPUT_MODIFIER_ALT |
USER_INPUT_MODIFIER_CTRL)),
"insert", "after[implicitElement]"),
new Binding(new KeyDownInput("C", (USER_INPUT_MODIFIER_ALT |
USER_INPUT_MODIFIER_CTRL)),
"convert", "[implicitElement]"),
new Binding(new KeyDownInput("W", (USER_INPUT_MODIFIER_ALT |
USER_INPUT_MODIFIER_CTRL)),
"wrap", "[implicitElement]"),
new Binding(new KeyDownInput("V", (USER_INPUT_MODIFIER_SHIFT |
USER_INPUT_MODIFIER_CTRL)),
"paste", "before[implicitElement]"),
new Binding(new KeyDownInput("V", (USER_INPUT_MODIFIER_ALT |
USER_INPUT_MODIFIER_CTRL)),
"paste", "after[implicitElement]"),
new Binding(new KeyDownInput("M", (USER_INPUT_MODIFIER_ALT |
USER_INPUT_MODIFIER_CTRL)),
"editAttributes", "[implicitElement]"),
];
const MAC_DEFAULT_BINDINGS = [
new Binding(new KeyDownInput("F1"),
"insertTextOrMoveDot", "after"),
new Binding(new KeyDownInput("F1", USER_INPUT_MODIFIER_SHIFT),
"insertTextOrMoveDot", "before"),
new Binding([new KeyDownInput("Escape"),
new KeyPressInput('s')],
"insertNode", "sameElementAfter[implicitElement]"),
new Binding([new KeyDownInput("Escape"),
new KeyPressInput('S')],
"insertNode", "sameElementBefore[implicitElement]"),
new Binding([new KeyDownInput("Escape"),
new KeyPressInput('<')],
"go", "back"),
new Binding([new KeyDownInput("Escape"),
new KeyPressInput('>')],
"go", "forward"),
new Binding(new KeyDownInput("R", USER_INPUT_MODIFIER_CTRL),
"replace", "[implicitElement]"),
new Binding(new KeyDownInput("I", USER_INPUT_MODIFIER_CTRL),
"insert", "into"),
new Binding(new KeyDownInput("H", USER_INPUT_MODIFIER_CTRL),
"insert", "before[implicitElement]"),
new Binding(new KeyDownInput("J", USER_INPUT_MODIFIER_CTRL),
"insert", "after[implicitElement]"),
new Binding(new KeyDownInput("T", USER_INPUT_MODIFIER_CTRL),
"convert", "[implicitElement]"),
new Binding(new KeyDownInput("T", (USER_INPUT_MODIFIER_SHIFT |
USER_INPUT_MODIFIER_CTRL)),
"wrap", "[implicitElement]"),
new Binding(new KeyDownInput("U", USER_INPUT_MODIFIER_CTRL),
"paste", "before[implicitElement]"),
new Binding(new KeyDownInput("W", USER_INPUT_MODIFIER_CTRL),
"paste", "after[implicitElement]"),
new Binding(new KeyDownInput("E", USER_INPUT_MODIFIER_CTRL),
"editAttributes", "[implicitElement]"),
];
export const ALL_LOCAL_COMMANDS = {};
class Bindings {
static compile(bindings=null) {
let builtInBindings;
if (PLATFORM_IS_MAC_OS) {
builtInBindings =
COMMON_DEFAULT_BINDINGS.concat(MAC_DEFAULT_BINDINGS);
} else {
builtInBindings =
COMMON_DEFAULT_BINDINGS.concat(OTHER_DEFAULT_BINDINGS);
}
let allBindings = [];
if (bindings === null) {
for (let b of builtInBindings) {
allBindings.push(Bindings.bindCommand(b));
}
} else {
let userInputToBinding = {};
for (let b of builtInBindings) {
userInputToBinding[b.getUserInputId()] = Bindings.bindCommand(b);
}
for (let b of bindings) {
userInputToBinding[b.getUserInputId()] = Bindings.bindCommand(b);
}
allBindings = Object.values(userInputToBinding);
}
return allBindings;
}
static bindCommand(binding) {
let cmd = ALL_LOCAL_COMMANDS[binding.commandName];
if (!cmd) {
cmd = new RemoteCommand(binding.commandName);
}
return new Binding(binding.userInput,
binding.commandName, binding.commandParams, cmd);
}
}
const TRAVERSAL_LEAVE_ELEMENT = "__TRAVERSAL__LEAVE__ELEMENT__";
export class DOMUtil {
static traverse(node, handler) {
switch (node.nodeType) {
case Node.TEXT_NODE:
case Node.PROCESSING_INSTRUCTION_NODE:
case Node.COMMENT_NODE:
return handler(node,  undefined);
case Node.ELEMENT_NODE:
{
let result = handler(node,  true);
if (result !== null) {
return (result === TRAVERSAL_LEAVE_ELEMENT)? null : result;
}
let child = node.firstChild;
while (child !== null) {
result = DOMUtil.traverse(child, handler);
if (result !== null) {
return result;
}
child = child.nextSibling;
}
result = handler(node,  false);
if (result !== null) {
return result;
}
}
return null;
default:
return null;
}
}
static traverseBackwards(node, handler) {
switch (node.nodeType) {
case Node.TEXT_NODE:
case Node.PROCESSING_INSTRUCTION_NODE:
case Node.COMMENT_NODE:
return handler(node,  undefined);
case Node.ELEMENT_NODE:
{
let result = handler(node,  true);
if (result !== null) {
return (result === TRAVERSAL_LEAVE_ELEMENT)? null : result;
}
let child = node.lastChild;
while (child !== null) {
result = DOMUtil.traverseBackwards(child, handler);
if (result !== null) {
return result;
}
child = child.previousSibling;
}
result = handler(node,  false);
if (result !== null) {
return result;
}
}
return null;
default:
return null;
}
}
static traverseFrom(node, handler) {
let result = DOMUtil.traverse(node, handler);
if (result !== null) {
return result;
}
return DOMUtil.traverseAfter(node, handler);
}
static traverseAfter(node, handler) {
let parent = DOMUtil.parentElementWithinView(node);
if (parent === null) {
return null;
}
let sibling = node.nextSibling;
while (sibling !== null) {
let result = DOMUtil.traverse(sibling, handler);
if (result !== null) {
return result;
}
sibling = sibling.nextSibling;
}
for (;;) {
let ancestor = DOMUtil.parentElementWithinView(parent);
if (ancestor === null) {
return null;
}
let from = parent.nextSibling;
if (from !== null) {
return DOMUtil.traverseFrom(from, handler);
}
parent = ancestor;
}
}
static traverseBackwardsFrom(node, handler) {
let result = DOMUtil.traverseBackwards(node, handler);
if (result !== null) {
return result;
}
return DOMUtil.traverseBefore(node, handler);
}
static traverseBefore(node, handler) {
let parent = DOMUtil.parentElementWithinView(node);
if (parent === null) {
return null;
}
let sibling = node.previousSibling;
while (sibling !== null) {
let result = DOMUtil.traverseBackwards(sibling, handler);
if (result !== null) {
return result;
}
sibling = sibling.previousSibling;
}
for (;;) {
let result = handler(parent,  false);
if (result !== null) {
return result;
}
let ancestor = DOMUtil.parentElementWithinView(parent);
if (ancestor === null) {
return null;
}
let from = parent.previousSibling;
if (from !== null) {
return DOMUtil.traverseBackwardsFrom(from, handler);
}
parent = ancestor;
}
}
static parentElementWithinView(node) {
let parent = node.parentElement;
if (parent !== null && (parent instanceof DocumentView)) {
parent = null;
}
return parent;
}
static lookupAncestorByTag(node, ancestorName) {
let found = null;
let ancestor = node.parentElement;
while (ancestor !== null) {
if (ancestor.localName === ancestorName) {
found = ancestor;
break;
}
ancestor = ancestor.parentElement;
}
return found;
}
static textContentLength(node) {
let count = 0;
if (node !== null) {
switch (node.nodeType) {
case Node.TEXT_NODE:
count += node.length;
break;
case Node.ELEMENT_NODE:
{
let child = node.firstChild;
while (child !== null) {
count += DOMUtil.textContentLength(child);
child = child.nextSibling;
}
}
break;
}
}
return count;
}
static getAllTextNodes(node) {
let list = [];
if (node !== null) {
DOMUtil.doGetAllTextNodes(node, list);
}
return list;
}
static doGetAllTextNodes(node, list) {
switch (node.nodeType) {
case Node.TEXT_NODE:
list.push(node)
break;
case Node.ELEMENT_NODE:
{
let child = node.firstChild;
while (child !== null) {
DOMUtil.doGetAllTextNodes(child, list);
child = child.nextSibling;
}
}
break;
}
}
static createElementFromHTML(html) {
let template = document.createElement('template');
template.innerHTML = html;
return template.content.firstElementChild;
}
static isDisplayedNode(node, root) {
let elem = node;
if (node !== null &&
node.nodeType !== Node.ELEMENT_NODE) {
elem = node.parentElement;
}
while (elem !== null && elem !== root) {
const display =
window.getComputedStyle(elem).getPropertyValue("display");
if (display === "none") {
return false;
}
elem = elem.parentElement;
}
return true;
}
static dumpNode(node, dump="", indent=0) {
if (node !== null) {
dump = DOMUtil.indentDumpNode(dump, indent);
switch (node.nodeType) {
case Node.TEXT_NODE:
dump += '"' + node.data + '" (' + node.length + ' chars)\n';
break;
case Node.ELEMENT_NODE:
{
dump += '<' + node.localName;
const attrs = node.attributes;
for (let i = attrs.length-1; i >= 0; --i) {
dump += " " + attrs[i].name +
"='" + attrs[i].value + "'";
}
let child = node.firstChild;
if (child === null) {
dump += ' />\n';
} else {
dump += '>\n';
while (child !== null) {
dump = DOMUtil.dumpNode(child, dump, indent+2);
child = child.nextSibling;
}
dump = DOMUtil.indentDumpNode(dump, indent);
dump += '</' + node.localName + '>\n';
}
}
break;
}
}
return dump;
}
static indentDumpNode(dump, indent) {
while (indent > 0) {
dump += " ";
--indent;
}
return dump;
}
static escapeXML(text, maxCode=0xFFFF) {
let escaped = "";
const count = text.length;
for (let i = 0; i < count; ++i) {
let c = text.charAt(i);
switch (c) {
case "'":
escaped += "&#39;";
break;
case '"':
escaped += "&#34;";
break;
case '<':
escaped += "&#60;";
break;
case '>':
escaped += "&#62;";
break;
case '&':
escaped += "&#38;";
break;
default:
{
const code = c.charCodeAt(0);
if (code > maxCode) {
escaped += "&#";
escaped += String(code);
escaped += ';';
} else {
escaped += c;
}
}
}
}
return escaped;
}
static unescapeXML(text) {
const parseCharRef = (charRef) => {
if (charRef.length >= 2 && charRef.charAt(0) === '#') {
let i;
if (charRef.charAt(1) === 'x') {
i = parseInt(charRef.substring(2), 16);
} else {
i = parseInt(charRef.substring(1));
}
if (isNaN(i) || i <= 0 || i > 0xFFFF) {
return '?';
} else {
return String.fromCharCode(i);
}
} if (charRef === "amp") {
return '&';
} if (charRef === "apos") {
return "'";
} if (charRef === "quot") {
return '"';
} if (charRef === "lt") {
return '<';
} if (charRef === "gt") {
return '>';
} else {
return '?';
}
}
let unescaped = "";
const count = text.length;
for (let i = 0; i < count; ++i) {
let c = text.charAt(i);
if (c === '&') {
let charRef = "";
++i;
while (i < count) {
c = text.charAt(i);
if (c === ';') {
break;
}
charRef += c;
++i;
}
c = parseCharRef(charRef);
}
unescaped += c;
}
return unescaped;
}
}
export class NodeView {
static content(root, uid, reportError=true) {
let content = root.getElementById(uid);
if (content === null && reportError) {
console.error(`NodeView.content: INTERNAL ERROR: \
cannot find node view "${uid}"`);
}
return content;
}
static textualContent(root, uid, reportError=true) {
let content = NodeView.content(root, uid, reportError);
if (content !== null &&
(content.hasAttribute("data-t") ||
content.hasAttribute("data-c") ||
content.hasAttribute("data-p"))) {
return content;
}
return null;
}
static view(root, uid, reportError=true) {
let content = NodeView.content(root, uid, reportError);
if (content !== null) {
return NodeView.contentToView(content, uid);
}
return null;
}
static contentToView(content, uid) {
assertOrError(NodeView.isContent(content));
let parent = content.parentElement;
if (parent !== null &&
(parent.getAttribute("data-we") === uid ||
parent.getAttribute("data-wp") === uid ||
parent.getAttribute("data-wc") === uid)) {
content = parent;
}
return content;
}
static isContent(node) {
return (node !== null &&
node.nodeType === Node.ELEMENT_NODE &&
node.id &&
(node.hasAttribute("data-t") ||
node.hasAttribute("data-e") ||
node.hasAttribute("data-c") ||
node.hasAttribute("data-p") ||
node.hasAttribute("data-re") ||
node.hasAttribute("data-rc") ||
node.hasAttribute("data-rp")));
}
static isTextualContent(node) {
return (node !== null &&
node.nodeType === Node.ELEMENT_NODE &&
node.id &&
(node.hasAttribute("data-t") ||
node.hasAttribute("data-c") ||
node.hasAttribute("data-p")));
}
static isReplacedContent(node) {
return (node !== null &&
node.nodeType === Node.ELEMENT_NODE &&
node.id &&
(node.hasAttribute("data-re") ||
node.hasAttribute("data-rc") ||
node.hasAttribute("data-rp")));
}
static isContentBefore(node, uid) {
return (node !== null &&
node.nodeType === Node.ELEMENT_NODE &&
(node.getAttribute("data-be") === uid ||
node.getAttribute("data-bp") === uid ||
node.getAttribute("data-bc") === uid));
}
static isContentAfter(node, uid) {
return (node !== null &&
node.nodeType === Node.ELEMENT_NODE &&
(node.getAttribute("data-ae") === uid ||
node.getAttribute("data-ap") === uid ||
node.getAttribute("data-ac") === uid));
}
static isContentWrapper(node) {
return (node !== null &&
node.nodeType === Node.ELEMENT_NODE &&
(node.hasAttribute("data-we") ||
node.hasAttribute("data-wp") ||
node.hasAttribute("data-wc")));
}
static isGeneratedContent(node) {
return (node !== null &&
node.nodeType === Node.ELEMENT_NODE &&
(node.hasAttribute("data-be") ||
node.hasAttribute("data-bp") ||
node.hasAttribute("data-bc") ||
node.hasAttribute("data-ae") ||
node.hasAttribute("data-ap") ||
node.hasAttribute("data-ac") ||
node.hasAttribute("data-re") ||
node.hasAttribute("data-rp") ||
node.hasAttribute("data-rc")));
}
static isActualContent(node) {
return (node !== null &&
node.nodeType === Node.ELEMENT_NODE &&
node.id &&
(node.hasAttribute("data-t") ||
node.hasAttribute("data-e") ||
node.hasAttribute("data-p") ||
node.hasAttribute("data-c")));
}
static getUID(view) {
if (view !== null &&
view.nodeType === Node.ELEMENT_NODE) {
if (view.id && NodeView.isContent(view)) {
return view.id;
} else {
for (const attrName of ["data-we", "data-wp", "data-wc"]) {
let uid = view.getAttribute(attrName);
if (uid !== null) {
return uid;
}
}
}
}
return null;
}
static getContent(view) {
if (view !== null &&
view.nodeType === Node.ELEMENT_NODE) {
if (view.id && NodeView.isContent(view)) {
return view;
} else {
for (const attrName of ["data-we", "data-wp", "data-wc"]) {
let uid = view.getAttribute(attrName);
if (uid !== null) {
let child = view.firstChild;
while (child !== null) {
if (child.id === uid) {
return child;
}
child = child.nextSibling;
}
}
}
}
}
return null;
}
static getTextualContent(view) {
let content = NodeView.getContent(view);
if (content !== null &&
(content.hasAttribute("data-t") ||
content.hasAttribute("data-c") ||
content.hasAttribute("data-p"))) {
return content;
}
return null;
}
static getReplacedContent(view) {
let content = NodeView.getContent(view);
if (content !== null &&
(content.hasAttribute("data-re") ||
content.hasAttribute("data-rc") ||
content.hasAttribute("data-rp"))) {
return content;
}
return null;
}
static isView(node) {
if (node !== null &&
node.nodeType === Node.ELEMENT_NODE) {
if (NodeView.isContentWrapper(node)) {
return true;
} else {
let uid = node.id;
if (uid && NodeView.isContent(node)) {
return (NodeView.contentToView(node, uid) === node);
}
}
}
return false;
}
static uidIfElementView(node) {
if (node !== null &&
node.nodeType === Node.ELEMENT_NODE) {
let uid = node.getAttribute("data-we");
if (uid !== null) {
return uid;
}
uid = node.id;
if (uid &&
(node.hasAttribute("data-e") || node.hasAttribute("data-re"))) {
let parent = node.parentElement;
if (parent === null ||
parent.getAttribute("data-we") !== uid) {
return uid;
}
}
}
return null;
}
static getNextViewSibling(view) {
assertOrError(NodeView.isView(view));
let node = view.nextSibling;
while (node !== null) {
if (NodeView.isView(node)) {
return node;
}
node = node.nextSibling;
}
return null;
}
static getPreviousViewSibling(view) {
assertOrError(NodeView.isView(view));
let node = view.previousSibling;
while (node !== null) {
if (NodeView.isView(node)) {
return node;
}
node = node.previousSibling;
}
return null;
}
static getParentView(view) {
assertOrError(NodeView.isView(view));
return NodeView.lookupView(view.parentElement);
}
static lookupView(node) {
let elem = node;
if (node !== null &&
node.nodeType !== Node.ELEMENT_NODE) {
elem = node.parentElement;
}
while (elem !== null) {
let uid = elem.id;
if (uid) {
if (NodeView.isContent(elem)) {
return NodeView.contentToView(elem, uid);
}
} else {
if (NodeView.isContentWrapper(elem)) {
return elem;
}
}
elem = elem.parentElement;
}
return null;
}
static lookupViewPart(node) {
while (node !== null) {
if (node.nodeType === Node.ELEMENT_NODE &&
(NodeView.isContentWrapper(node) ||
NodeView.isGeneratedContent(node) ||
NodeView.isActualContent(node))) {
return node;
}
node = node.parentElement;
}
return null;
}
static lookupTextualContent(node) {
while (node !== null) {
if (NodeView.isTextualContent(node)) {
return node;
}
node = node.parentElement;
}
return null;
}
static findTextualContent(tree) {
if (NodeView.isTextualContent(tree)) {
return tree;
}
let child = tree.firstChild;
while (child !== null) {
if (child.nodeType === Node.ELEMENT_NODE &&
!NodeView.isGeneratedContent(child)) {
let found = NodeView.findTextualContent(child);
if (found !== null) {
return found;
}
}
child = child.nextSibling;
}
return null;
}
static pickNearestTextualContent(tree, clientX, clientY) {
let found = [null, Number.MAX_SAFE_INTEGER];
NodeView.doPickNearestTextualContent(tree, clientX, clientY, found);
return (found[0] === null)? null : found[0];
}
static doPickNearestTextualContent(tree, clientX, clientY, found) {
if (NodeView.isTextualContent(tree)) {
const rects = tree.getClientRects();
const rectCount = rects.length;
for (let i = 0; i < rectCount; ++i) {
const rect = rects[i];
if (rect !== null &&
rect.height > 0 &&
clientY >= rect.top && clientY < rect.bottom) {
let distance = 0;
if (clientX < rect.left) {
distance = rect.left - clientX;
} else if (clientX >= rect.right) {
distance = clientX - rect.right;
}
if (distance < found[1]) {
found[0] = tree;
found[1] = distance;
}
}
}
}
let child = tree.firstChild;
while (child !== null) {
if (child.nodeType === Node.ELEMENT_NODE &&
!NodeView.isGeneratedContent(child)) {
NodeView.doPickNearestTextualContent(child, clientX, clientY,
found);
}
child = child.nextSibling;
}
}
static collectTextualContent(view1, offset1, view2, offset2, collected) {
collected.length = 0;
if (view1 === view2) {
let textContent1 = NodeView.getTextualContent(view1);
if (textContent1 !== null) {
collected.push(textContent1);
}
return (offset1 > offset2);
}
let reversed = false;
if (view1.compareDocumentPosition(view2) &
Node.DOCUMENT_POSITION_PRECEDING) {
reversed = true;
}
if (reversed) {
let tmp = view1; view1 = view2; view2 = tmp;
}
let start = NodeView.getTextualContent(view1);
if (start === null) {
start = view1;
}
let end = NodeView.getTextualContent(view2);
if (end === null) {
end = view2;
}
let range = new Range();
range.setStart(start, 0);
range.setEnd(end, 0);
NodeView.doCollectTextualContent(range.commonAncestorContainer,
start, end, [false], collected);
return reversed;
}
static doCollectTextualContent(tree, start, end, collecting, collected) {
if (collecting[0]) {
if (NodeView.isTextualContent(tree)) {
collected.push(tree);
}
if (tree === end) {
return true;
}
} else {
if (tree === start) {
collecting[0] = true;
if (NodeView.isTextualContent(tree)) {
collected.push(tree);
}
if (tree === end) {
return true;
}
}
}
if (!NodeView.isGeneratedContent(tree)) {
let child = tree.firstChild;
while (child !== null) {
if (child.nodeType === Node.ELEMENT_NODE &&
NodeView.doCollectTextualContent(child, start, end,
collecting, collected)) {
return true;
}
child = child.nextSibling;
}
}
return false;
}
static normalizeTextualContent(textContent) {
assertOrError(NodeView.isTextualContent(textContent));
let child = textContent.firstChild;
if (child === null) {
child = document.createTextNode("");
textContent.appendChild(child);
return;
}
if (child.nextSibling === null &&
child.nodeType === Node.TEXT_NODE) {
return;
}
let needNormalize = 0;
while (child !== null) {
let next = child.nextSibling;
switch (child.nodeType) {
case Node.TEXT_NODE:
++needNormalize;
break;
case Node.ELEMENT_NODE:
{
let inserted = null;
let node = child.firstChild;
if (node !== null) {
if (node.nextSibling === null &&
node.nodeType === Node.TEXT_NODE) {
child.removeChild(node);
inserted = node;
} else {
let text = child.textContent;
if (text !== null && text.length > 0) {
inserted = document.createTextNode(text);
}
}
}
if (inserted !== null) {
textContent.insertBefore(inserted,  child);
++needNormalize;
}
textContent.removeChild(child);
}
break;
}
child = next;
}
if (needNormalize > 1) {
textContent.normalize();
}
}
static textualContentToCharOffset(textContent, offset) {
assertOrError(NodeView.isTextualContent(textContent));
let result = [null, -1];
let node = textContent.firstChild;
if (node === null) {
node = document.createTextNode("");
textContent.appendChild(node);
result[0] = node;
result[1] = 0;
} else {
result[1] = 0;
if (!NodeView.findCharOffset(textContent, offset, result)) {
if (result[0] !== null && result[1] === offset) {
result[1] = result[0].length;
} else {
result[0] = null;
result[1] = -1;
}
}
}
return result;
}
static findCharOffset(tree, offset, result) {
let node = tree.firstChild;
while (node !== null) {
switch (node.nodeType) {
case Node.TEXT_NODE:
{
result[0] = node;
let start = result[1];
result[1] += node.length;
if (offset < result[1]) {
result[1] = offset - start;
return true;
}
}
break;
case Node.ELEMENT_NODE:
if (NodeView.findCharOffset(node, offset, result)) {
return true;
}
break;
}
node = node.nextSibling;
}
return false;
}
static charToTextualContentOffset(pos) {
let done = false;
const node = pos[0];
const offset = pos[1];
pos[0] = null;
pos[1] = -1;
let textContent = NodeView.lookupTextualContent(node);
if (textContent !== null) {
let child = textContent.firstChild;
if (child === null) {
pos[0] = textContent;
pos[1] = 0;
done = true;
} else {
if (child.nextSibling === null &&
child.nodeType === Node.TEXT_NODE) {
pos[0] = textContent;
pos[1] = offset;
done = true;
} else {
let startOffset = [0];
if (NodeView.findNodeStartOffset(textContent, node,
startOffset)) {
pos[0] = textContent;
pos[1] = startOffset[0] + offset;
done = true;
}
}
}
}
return done;
}
static findNodeStartOffset(tree, searchedNode, result) {
let node = tree.firstChild;
while (node !== null) {
if (node === searchedNode) {
return true;
}
switch (node.nodeType) {
case Node.TEXT_NODE:
result[0] += node.length;
break;
case Node.ELEMENT_NODE:
if (NodeView.findNodeStartOffset(node, searchedNode, result)) {
return true;
}
break;
}
node = node.nextSibling;
}
return false;
}
static expandViewBranch(node, docView) {
let elem = node;
if (node !== null &&
node.nodeType !== Node.ELEMENT_NODE) {
elem = node.parentElement;
}
while (elem !== null && elem !== docView) {
let uid = elem.id;
if (uid && NodeView.isContent(elem)) {
NodeView.expandCollapsers(elem, uid);
} else {
uid = null;
for (const attrName of ["data-we", "data-wp", "data-wc"]) {
uid = elem.getAttribute(attrName);
if (uid !== null) {
NodeView.expandCollapsers(elem, uid);
break;
}
}
}
if (uid && elem.hasAttribute("data-collapsible")) {
NodeView.expandCollapsers2(elem, uid);
}
elem = elem.parentElement;
}
}
static expandCollapsers(elem, uid) {
let child = elem.firstChild;
while (child !== null) {
if (child.nodeType === Node.ELEMENT_NODE &&
(NodeView.isContentBefore(child, uid) ||
NodeView.isContentAfter(child, uid))) {
NodeView.doExpandCollapsers(child);
}
child = child.nextSibling;
}
}
static doExpandCollapsers(elem) {
let child = elem.firstChild;
while (child !== null) {
if (child.nodeType === Node.ELEMENT_NODE) {
if ("xxe-collapser" === child.localName) {
if (child.collapsed) {
child.collapsed = false;
}
} else {
NodeView.doExpandCollapsers(child);
}
}
child = child.nextSibling;
}
}
static expandCollapsers2(elem, uid) {
let collapsers = elem.querySelectorAll(`xxe-collapser2[for="${uid}"]`);
const count = collapsers.length;
if (count > 0) {
for (let i = 0; i < count; ++i) {
let collapser = collapsers[i];
if (collapser.collapsed) {
collapser.collapsed = false;
}
}
}
}
static lookupVisibleView(node, docView) {
let elem = node;
if (node !== null &&
node.nodeType !== Node.ELEMENT_NODE) {
elem = node.parentElement;
}
let visibleView = null;
while (elem !== null && elem !== docView) {
let view = null;
let uid = elem.id;
if (uid) {
if (NodeView.isContent(elem)) {
view = NodeView.contentToView(elem, uid);
}
} else {
if (NodeView.isContentWrapper(elem)) {
view = elem;
}
}
if (view !== null) {
let display =
window.getComputedStyle(view).getPropertyValue("display");
if (display) {
if (display === "none") {
visibleView = null;
} else {
if (visibleView === null) {
visibleView = view;
}
}
}
}
elem = elem.parentElement;
}
return visibleView;
}
}
export class Name {
constructor(namespace, localPart) {
if (namespace === null) {
throw new Error("null namespace");
}
if (localPart === null) {
throw new Error("null localPart");
}
this._namespace = namespace.trim();
this._localPart = localPart;
}
get namespace() {
return this._namespace;
}
get localPart() {
return this._localPart;
}
static fromString(spec) {
if (spec === null || (spec = spec.trim()).length === 0) {
return null;
}
let pos = -1;
if (spec.charAt(0) === '{' && (pos = spec.lastIndexOf('}')) > 0) {
if (pos+1 === length) {
return null;
}
let ns = spec.substring(1, pos);
let localPart = spec.substring(pos+1);
if (ns.length === 0) {
if (!Name.isName(localPart)) {
return null;
}
return new Name(Name.NS_NONE, localPart);
} else {
if (ns.trim().length === 0 ||
!Name.isNCName(localPart)) {
return null;
}
return new Name(ns, localPart);
}
} else {
if (spec.startsWith("xml:")) {
let localPart = spec.substring(4);
if (!Name.isNCName(localPart)) {
return null;
}
return new Name(Name.NS_XML, localPart);
} else {
if (!Name.isName(spec)) {
return null;
}
return new Name(Name.NS_NONE, spec);
}
}
}
static isName(spec) {
return (spec !== null &&
spec.length > 0 &&
spec.match(/^[:_\p{L}][:_\p{L}0-9\u00B7.-]*$/u) !== null);
}
static isNCName(spec) {
return (spec !== null &&
spec.length > 0 &&
spec.match(/^[_\p{L}][_\p{L}0-9\u00B7.-]*$/u) !== null);
}
toString() {
if (this._namespace === Name.NS_NONE) {
return this._localPart;
} else if (this._namespace === Name.NS_XML) {
return "xml:" + this._localPart;
}
return '{' + this._namespace + '}' + this._localPart;
}
static parse(qName, isAttribute, nsPrefixes) {
let prefix = null, localPart = null;
let colon = qName.indexOf(':');
if (colon < 0) {
prefix = "";
localPart = qName;
if (!Name.isNCName(localPart)) {
return null;
}
} else {
if (colon === 0 || colon === qName.length-1) {
return null;
}
prefix = qName.substring(0, colon);
localPart = qName.substring(colon+1);
if (!Name.isNCName(prefix) ||
!Name.isNCName(localPart)) {
return null;
}
}
let namespace = null;
if (prefix.length === 0) {
if (isAttribute) {
namespace = Name.NS_NONE;
} else {
namespace = Name.findNS(nsPrefixes, prefix);
if (namespace === null) {
namespace = Name.NS_NONE;
}
}
} else if ("xml" === prefix) {
namespace = Name.NS_XML;
} else {
namespace = Name.findNS(nsPrefixes, prefix);
if (namespace === null) {
return null;
}
}
return new Name(namespace, localPart);
}
static findNS(nsPrefixes, prefix) {
if (nsPrefixes !== null && nsPrefixes.length > 0) {
for (let nsPrefix of nsPrefixes) {
let [pre, ns] = nsPrefix;
if (pre === prefix) {
return ns;
}
}
}
return null;
}
format(isAttribute, nsPrefixes) {
if (this._namespace === Name.NS_NONE) {
return this._localPart;
}
let prefix = null;
if (this._namespace === Name.NS_XML) {
prefix = "xml";
} else {
let count = 0;
if (nsPrefixes !== null && (count = nsPrefixes.length) > 0) {
if (!isAttribute) {
for (let i =  count-1; i >= 0; --i) {
let [pre, ns] = nsPrefixes[i];
if (ns === this._namespace && pre.length === 0) {
prefix = pre;
break;
}
}
}
if (prefix === null) {
for (let i = 0; i < count; ++i) {
let [pre, ns] = nsPrefixes[i];
if (ns === this._namespace && pre.length > 0) {
prefix = pre;
break;
}
}
}
}
if (prefix === null) {
return this.toString();
}
}
if (prefix.length > 0) {
return prefix + ':' + this._localPart;
} else {
return this._localPart;
}
}
}
Name.NS_NONE = "";
Name.NS_XML = "http://www.w3.org/XML/1998/namespace";
class TreeCollapser extends HTMLElement {
constructor() {
super();
this._settingCollapsed = false;
let shadow = this.attachShadow({mode: "open"});
shadow.appendChild(this.getTemplate().content.cloneNode(true));
this._iconSpan = shadow.lastElementChild;
const blockEvent = (event) => {
XUI.Util.consumeEvent(event);
};
for (const eventName of ["mousedown", "mousemove", "mouseup",
"auxclick"]) {
this.addEventListener(eventName, blockEvent);
}
this.addEventListener("click", (e) => {
if (!this.disabled) {
e.preventDefault();
e.stopPropagation();
switch (e.detail) {
case 1:
this.setCollapsed(!this.collapsed,  false);
break;
case 2:
this.setCollapsed(this.collapsed,  true);
break;
}
}
});
this.addEventListener("contextmenu", (e) => {
if (!this.disabled) {
e.preventDefault();
e.stopPropagation();
const menu = XUI.Menu.create([
{ text: "Collapse All", name: "collapseAll" },
{ text: "Expand All", name: "expandAll" }
]);
menu.addEventListener("menuitemselected", (menuEvent) => {
switch (menuEvent.detail.menuItem.name) {
case "collapseAll":
this.setCollapsed(true,  true);
break;
case "expandAll":
this.setCollapsed(false,  true);
break;
}
});
menu.open([e.clientX, e.clientY]);
}
});
}
getTemplate() {
return TreeCollapser.TEMPLATE;
}
setCollapsed(collapsed, deep) {
this.applyCollapsed(collapsed, deep);
let docView = DOMUtil.lookupAncestorByTag(this, "xxe-document-view");
if (docView !== null) {
setTimeout(() => { docView.requestFocus(); }, 0 );
}
}
connectedCallback() {
this.applyCollapsed(this.collapsed,  false);
}
static get observedAttributes() {
return [ "collapsed" ];
}
attributeChangedCallback(attrName, oldVal, newVal) {
if (!this._settingCollapsed) {
this.applyCollapsed(newVal,  false);
}
}
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(val) {
if (val) {
this.setAttribute("disabled", "disabled");
} else {
this.removeAttribute("disabled");
}
}
get collapsed() {
return this.hasAttribute("collapsed");
}
set collapsed(val) {
this.applyCollapsed(val,  false);
}
applyCollapsed(collapsed, deep) {
this.setCollapsedAttribute(collapsed, XUI.StockIcon,
"plus-squared", "minus-squared");
let be = this.parentElement;
if (be !== null && be.hasAttribute("data-be")) {
let e = be.parentElement;
if (e !== null && e.hasAttribute("data-e")) {
let ag = this.findAttributeGroup(be);
if (ag !== null) {
this.setHidden(ag, collapsed);
}
let sibling = be.nextSibling;
while (sibling !== null) {
if (sibling.nodeType === Node.ELEMENT_NODE &&
!sibling.hasAttribute("data-ae")) {
this.setHidden(sibling, collapsed);
if (deep) {
let collapser = this.findCollapser(sibling);
if (collapser !== null) {
collapser.applyCollapsed(collapsed, true);
}
}
}
sibling = sibling.nextSibling;
}
}
}
}
setCollapsedAttribute(collapsed,
iconSet, expandIconName, collapseIconName) {
if (collapsed) {
if (!this.hasAttribute("collapsed")) {
this._settingCollapsed = true;
this.setAttribute("collapsed", "collapsed");
this._settingCollapsed = false;
}
this._iconSpan.textContent = iconSet[expandIconName];
} else {
if (this.hasAttribute("collapsed")) {
this._settingCollapsed = true;
this.removeAttribute("collapsed");
this._settingCollapsed = false;
}
this._iconSpan.textContent = iconSet[collapseIconName];
}
}
setHidden(element, hide) {
if (hide) {
if (!element.classList.contains("xxe-collapsed")) {
element.classList.add("xxe-collapsed");
}
} else {
if (element.classList.contains("xxe-collapsed")) {
element.classList.remove("xxe-collapsed");
}
}
}
findAttributeGroup(element) {
let node = element.firstChild;
while (node !== null) {
if (node.nodeType === Node.ELEMENT_NODE &&
node.classList.contains("xxe-ag")) {
return node;
}
node = node.nextSibling;
}
return null;
}
findCollapser(element) {
if (element !== null &&
element.nodeType === Node.ELEMENT_NODE &&
element.hasAttribute("data-e")) {
let firstChild = element.firstChild;
if (firstChild !== null &&
firstChild.nodeType === Node.ELEMENT_NODE &&
firstChild.hasAttribute("data-be")) {
firstChild = firstChild.firstChild;
if (firstChild !== null &&
firstChild.nodeType === Node.ELEMENT_NODE &&
firstChild.localName === "xxe-collapser") {
return firstChild;
}
}
}
return null;
}
}
TreeCollapser.TEMPLATE = document.createElement("template");
TreeCollapser.TEMPLATE.innerHTML = `
<style>
.collapser {
    display: inline-block;
    width: 14px;
    /* Note that inheritable styles (e.g. color, font-style)
       are also inherited by the Shadow DOM. */
    font-family: "xui-stock-icons";
    font-size: 12px;
    font-style: normal;
    font-weight: normal;
    text-decoration: none;
    color: gray;
    cursor: default;
}
</style>
<span class="collapser"></span>
`;
window.customElements.define("xxe-collapser", TreeCollapser);
const CSSIcon = {
"left-link": "\ue800",
"right-link": "\ue801",
"circle": "\ue802",
"disc": "\ue803",
"square": "\ue804",
"filled-square": "\ue805",
"cog": "\ue806",
"hollow-diamond": "\ue807",
"delete": "\ue808",
"convert": "\ue809",
"replace": "\ue80a",
"wrap": "\ue80b",
"insert": "\ue80c",
"insert-before": "\ue80d",
"insert-after": "\ue80e",
"external-link": "\ue80f",
"file2": "\ue810",
"link": "\ue811",
"line-break": "\ue812",
"left-target": "\ue813",
"image-small": "\ue814",
"no-image-small": "\ue815",
"right-target": "\ue816",
"no-image": "\ue817",
"image": "\ue818",
"key": "\ue819",
"plus": "\ue81a",
"minus": "\ue81b",
"play": "\ue81c",
"new-window": "\ue81d",
"right-arrow": "\ue821",
"right": "\ue822",
"left": "\ue823",
"external-link-small": "\ue827",
"pop-up": "\ue828",
"pop-down": "\ue829",
"pop-se": "\ue82a",
"pop-left": "\ue82b",
"pop-right": "\ue82c",
"collapsed-right": "\ue82d",
"expanded-down": "\ue82e",
"diamond": "\ue82f",
"left-half-disc": "\ue830",
"half-disc-separator": "\ue831",
"right-half-disc": "\ue832",
};
class StyledCollapser extends TreeCollapser {
constructor() {
super();
this._onClickCollapsedContent = this.onClickCollapsedContent.bind(this);
this.unconfigure();
}
onClickCollapsedContent(event) {
event.preventDefault();
event.stopPropagation();
if (!this.disabled && event.detail === 1) {
this.setCollapsed(false,  false);
}
}
getTemplate() {
return StyledCollapser.TEMPLATE;
}
unconfigure() {
this._collapsibleView = null;
this._notCollapsibleHead = 0;
this._notCollapsibleFoot = 0;
this._collapsedContentAlign = null;
this._collapsedContent = null;
this._collapsedIconName = "collapsed-right";
this._expandedIconName = "expanded-down";
}
configure(options) {
this.unconfigure();
let collapsibleInfo = null;
let ancestor = this.parentElement;
while (ancestor !== null) {
collapsibleInfo = ancestor.getAttribute("data-collapsible");
if (collapsibleInfo !== null) {
this._collapsibleView = ancestor;
break;
}
ancestor = ancestor.parentElement;
}
let collapsibleUID = null;
if (this._collapsibleView !== null &&
(collapsibleUID =
NodeView.uidIfElementView(this._collapsibleView)) === null) {
this._collapsibleView = null;
collapsibleInfo = null;
}
let initiallyCollapsed = false;
if (collapsibleInfo !== null) {
let infoList = collapsibleInfo.split(';', 4);
if (infoList.length >= 3) {
initiallyCollapsed = (infoList[0] === "true");
let notCollapsibleHead = parseInt(infoList[1]);
if (!isNaN(notCollapsibleHead) && notCollapsibleHead >= 0) {
this._notCollapsibleHead = notCollapsibleHead;
}
let notCollapsibleFoot = parseInt(infoList[2]);
if (!isNaN(notCollapsibleFoot) && notCollapsibleFoot >= 0) {
this._notCollapsibleFoot = notCollapsibleFoot;
}
if (infoList.length >= 4) {
this._collapsedContentAlign = infoList[3];
if (this._collapsedContentAlign.length === 0) {
this._collapsedContentAlign = null;
}
let pos = -1;
for (let k = 0; k <= 3; ++k) {
pos = collapsibleInfo.indexOf(';', pos+1);
if (pos < 0) {
break;
}
}
if (pos > 0 && pos+1 < collapsibleInfo.length) {
this._collapsedContent =
collapsibleInfo.substring(pos+1);
}
}
}
}
if (options !== null) {
let optionList = options.split(';');
if (optionList.length >= 2) {
let collapsedIconName = optionList[0];
let expandedIconName = optionList[1];
if (collapsedIconName.length > 0 &&
(collapsedIconName in CSSIcon)) {
this._collapsedIconName = collapsedIconName;
}
if (expandedIconName.length > 0 &&
(expandedIconName in CSSIcon)) {
this._expandedIconName = expandedIconName;
}
}
}
if (this._collapsibleView === null) {
this._iconSpan.textContent = CSSIcon[this._expandedIconName];
this.disabled = true;
} else {
this.setAttribute("for", collapsibleUID);
if (initiallyCollapsed) {
this._settingCollapsed = true;
this.setAttribute("collapsed", "collapsed");
this._settingCollapsed = false;
}
}
}
connectedCallback() {
this.configure(this.getAttribute("options"));
super.connectedCallback();
}
applyCollapsed(collapsed, deep) {
if (this._collapsibleView !== null) {
StyledCollapser.doApplyCollapsed(this._collapsibleView, collapsed,
deep);
}
}
static doApplyCollapsed(tree, collapsed, deep) {
let uid = null;
if (tree.hasAttribute("data-collapsible") &&
(uid = NodeView.uidIfElementView(tree)) !== null) {
let collapsers =
tree.querySelectorAll(`xxe-collapser2[for="${uid}"]`);
const count = collapsers.length;
if (count > 0) {
const noCollapsibleChildren =
(collapsers[0].getCollapsibleChildren(tree) === null);
for (let i = 0; i < count; ++i) {
let collapser = collapsers[i];
let expandIcon = collapser._collapsedIconName;
let collapseIcon = collapser._expandedIconName;
if (noCollapsibleChildren) {
expandIcon = collapseIcon = "pop-se";
}
collapser.setCollapsedAttribute(collapsed, CSSIcon,
expandIcon, collapseIcon);
}
collapsers[0].collapseView(tree, collapsed);
}
}
if (deep) {
let child = tree.firstElementChild;
while (child !== null) {
StyledCollapser.doApplyCollapsed(child, collapsed, true);
child = child.nextElementSibling;
}
}
}
collapseView(collapsibleView, collapse) {
if (collapse) {
if (!collapsibleView.hasAttribute("data-collapsed")) {
collapsibleView.setAttribute("data-collapsed", "true");
this.doCollapseView(collapsibleView, true);
}
} else {
if (collapsibleView.hasAttribute("data-collapsed")) {
collapsibleView.removeAttribute("data-collapsed");
this.doCollapseView(collapsibleView, false);
}
}
}
doCollapseView(collapsibleView, collapse) {
let collapsibleChildren = this.getCollapsibleChildren(collapsibleView);
if (collapsibleChildren === null) {
return;
}
for (let child of collapsibleChildren) {
let style = child.getAttribute("style");
if (style === null) {
style = "";
}
if (collapse) {
style += ";display:none";
} else {
if (style.endsWith(";display:none")) {
style = style.substring(0, style.length - 13);
}
}
if (style.length === 0) {
child.removeAttribute("style");
} else {
child.setAttribute("style", style);
}
}
if (this._collapsedContent !== null) {
let lastCollapsibleChild =
collapsibleChildren[collapsibleChildren.length-1];
if (collapse) {
let placeholder = document.createElement("div");
placeholder.innerHTML = this._collapsedContent;
placeholder.onclick = this._onClickCollapsedContent;
const clsList = placeholder.classList;
clsList.add("xxe-collapsed-content");
for (let cls of lastCollapsibleChild.classList.values()) {
if (cls.startsWith("xxe-s")) {
clsList.add(cls);
}
}
if (this._collapsedContentAlign !== null) {
placeholder.style.textAlign = this._collapsedContentAlign;
}
lastCollapsibleChild.parentNode.insertBefore(
placeholder, lastCollapsibleChild.nextElementSibling);
} else {
let placeholder = lastCollapsibleChild.nextElementSibling;
if (placeholder !== null &&
placeholder.classList.contains("xxe-collapsed-content")) {
placeholder.parentNode.removeChild(placeholder);
}
}
}
}
getCollapsibleChildren(collapsibleView) {
const collapsibleContent = NodeView.getContent(collapsibleView);
let collapsibleChildren = [];
let child = collapsibleContent.firstElementChild;
while (child !== null) {
if (this._collapsedContent !== null &&
child.classList.contains("xxe-collapsed-content")) {
child = child.nextElementSibling;
continue;
}
collapsibleChildren.push(child);
child = child.nextElementSibling;
}
const start = this._notCollapsibleHead;
const end = collapsibleChildren.length - this._notCollapsibleFoot;
if (end <= start) {
return null;
} else {
if (end < collapsibleChildren.length) {
collapsibleChildren.splice(end);
}
if (start > 0) {
collapsibleChildren.splice(0, start);
}
return ((collapsibleChildren.length === 0)?
null : collapsibleChildren);
}
}
}
StyledCollapser.TEMPLATE = document.createElement("template");
StyledCollapser.TEMPLATE.innerHTML = `
<style>
.collapser {
    font-family: "xxe-css-icons";
    font-size: 14px;
    font-style: normal;
    font-weight: normal;
    text-decoration: none;
    /*color is inherited*/
    vertical-align: 2px;
    cursor: default;
}
:host([disabled]) {
    color: #A6A6A6;
}
</style>
<span class="collapser"></span>
`;
window.customElements.define("xxe-collapser2", StyledCollapser);
class ReferenceImageDialog extends XUI.Dialog {
static showDialog(imageData, imageURI, resourceType, docURI,
reference=null) {
return new Promise((resolve, reject) => {
let dialog = new ReferenceImageDialog(imageData, imageURI,
resourceType, docURI,
resolve);
dialog.open("center", reference);
});
}
constructor(imageData, imageURI, resourceType, docURI, resolve) {
super({ title: "Change Image",
movable: true, resizable: true, closeable: true,
template: ReferenceImageDialog.TEMPLATE,
buttons: [ { label: "Cancel", action: "cancelAction" },
{ label: "OK", action: "okAction",
default: true } ]});
this._imageData = imageData;
this._resolve = resolve;
let pane = this.contentPane.firstElementChild;
this._refAsToggle = pane.querySelector(".xxe-refimg-refas-toggle");
let id = XUI.Util.uid();
this._refAsToggle.setAttribute("id", id);
let refAsLabel = this._refAsToggle.nextElementSibling;
refAsLabel.setAttribute("for", id);
this._parentURIText = pane.querySelector(".xxe-refimg-parent-uri");
this._fileNameText = pane.querySelector(".xxe-refimg-file-name");
let parentURI, fileName;
if (URIUtil.isAbsoluteURI(imageURI)) {
let uri = URIUtil.relativizeURI(imageURI, docURI);
parentURI = URIUtil.uriParent(uri,  false);
if (parentURI === null) {
parentURI = ".";
}
fileName = URIUtil.uriRawBasename(uri);
} else {
parentURI = null;
fileName = imageURI;
}
let addDataList = true;
if (parentURI !== null) {
this._parentURIText.value = parentURI;
this._parentURIText.setAttribute("readonly", "readonly");
addDataList = false;
}
this.rememberParentURI(parentURI, addDataList);
if (fileName) {
this._fileNameText.value = fileName;
this._fileNameText.setAttribute("readonly", "readonly");
}
let docURILabel = pane.querySelector(".xxe-refimg-doc-uri");
docURILabel.textContent = docURI;
this._embedToggle = pane.querySelector(".xxe-refimg-embed-toggle");
id = XUI.Util.uid();
this._embedToggle.setAttribute("id", id);
let embedLabel = this._embedToggle.nextElementSibling;
embedLabel.setAttribute("for", id);
const mimeType =
!imageData.type? "application/octet-stream" : imageData.type;
const imageSize = URIUtil.formatFileSize(imageData.size);
let embedHTML = XUI.Util.escapeHTML(embedLabel.textContent);
embedHTML += " (<code>";
embedHTML += XUI.Util.escapeHTML(mimeType);
embedHTML += "</code>, ";
if (imageData.size <= 1024*1024) {
embedHTML += imageSize;
} else {
embedHTML += "<strong style='color:red'>" + imageSize + "</strong>";
}
embedHTML += ")";
embedLabel.innerHTML = embedHTML;
if (resourceType === "anyURI") {
this._refAsToggle.checked = true;
} else {
this._embedToggle.checked = true;
this._refAsToggle.disabled = true;
}
}
rememberParentURI(parentURI, addDataList) {
let valueList = XUI.Util.rememberDatalistItem(
"XXE.ReferenceImageDialog.lastParentURIs",
parentURI);
if (addDataList) {
XUI.Util.attachDatalist(this._parentURIText, valueList,
this.contentPane.firstElementChild);
}
}
dialogClosed(imageInfo) {
this._resolve(imageInfo);
}
cancelAction() {
this.close(null);
}
okAction() {
let info = { data: this._imageData };
if (this._embedToggle.checked) {
info.embed = true;
} else {
let parentURI =
ReferenceImageDialog.checkPath(this._parentURIText.value);
if (parentURI.length === 0) {
XUI.Util.badTextField(this._parentURIText);
return;
}
if (parentURI !== ".") {
this.rememberParentURI(parentURI,  false);
}
let fileName =
ReferenceImageDialog.checkPath(this._fileNameText.value);
if (fileName.length === 0 ||
fileName.indexOf('/') >= 0) {
XUI.Util.badTextField(this._fileNameText);
return;
}
if (parentURI === ".") {
info.referenceAs = fileName;
} else {
info.referenceAs = parentURI;
if (!parentURI.endsWith('/')) {
info.referenceAs += "/";
}
info.referenceAs += fileName;
}
}
this.close(info);
}
static checkPath(path) {
path = path.trim();
if (path.length === 0) {
return path;
}
if (URIUtil.FILE_PATH_SEPARATOR !== '/' &&
path.indexOf(URIUtil.FILE_PATH_SEPARATOR) >= 0) {
path = path.replaceAll(URIUtil.FILE_PATH_SEPARATOR, '/');
}
if (path.indexOf(' ') >= 0) {
path = path.replaceAll(' ', "%20");
}
return path;
}
}
ReferenceImageDialog.TEMPLATE = document.createElement("template");
ReferenceImageDialog.TEMPLATE.innerHTML = `
<div class="xxe-refimg-pane">
  <div class="xxe-refimg-refas-pane">
    <input type="radio" name="radio_group" class="xxe-refimg-refas-toggle"/>
    <label class="xxe-refimg-refas-label">Reference image using the
    following URI:</label>
  </div>
  <div class="xxe-refimg-uri-pane">
    <input type="text" size="50" class="xui-control xxe-refimg-parent-uri"
           spellcheck="false" autocomplete="off" />
    <span class="xxe-refimg-uri-separ">/</span>
    <input type="text" size="20" class="xui-control xxe-refimg-file-name"
           spellcheck="false" autocomplete="off" />
  </div>
  <div class="xxe-refimg-uri-hint">A relative URI is relative to<br />
  <span class="xxe-refimg-doc-uri"></span><br />Use
  "<b><tt>.</tt></b>" to specify "same directory".</div>
  <div class="xxe-refimg-embed-pane">
    <input type="radio" name="radio_group" class="xxe-refimg-embed-toggle"/>
    <label class="xxe-refimg-embed-label">Embed image</label>
  </div>
</div>
`;
class ImageViewport extends HTMLElement {
constructor() {
super();
this._xmlEditor = null;
this._setImageParams = null;
this._beginResize = this._doResize = this._endResize = null;
this._editingContextChanged = null;
this._draggedHandle = null;
this._imgMinSize = 3*ImageViewport.RESIZER_SIZE;
this._img = null;
this._imgW0 = this._imgH0 = -1;
this._dragX0 = this._dragY0 = -1;
this._preserveAspect = true;
this._showResizeHandles = this.showResizeHandles.bind(this);
this.addEventListener("click", this._showResizeHandles);
this._chooseImage = this.chooseImage.bind(this);
this.addEventListener("dblclick", this._chooseImage);
this._dragOverViewport = this.dragOverViewport.bind(this);
this.addEventListener("dragover", this._dragOverViewport);
this._dropImage = this.dropImage.bind(this);
this.addEventListener("drop", this._dropImage);
}
get contextualMenuItems() {
if (this.canEdit()) {
return [ { label: "Change Image...",
cmdName: "_chooseImage()",
enabled: true } ];
} else {
return null;
}
}
showResizeHandles(event) {
if (this._beginResize === null) {
this._beginResize = this.beginResize.bind(this);
this._doResize = this.doResize.bind(this);
this._endResize = this.endResize.bind(this);
const docView = this._xmlEditor.documentView;
if (docView.getAppEventBinding("resize-image") === null &&
docView.getAppEventBinding("rescale-image") === null) {
this.removeEventListener("click", this._showResizeHandles);
return;
}
}
let img = event.target;
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey ||
event.detail !== 1) {
return;
}
if (img.localName !== "img") {
let ancestor = img;
img = null;
while (ancestor !== null &&
ancestor.namespaceURI === "http://www.w3.org/2000/svg") {
if (ancestor.localName === "svg") {
img = ancestor;
break;
}
ancestor = ancestor.parentElement;
}
}
if (img === null) {
return;
}
let [imgW, imgH] = ImageViewport.getImageSize(img);
if (imgW < 3*ImageViewport.RESIZER_MIN_SIZE ||
imgH < 3*ImageViewport.RESIZER_MIN_SIZE) {
return;
}
this.enableResize(img, true);
}
static getImageSize(img) {
if (img instanceof HTMLImageElement) {
return [ img.width, img.height ];
} else if (img instanceof SVGSVGElement) {
return [ Math.round(img.width.baseVal.value),
Math.round(img.height.baseVal.value) ];
} else {
return [ -1, -1 ];
}
}
static setImageSize(img, width, height) {
width = Math.round(width);
height = Math.round(height);
if (width > 0 && height > 0) {
if (img instanceof HTMLImageElement) {
img.width = width;
img.height = height;
} else if (img instanceof SVGSVGElement) {
img.width.baseVal.value = width;
img.height.baseVal.value = height;
}
}
}
enableResize(img, enable) {
if (enable) {
if (this._editingContextChanged === null) {
let view = NodeView.lookupView(this);
let uid = null;
if (view === null || (uid = NodeView.getUID(view)) === null) {
return;
}
let resizers = document.createElement("span");
resizers.setAttribute("class", "xxe-imgvp-resizers");
let resizerClassPrefix = "xxe-imgvp-resizer xxe-imgvp-handle-";
let resizerSize = ImageViewport.RESIZER_SIZE;
let [imgW, imgH] = ImageViewport.getImageSize(img);
if (imgW < 5*ImageViewport.RESIZER_SIZE ||
imgH < 5*ImageViewport.RESIZER_SIZE) {
resizerSize = ImageViewport.RESIZER_MIN_SIZE;
resizerClassPrefix =
"xxe-imgvp-min-handle " + resizerClassPrefix;
}
let handles = [ "ne", "se", "sw", "nw" ];
if (imgW >= 5*resizerSize) {
handles.push("n", "s");
}
if (imgH >= 5*resizerSize) {
handles.push("w", "e");
}
for (let handle of handles) {
let resizer = document.createElement("span");
resizer.setAttribute("class",
resizerClassPrefix + handle);
resizer.addEventListener("mousedown", this._beginResize);
resizers.appendChild(resizer);
}
let imgParent = img.parentElement;
imgParent.insertBefore(resizers, img);
imgParent.removeChild(img);
resizers.appendChild(img);
this._editingContextChanged = (event) => {
const items = event.nodePathItems;
const selectedUID = (items === null)? null :
items[items.length-3];
if (selectedUID !== uid) {
this.enableResize(img, false);
}
}
this._xmlEditor.addEventListener("editingContextChanged",
this._editingContextChanged);
}
} else {
if (this._editingContextChanged !== null) {
this._xmlEditor.removeEventListener("editingContextChanged",
this._editingContextChanged);
this._editingContextChanged = null;
let resizers = img.parentElement;
if (resizers.classList.contains("xxe-imgvp-resizers")) {
resizers.removeChild(img);
resizers.parentNode.replaceChild(img, resizers);
}
}
}
}
beginResize(event) {
event.preventDefault();
event.stopImmediatePropagation();
if (event.altKey || event.shiftKey ||
(PLATFORM_IS_MAC_OS && event.ctrlKey) ||
(!PLATFORM_IS_MAC_OS && event.metaKey)) {
return;
}
this._draggedHandle = null;
this._imgMinSize = 3*ImageViewport.RESIZER_SIZE;
for (let cls of event.target.classList.values()) {
if (cls === "xxe-imgvp-min-handle") {
this._imgMinSize = 2*ImageViewport.RESIZER_MIN_SIZE;
} else if (cls.startsWith("xxe-imgvp-handle-")) {
this._draggedHandle = cls.substring(17);
}
}
if (this._draggedHandle === null) {
return;
}
this._img = event.target.parentElement.lastElementChild;
[this._imgW0, this._imgH0] = ImageViewport.getImageSize(this._img);
this._dragX0 = event.clientX;
this._dragY0 = event.clientY;
this._preserveAspect =
!(PLATFORM_IS_MAC_OS? event.metaKey : event.ctrlKey);
const docView = this._xmlEditor.documentView;
if (this._preserveAspect) {
if (docView.getAppEventBinding("rescale-image") === null) {
this._preserveAspect = false;
}
} else {
if (docView.getAppEventBinding("resize-image") === null) {
this._preserveAspect = true;
}
}
document.addEventListener("mousemove", this._doResize);
document.addEventListener("mouseup", this._endResize);
document.addEventListener("mouseleave", this._endResize);
}
doResize(event) {
event.preventDefault();
event.stopImmediatePropagation();
let resizingX = false;
let resizingY = false;
let incrX = 0;
let incrY = 0;
switch (this._draggedHandle) {
case "nw":
incrX = this._dragX0 - event.clientX;
incrY = this._dragY0 - event.clientY;
resizingX = resizingY = true;
break;
case "ne":
incrX = event.clientX - this._dragX0;
incrY = this._dragY0 - event.clientY;
resizingX = resizingY = true;
break;
case "se":
incrX = event.clientX - this._dragX0;
incrY = event.clientY - this._dragY0;
resizingX = resizingY = true;
break;
case "sw":
incrX = this._dragX0 - event.clientX;
incrY = event.clientY - this._dragY0;
resizingX = resizingY = true;
break;
case "n":
incrY = this._dragY0 - event.clientY;
resizingY = true;
break;
case "e":
incrX = event.clientX - this._dragX0;
resizingX = true;
break;
case "s":
incrY = event.clientY - this._dragY0;
resizingY = true;
break;
case "w":
incrX = this._dragX0 - event.clientX;
resizingX = true;
break;
}
let newW = this._imgW0 + incrX;
if (resizingX && newW < this._imgMinSize) {
newW = this._imgMinSize;
incrX = this._imgMinSize - this._imgW0;
}
let newH = this._imgH0 + incrY;
if (resizingY && newH < this._imgMinSize) {
newH = this._imgMinSize;
incrY = this._imgMinSize - this._imgH0;
}
let scale = -1;
if (this._preserveAspect) {
if (resizingX && resizingY) {
if (Math.abs(incrX) >= Math.abs(incrY)) {
scale = newW / this._imgW0;
} else {
scale =  newH / this._imgH0;
}
} else if (resizingX) {
scale = newW / this._imgW0;
} else {
scale = newH / this._imgH0;
}
newW = Math.round(this._imgW0 * scale);
newH = Math.round(this._imgH0 * scale);
}
let [imgW, imgH] = ImageViewport.getImageSize(this._img);
if (newW !== imgW || newH !== imgH) {
ImageViewport.setImageSize(this._img, newW, newH);
}
}
endResize(event) {
this.doResize(event);
const img = this._img;
const preserveAspect = this._preserveAspect;
this._draggedHandle = null;
this._imgMinSize = 3*ImageViewport.RESIZER_SIZE;
this._img = null;
this._imgW0 = this._imgH0 = -1;
this._dragX0 = this._dragY0 = -1;
this._preserveAspect = true;
document.removeEventListener("mousemove", this._doResize);
document.removeEventListener("mouseup", this._endResize);
document.removeEventListener("mouseleave", this._endResize);
this.enableResize(img, false);
const docView = this._xmlEditor.documentView;
let cmdName = null;
let cmdParams = null;
let binding = null;
if (preserveAspect) {
binding = docView.getAppEventBinding("rescale-image");
} else {
binding = docView.getAppEventBinding("resize-image");
}
if (binding !== null) {
cmdName = binding.commandName;
cmdParams = binding.commandParams;
if (cmdParams !== null) {
let [imgW, imgH] = ImageViewport.getImageSize(img);
if (cmdParams.indexOf("%{width}") >= 0) {
cmdParams = cmdParams.replaceAll("%{width}", String(imgW));
}
if (cmdParams.indexOf("%{height}") >= 0) {
cmdParams = cmdParams.replaceAll("%{height}", String(imgH));
}
if (cmdParams.indexOf("%{preserveAspect}") >= 0) {
cmdParams = cmdParams.replaceAll("%{preserveAspect}",
String(preserveAspect));
}
}
}
if (cmdName !== null) {
docView.executeCommand(EXECUTE_NORMAL, cmdName, cmdParams);
}
}
chooseImage(event) {
event.preventDefault();
event.stopPropagation();
let view = NodeView.lookupView(this);
if (view === null) {
return;
}
const xmlEditor = this._xmlEditor;
const docView = xmlEditor.documentView;
let imageResource = [];
return docView.selectNode(view)
.then((selected) => {
if (!selected) {
return null;
} else {
const options = {
title: "Choose Image",
extensions: [ ["Image files",
"image/*",
"png",
"svg",
"jpg", "jpeg", "jpe", "jif", "jfif",
"gif",
"bmp",
"ico",
"webp",
"apng",
"avif"] ]
};
return xmlEditor.openResource(options);
}
})
.then((resource) => {
if (resource === null) {
return null;
} else {
imageResource.push(resource);
return ReferenceImageDialog.showDialog(
resource.data, resource.uri, this._setImageParams[2],
xmlEditor.documentURI, xmlEditor);
}
})
.then((imageInfo) => {
if (imageInfo === null) {
return false;
} else {
if (!imageInfo.embed) {
xmlEditor.cacheResource(imageResource[0]);
}
return this.changeImage(imageInfo, xmlEditor);
}
})
.catch((error) => {
XUI.Alert.showError(`Cannot change image:\n${error}`,
xmlEditor);
return false;
});
}
dragOverViewport(event) {
event.preventDefault();
event.stopPropagation();
event.dataTransfer.dropEffect =
(event.dataTransfer.types.indexOf("Files") >= 0)?
"copyLink" : "none";
}
dropImage(event) {
event.preventDefault();
event.stopPropagation();
let imageFile = ImageViewport.getDroppedImageFile(event);
if (imageFile === null) {
return;
}
let view = NodeView.lookupView(this);
if (view === null) {
return;
}
const xmlEditor = this._xmlEditor;
const docView = xmlEditor.documentView;
let imageInfo = {};
return docView.selectNode(view)
.then((selected) => {
if (!selected) {
return null;
} else {
return ReferenceImageDialog.showDialog(
imageFile, encodeURIComponent(imageFile.name),
this._setImageParams[2],
xmlEditor.documentURI, xmlEditor);
}
})
.then((dialogInfo) => {
if (dialogInfo === null) {
return null;
} else {
Object.assign(imageInfo, dialogInfo);
if (imageInfo.embed) {
return null;
} else {
const uri = URIUtil.resolveURI(imageInfo.referenceAs,
xmlEditor.documentURI);
return xmlEditor.putResource(imageInfo.data, uri);
}
}
})
.then((resource) => {
if (!imageInfo.data) {
return false;
} else {
return this.changeImage(imageInfo, xmlEditor);
}
})
.catch((error) => {
XUI.Alert.showError(`Cannot change image:\n${error}`,
xmlEditor);
return false;
});
}
static getDroppedImageFile(event) {
let imageFile = null;
for (let file of event.dataTransfer.files) {
if (file.type &&
(file.type.startsWith("image/") ||
file.type.startsWith("application/mathml"))) {
imageFile = file;
break;
}
}
return imageFile;
}
changeImage(imageInfo, xmlEditor) {
const params = this._setImageParams.join(' ');
let imageValue = [null];
return ImageViewport.imageValue(imageInfo)
.then((value) => {
imageValue[0] = value;
return xmlEditor.documentView.executeCommand(
EXECUTE_HELPER,
"changeImage", `${params} ${imageValue[0]}`);
})
.then((result) => {
if (CommandResult.isDone(result)) {
return true;
} else {
const reason = CommandResult.formatCommandResult(
`changeImage ${params} ${imageValue[0]}`,
result);
throw new Error(reason);
}
})
.catch((error) => {
XUI.Alert.showError(`Cannot change image:\n${error}`,
xmlEditor);
return false;
});
}
static imageValue(imageInfo) {
if (imageInfo.embed) {
return ImageViewport.blobToDataURL(imageInfo.data);
} else {
let value = imageInfo.referenceAs;
if (value.indexOf("\"") >= 0) {
value = value.replace(/"/g, "\\\"");
            }
            value = "\"" + value + "\"";
return Promise.resolve(value);
}
}
static blobToDataURL(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(reader.result);
reader.onerror = (e) => reject(reader.error);
reader.onabort = (e) => reject(new Error("read aborted"));
reader.readAsDataURL(blob);
});
}
connectedCallback() {
this._xmlEditor = DOMUtil.lookupAncestorByTag(this, "xxe-client");
if (this._xmlEditor === null) {
return;
}
let imgParent = this.firstElementChild;
if (imgParent === null) {
return;
}
this._setImageParams = this.getAttribute("setimageparam");
if (this._setImageParams) {
this._setImageParams = this._setImageParams.split(/\s+/);
}
if (!Array.isArray(this._setImageParams) ||
this._setImageParams.length !== 4) {
this._setImageParams = null;
}
let contentURI = imgParent.getAttribute("data-src");
if (contentURI === null) {
this.setTooltip(imgParent);
return;
}
imgParent.classList.remove("xxe-imgvp-placeholder");
contentURI = URIUtil.csriURLToURI(contentURI);
imgParent.removeAttribute("data-src");
let contentWidth = imgParent.getAttribute("data-cw");
if (contentWidth !== null) {
imgParent.removeAttribute("data-cw");
}
let contentHeight = imgParent.getAttribute("data-ch");
if (contentHeight !== null) {
imgParent.removeAttribute("data-ch");
}
let preserveAspectRatio = true;
let par = imgParent.getAttribute("data-par");
if (par !== null) {
preserveAspectRatio = (par !== "no");
imgParent.removeAttribute("data-par");
}
setTimeout(() => {
this.addImg(this._xmlEditor,
contentURI, contentWidth, contentHeight,
preserveAspectRatio, imgParent);
}, 0 );
}
setTooltip(imgParent) {
if (this.canEdit()) {
let tooltip = imgParent.getAttribute("data-label");
if (!tooltip) {
tooltip = "";
} else {
tooltip += "\n\n";
imgParent.setAttribute("data-label", "");
}
tooltip += "\u2022 Drop a file here to change the image.\n\
\u2022 Double-click to change the image.";
this.setAttribute("title", tooltip);
} else {
this.removeEventListener("click", this._showResizeHandles);
this.removeEventListener("dblclick", this._chooseImage);
this.removeEventListener("dragover", this._dragOverViewport);
this.removeEventListener("drop", this._dropImage);
}
}
canEdit() {
return (Array.isArray(this._setImageParams) &&
this._setImageParams.length === 4);
}
async addImg(xmlEditor,
contentURI, contentWidth, contentHeight, preserveAspectRatio,
imgParent) {
await this.doAddImg(xmlEditor, contentURI, contentWidth, contentHeight,
preserveAspectRatio, imgParent);
this.setTooltip(imgParent);
}
async doAddImg(xmlEditor,
contentURI, contentWidth, contentHeight, preserveAspectRatio,
imgParent) {
let res = null;
let getError = null;
try {
res = await xmlEditor.getResource(contentURI);
} catch (error) {
getError = error;
}
if (res === null) {
if (getError === null) {
getError = new Error("null resource");
}
ImageViewport.noImg(getError, imgParent);
return;
}
if (res.data === null) {
imgParent.classList.add("xxe-imgvp-placeholder");
imgParent.textContent = CSSIcon["image"];
return;
}
let img = document.createElement("img");
const imgURL = URL.createObjectURL(res.data);
img.src = imgURL;
let displayError = null;
try {
await img.decode();
if (img.naturalWidth <= 0 || img.naturalHeight <= 0) {
displayError =
"the intrinsic dimensions of the image are not known";
}
} catch (error) {
displayError = error.message;
} finally {
URL.revokeObjectURL(imgURL);
}
if (displayError !== null) {
ImageViewport.noImg(
`Cannot display image "${contentURI}":\n${displayError}`,
imgParent);
return;
}
if (contentWidth !== null || contentHeight !== null) {
let [width, widthType] =
ImageViewport.parseSizeSpec(contentWidth);
let [height, heightType] =
ImageViewport.parseSizeSpec(contentHeight);
let viewportWidth = 0;
if (this.style.getPropertyValue("width")) {
viewportWidth = this.clientWidth;
}
let viewportHeight = 0;
if (this.style.getPropertyValue("height")) {
viewportHeight = this.clientHeight;
}
let size = ImageViewport.computeScaledSize(
viewportWidth, viewportHeight,
img.naturalWidth, img.naturalHeight,
width, widthType, height, heightType, preserveAspectRatio);
if (size !== null) {
img.width = size[0];
img.height = size[1];
}
}
imgParent.replaceChildren(img);
}
static noImg(error, imgParent) {
imgParent.setAttribute("data-label", error);
imgParent.classList.add("xxe-imgvp-error");
imgParent.textContent = CSSIcon["no-image"];
}
static parseSizeSpec(spec) {
let size = -1;
let sizeType = null;
if (spec !== null) {
if (spec.endsWith("px")) {
sizeType = "px";
size = Number(spec.substring(0, spec.length-2));
} else if (spec.endsWith("%")) {
sizeType = "%";
size = Number(spec.substring(0, spec.length-1));
} else if (spec.endsWith("max")) {
sizeType = "max";
size = Number(spec.substring(0, spec.length-3));
} else if (spec === "scale-to-fit") {
sizeType = "fit";
size = 1;
}
if (isNaN(size) || size <= 0) {
size = -1;
}
}
return [ size, sizeType ];
}
static computeScaledSize(viewportWidth, viewportHeight,
imageWidth, imageHeight,
width, widthType, height, heightType,
preserveAspectRatio) {
let specWidth = -1;
let specHeight = -1;
if (width > 0) {
switch (widthType) {
case "px":
specWidth = width;
break;
case "%":
specWidth =
(imageWidth * width) / 100.0;
break;
case "fit":
specWidth = viewportWidth;
break;
case "max":
specWidth = Math.min(imageWidth, width);
break;
}
}
if (height > 0) {
switch (heightType) {
case "px":
specHeight = height;
break;
case "%":
specHeight =
(imageHeight * height) / 100.0;
break;
case "fit":
specHeight = viewportHeight;
break;
case "max":
specHeight = Math.min(imageHeight, height);
break;
}
}
let scaledWidth = -1;
let scaledHeight = -1;
let scaleX, scaleY;
if (specWidth <= 0) {
if (specHeight <= 0) {
return null;
} else {
scaleY = specHeight / imageHeight;
scaledWidth = Math.round(imageWidth * scaleY);
scaledHeight = Math.round(imageHeight * scaleY);
}
} else {
if (specHeight <= 0) {
scaleX = specWidth / imageWidth;
scaledWidth = Math.round(imageWidth * scaleX);
scaledHeight = Math.round(imageHeight * scaleX);
} else {
if (((widthType === "fit" && heightType === "fit") ||
(widthType === "max" && heightType === "max")) &&
preserveAspectRatio) {
scaleX = specWidth / imageWidth;
scaleY = specHeight / imageHeight;
let scale = Math.min(scaleX, scaleY);
scaledWidth = Math.round(imageWidth * scale);
scaledHeight = Math.round(imageHeight * scale);
} else {
scaledWidth = specWidth;
scaledHeight = specHeight;
}
}
}
return [ scaledWidth, scaledHeight ];
}
disconnectedCallback() {
let img = this.getElementsByTagName("img");
if (img.length === 1) {
this.enableResize(img.item(0), false);
}
}
}
ImageViewport.RESIZER_MIN_SIZE = 5;
ImageViewport.RESIZER_SIZE = 2*ImageViewport.RESIZER_MIN_SIZE;
window.customElements.define("xxe-image-viewport", ImageViewport);
class EditMathSourceDialog extends XUI.Dialog {
static showDialog(mathNotation, mathSource, readOnly, reference=null) {
return new Promise((resolve, reject) => {
let dialog = new EditMathSourceDialog(mathNotation, mathSource,
readOnly, resolve);
dialog.open("center", reference);
});
}
constructor(mathNotation, mathSource, readOnly, resolve) {
super({ title: `Edit ${mathNotation}`,
movable: true, resizable: true, closeable: true,
template: EditMathSourceDialog.TEMPLATE,
buttons: [ { label: "Cancel", action: "cancelAction" },
{ label: "OK", action: "okAction" } ]});
this._mathSource = mathSource.trim();
this._resolve = resolve;
this._mathArea = this.contentPane.firstElementChild.firstElementChild;
this._mathArea.value = mathSource;
this._mathArea.setSelectionRange(0, 0);
if (readOnly) {
this._mathArea.readOnly = readOnly;
const okButton = this.buttons[1];
okButton.disabled = true;
okButton.classList.add("xui-control-disabled");
}
}
dialogClosed(mathSource) {
this._resolve(mathSource);
}
cancelAction() {
this.close(null);
}
okAction() {
let editedSource = this._mathArea.value.trim();
if (editedSource.length === 0) {
this._mathArea.focus();
return;
}
if (editedSource === this._mathSource) {
editedSource = null;
}
this.close(editedSource);
}
}
EditMathSourceDialog.TEMPLATE = document.createElement("template");
EditMathSourceDialog.TEMPLATE.innerHTML = `<div class="xxe-edmath-pane">
  <textarea class="xui-control xxe-edmath-source"
    rows="20" cols="70" wrap="off"
    autocomplete="off" spellcheck="false"></textarea>
</div>`;
class MathJaxPane extends HTMLElement {
constructor() {
super();
this._xmlEditor = null;
this._mathNotation = null;
this._mathStartTag = null;
this._mathEndTag = null;
this._mathSource = null;
this._editMathSource = this.editMathSource.bind(this);
this.addEventListener("dblclick", this._editMathSource);
}
get contextualMenuItems() {
return [ { label: `Edit ${this._mathNotation}...`,
cmdName: "_editMathSource()",
enabled: true } ];
}
editMathSource(event) {
event.preventDefault();
event.stopPropagation();
let view = NodeView.lookupView(this);
if (view === null) {
return;
}
const xmlEditor = this._xmlEditor;
const docView = xmlEditor.documentView;
docView.selectNode(view)
.then((selected) => {
if (!selected) {
return null;
} else {
return EditMathSourceDialog.showDialog(
this._mathNotation, this._mathSource,
!docView.canEdit(), xmlEditor);
}
})
.then((mathSource) => {
if (mathSource === null) {
return CommandResult.CANCELED;
} else {
let pasteParam = "to <?xml version='1.0'?>";
if (this._mathNotation === "MathML") {
pasteParam += mathSource.replaceAll(/>\s+</g, "><");
} else {
pasteParam += this._mathStartTag;
pasteParam += DOMUtil.escapeXML(mathSource);
pasteParam += this._mathEndTag;
}
return docView.executeCommand(EXECUTE_HELPER,
"paste", pasteParam);
}
})
.then((result) => {
if (result === null ||
result.status === COMMAND_RESULT_FAILED ||
result.status === COMMAND_RESULT_STOPPED) {
let msg;
if (result === null) {
msg = `Could not change ${this._mathNotation}.`;
} else {
msg = `Failed to change ${this._mathNotation}:
${result}`;
}
XUI.Alert.showError(msg, xmlEditor);
}
});
}
connectedCallback() {
this._xmlEditor = DOMUtil.lookupAncestorByTag(this, "xxe-client");
if (this._xmlEditor === null) {
return;
}
this._mathNotation = null;
this._mathStartTag = null;
this._mathEndTag = null;
this._mathSource = null;
const replacedSrc = this.getAttribute("replace");
if (replacedSrc) {
this.removeAttribute("replace");
if (replacedSrc.match(/^<([\w]+:)?math\s/)) {
this._mathNotation = "MathML";
this._mathSource = replacedSrc;
} else if (replacedSrc.startsWith("<")) {
this._mathNotation = "TeX/LaTeX math";
if (replacedSrc.endsWith("/>")) {
this._mathStartTag =
replacedSrc.substring(0, replacedSrc.length-2).trim() +
">";
let end = replacedSrc.indexOf(' ', 1);
if (end < 0) {
end = replacedSrc.indexOf('/', 1);
}
if (end > 1) {
this._mathEndTag =
"</" + replacedSrc.substring(1, end) + ">";
this._mathSource = "";
}
} else {
const start = replacedSrc.indexOf('>');
if (start > 0) {
const end = replacedSrc.lastIndexOf("</");
if (end >= start+1) {
this._mathSource = DOMUtil.unescapeXML(
replacedSrc.substring(start+1, end).trim());
this._mathStartTag =
replacedSrc.substring(0, start+1);
this._mathEndTag = replacedSrc.substring(end);
}
}
}
}
}
if (this._mathSource === null) {
return;
}
this.setAttribute("title",
`Double-click to edit ${this._mathNotation}.`);
let addMathJax = true;
if (document.getElementById("xxe-mathjax-script") !== null) {
addMathJax = false;
} else {
const head = document.head;
let child = head.firstElementChild;
while (child !== null) {
if (child.localName === "script") {
const src = child.getAttribute("src");
if (src && src.indexOf("mathjax") > 0) {
addMathJax = true;
break;
}
}
child = child.nextElementSibling;
}
if (addMathJax) {
let script = document.createElement("script");
script.setAttribute("type", "text/javascript");
script.textContent = `MathJax = {
    options: { enableMenu: false },
    loader: { load: ['[tex]/color'] },
    tex: { packages: { '[+]': ['color'] } }
};`;
head.appendChild(script);
script = document.createElement("script");
script.setAttribute("id", "xxe-mathjax-script");
script.setAttribute("type", "text/javascript");
script.setAttribute("src",
"https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js");
head.appendChild(script);
}
}
if (!addMathJax &&
window.MathJax.typesetPromise) {
window.MathJax.typesetPromise();
}
}
}
window.customElements.define("xxe-mathjax-pane", MathJaxPane);
class CommandButton extends HTMLElement {
constructor() {
super();
this._docView = null;
this._options = this._optionDefaults = {
"type": "command",
"text": null,
"tool-tip": null,
"command": null,
"parameter": null,
"menu": null,
"menu-at-left": false,
"icon-gap": 4,
"icon-position": "left",
"select": true,
};
const blockEvent = (event) => {
XUI.Util.consumeEvent(event);
};
for (const eventName of ["mousedown", "mousemove", "mouseup",
"auxclick"]) {
this.addEventListener(eventName, blockEvent);
}
this._onButtonClicked = this.onButtonClicked.bind(this);
this.addEventListener("click", this._onButtonClicked);
this.addEventListener("contextmenu", this._onButtonClicked);
}
onButtonClicked(event) {
XUI.Util.consumeEvent(event);
if (this._docView === null ||
(event.type === "click" && event.detail !== 1)) {
return;
}
let view = NodeView.lookupView(this);
if (view === null || NodeView.uidIfElementView(view) === null) {
return;
}
let selectElem;
if (this._options["select"] &&
(!Object.is(this._docView.selected, view) ||
this._docView.selected2 !== null)) {
selectElem = this._docView.selectNode(view,  false);
} else {
selectElem = Promise.resolve(true);
}
selectElem.then((selected) => {
if (selected) {
const type = this._options["type"];
const cmdName = this._options["command"];
const cmdParam = this._options["parameter"];
const menu = this._options["menu"];
if (event.type === "contextmenu" ||
(event.type === "click" && cmdName === null)) {
if (type && CommandButton.isMenuSpec(menu)) {
let menuPos, menuRef;
if (event.type === "contextmenu") {
menuPos = [event.clientX, event.clientY];
menuRef = null;
} else {
menuPos = this._options["menu-at-left"]?
"menu" : "comboboxmenu";
menuRef = this;
}
this._docView.executeCommand(
EXECUTE_HELPER, "commandButtonMenu",
type + " " + JSON.stringify(menu))
.then((result) => {
this.showMenu(result, menuPos, menuRef);
});
}
} else {
if (cmdName !== null) {
this._docView.executeCommand(EXECUTE_NORMAL,
cmdName, cmdParam);
}
}
}
});
}
static isMenuSpec(menu) {
return (Array.isArray(menu) && (menu.length % 3) === 0);
}
showMenu(result, position, reference) {
if (!CommandResult.isDone(result)) {
return;
}
let expandedMenu = null;
try {
expandedMenu = JSON.parse(result.value);
if (!CommandButton.isMenuSpec(expandedMenu)) {
throw new Error();
}
} catch (error) {
console.error(`"${result.value}", invalid menu specification`);
return;
}
const menuItems = CommandButton.createMenuItems(expandedMenu);
if (menuItems.length === 0) {
return;
}
const menu = XUI.Menu.create(menuItems);
menu.addEventListener("menuitemselected", (event) => {
let [cmdName, cmdParam] =
Command.splitCmdString(event.detail.menuItem.name,
CommandButton.CMD_STRING_SEPAR);
if (cmdName !== null) {
this._docView.executeCommand(EXECUTE_NORMAL, cmdName, cmdParam);
}
});
menu.open(position, reference);
}
static createMenuItems(triplets) {
let menuItems = [];
let addSepar = false;
const count = triplets.length;
for (let i = 0; i < count; i += 3) {
let label = triplets[i];
const name = triplets[i+1];
const param = triplets[i+2];
if (label === null) {
addSepar = true;
} else {
let menuItem = {};
if (label.startsWith("+")) {
label = label.substring(1);
} else if (label.startsWith("-")) {
label = label.substring(1);
menuItem.enabled = false;
}
menuItem.text = label;
if (name === null) {
if (!CommandButton.isMenuSpec(param)) {
continue;
}
let submenuItems = CommandButton.createMenuItems(param);
if (submenuItems.length === 0) {
continue;
}
menuItem.type = "submenu";
menuItem.items = submenuItems;
} else {
menuItem.type = "button";
menuItem.name =
Command.joinCmdString(name, !param? null : param,
CommandButton.CMD_STRING_SEPAR);
if (menuItem.name === null) {
continue;
}
}
if (addSepar) {
addSepar = false;
menuItem.separator = true;
}
menuItems.push(menuItem);
}
}
return menuItems;
}
connectedCallback() {
this._docView = DOMUtil.lookupAncestorByTag(this, "xxe-document-view");
if (this._docView === null) {
return;
}
let opts = {};
let optsVal = this.getAttribute("options");
if (optsVal !== null) {
try {
opts = JSON.parse(optsVal);
} catch (error) {
console.error(`"${optsVal}", invalid "options" attribute`);
}
}
this._options = Object.assign(this._optionDefaults, opts);
if (this._options["tool-tip"] !== null) {
this.setAttribute("title", this._options["tool-tip"]);
}
let buttonIcon = this.firstElementChild;
if (buttonIcon !== null) {
buttonIcon.classList.add("xxe-cmdb-icon");
}
if (this._options["text"] !== null) {
let buttonText = document.createElement("span");
buttonText.setAttribute("class", "xxe-cmdb-text");
let label = XUI.Util.escapeHTML(this._options["text"]);
if (label.indexOf('\n')) {
label = label.replaceAll('\n', "<br>");
}
buttonText.innerHTML = label;
if (buttonIcon !== null) {
let direction = null;
const iconPos = this._options["icon-position"];
switch (iconPos) {
case "bottom":
direction = "column";
//FALLTHROUGH
case "right":
this.insertBefore(buttonText, buttonIcon);
break;
case "top":
direction = "column";
//FALLTHROUGH
default:
this.appendChild(buttonText);
break;
}
if (direction !== null) {
this.setAttribute("style", `flex-direction: ${direction};`);
}
buttonText.setAttribute(
"style",
`margin-${iconPos}: ${this._options["icon-gap"]}px;`);
} else {
this.appendChild(buttonText);
}
}
if (!this._docView.canEdit()) {
this.removeEventListener("click", this._onButtonClicked);
this.removeEventListener("contextmenu", this._onButtonClicked);
this.classList.add("xui-control-disabled");
}
}
}
CommandButton.CMD_STRING_SEPAR = '\uF876';
window.customElements.define("xxe-command-button", CommandButton);
class RemarkIndicator extends HTMLElement {
constructor() {
super();
let shadow = this.attachShadow({mode: "open"});
shadow.appendChild(RemarkIndicator.TEMPLATE.content.cloneNode(true));
this._pole = shadow.lastElementChild;
this._flag = this._pole.previousElementSibling;
this._editRemark = this.editRemark.bind(this);
this.addEventListener("dblclick", this._editRemark);
this._docView = null;
this._info = null;
}
get contextualMenuItems() {
if (this._docView && this._docView.canEdit()) {
return [ { label: "Edit Remark...", cmdName: "_editRemark()",
enabled: true } ];
} else {
return null;
}
}
editRemark(event) {
event.preventDefault();
event.stopPropagation();
let view = NodeView.lookupView(this);
if (view === null) {
return;
}
this._docView.selectNode(view)
.then((selected) => {
if (selected) {
this._docView.executeCommand(EXECUTE_NORMAL,
"remark", null);
}
});
}
connectedCallback() {
this._docView = DOMUtil.lookupAncestorByTag(this, "xxe-document-view");
if (this._docView === null) {
return;
}
this._info = null;
let infoValue = this.getAttribute("info");
if (infoValue) {
try {
this._info = JSON.parse(infoValue);
} catch (error) {
console.error(`"${infoValue}", invalid "info" attribute`);
}
}
if (this._info === null) {
this._info = { "editor": "??", "bg": "#D3D3D3", "fg": "#808080",
"date": Date.now(), "text": "" };
}
let tooltip = null;
if (this._info.text) {
tooltip = RemarkIndicator.remarkHeader(this._info.editor,
this._info.date);
let text = this._info.text;
if (text.length > 200) {
text = text.substring(0, 199) + "\u2026";
}
tooltip += "\n" + text;
if (Array.isArray(this._info.replies)) {
const replyCount = this._info.replies.length;
if (replyCount > 0) {
tooltip += "\n\n+ " +
((replyCount === 1)?
`${replyCount} reply` : `${replyCount} replies`);
}
}
}
if (tooltip === null) {
tooltip = "";
} else {
tooltip += "\n\n\u2022 ";
}
tooltip += "Double-click to edit remark.";
this.setAttribute("title", tooltip);
this._flag.textContent = RemarkIndicator.getInitials(this._info.editor);
this._flag.setAttribute("style",
`border-color: ${this._info.fg}; background-color: ${this._info.bg};`);
this._pole.setAttribute("style", `background-color: ${this._info.fg};`);
}
static getInitials(editor) {
if (!editor) {
return "??";
} else {
return editor.substring(0, Math.min(2, editor.length));
}
}
static remarkHeader(editor, date) {
let ago = null;
const delta = Date.now() - date;
if (delta >= 0) {
const s = Math.floor(delta / 1000);
const m = Math.floor(s / 60);
const h = Math.floor(m / 60);
const d = Math.floor(h / 24);
if (d >= 1) {
if (d < 15) {
ago = `${editor}, ${d}d ago:`;
}
} else {
if (h >= 1) {
ago = `${editor}, ${h}h ago:`;
} else {
if (m >= 1) {
ago = `${editor}, ${m}m ago:`;
} else {
if (s >= 1) {
ago = `${editor}, ${s}s ago:`;
} else {
ago = `${editor}, now:`;
}
}
}
}
}
if (ago !== null) {
return ago;
} else {
return `On ${(new Date(date)).toLocaleString()}, ${editor} wrote:`;
}
}
}
RemarkIndicator.TEMPLATE = document.createElement("template");
RemarkIndicator.TEMPLATE.innerHTML = `
<style>
:host {
    display: inline-flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    /* Note that inheritable styles (e.g. color, font-style)
       are also inherited by the Shadow DOM. */
    font-family: var(--xui-font-family, sans-serif);
    font-variant: normal;
    font-size: smaller;
    font-weight: bold;
    font-style: normal;
    text-decoration: none;
    color: var(--xui-fg, black);
    vertical-align: calc(0.2em + 2px + 0.8em + 0.25em); /*Extra 0.25em*/
}
.flag {
    flex: none;
    padding: 0.2em 0.4em;
    border: 2px solid black;
    border-radius: 0.4em;
}
.pole {
    flex: none;
    width: 2px;
    height: 0.8em;
    background-color: black;
}
</style><div class="flag"></div><div class="pole"></div>`;
window.customElements.define("xxe-remark-indicator", RemarkIndicator);
class ValueEditorControl {
constructor(valueEditor, options) {
this._valueEditor = valueEditor;
let needsBg = false;
switch (options["controlType"]) {
case "text-area":
case "text-field":
case "date-field":
case "file-name-field":
case "password-field":
case "number-field":
needsBg = true;
break;
}
let opts = Object.assign({ "missing-color": "#008080",
"missing-background-color":
needsBg? "#F0FFFF" : null,
"error-color": "#C00000",
"error-background-color":
needsBg? "#FFF0F0" : null },
options);
this._missingForeground = opts["missing-color"];
this._missingBackground = opts["missing-background-color"];
this._errorForeground = opts["error-color"];
this._errorBackground = opts["error-background-color"];
this._element = null;
this._value = this._state = undefined;
}
static setControlStyle(elem, ...classes) {
elem.classList.add("xxe-valed-ctrl", ...classes);
}
init(elem, input, needsFocusHandler) {
this._element = elem;
if (input !== null && !input.disabled) {
if (needsFocusHandler) {
input.addEventListener("focus", this.onFocus.bind(this));
}
input.addEventListener("change", this.onChange.bind(this));
input.addEventListener("keydown", this.onTabOut.bind(this));
}
}
static labelsFromValues(labels, values) {
if (labels === null ||
!Array.isArray(labels) || labels.length !== values.length) {
labels = values;
}
if (labels.indexOf("") >= 0) {
labels = labels.map((l) => ((l.length === 0)? "(empty string)" : l));
}
return labels;
}
get valueEditor() {
return this._valueEditor;
}
get element() {
return this._element;
}
getValue() {
return this._value;
}
setValue(value) {
this._value = value;
}
getState() {
return this._state;
}
setState(state) {
if (state !== this._state) {
this._state = state;
switch (state) {
case ValueEditorControl.STATE_NORMAL:
if (this._missingForeground) {
this._element.style.removeProperty("color");
}
if (this._missingBackground) {
this._element.style.removeProperty("background-color");
}
break;
case ValueEditorControl.STATE_MISSING:
if (this._missingForeground) {
this._element.style.color = this._missingForeground;
}
if (this._missingBackground) {
this._element.style.backgroundColor =
this._missingBackground;
}
break;
case ValueEditorControl.STATE_ERROR:
if (this._errorForeground) {
this._element.style.color = this._errorForeground;
}
if (this._errorBackground) {
this._element.style.backgroundColor = this._errorBackground;
}
break;
}
}
}
onFocus(event) {
XUI.Util.consumeEvent(event);
this.valueEditor.selectEditedElement();
}
onChange(event) {
XUI.Util.consumeEvent(event);
let state;
if (this.checkValue()) {
state = ValueEditorControl.STATE_NORMAL;
} else {
state = ValueEditorControl.STATE_ERROR;
}
this.setState(state);
this.valueEditor.commitValue(this.getValue(), state);
}
checkValue() {
return true;
}
commitValueFailed() {
this.setState(ValueEditorControl.STATE_ERROR);
}
onTabOut(event) {
if (event.key === "Tab" && !event.ctrlKey && !event.metaKey) {
XUI.Util.consumeEvent(event);
this.valueEditor.selectEditedElement()
.then((selected) => {
if (selected) {
let param;
if (event.shiftKey) {
param =  `(preceding::text()|preceding::comment()|\
preceding::processing-instruction())[last()]`;
} else {
param = `(following::text()|following::comment()|\
following::processing-instruction())[1]`;
}
this.valueEditor.documentView.executeCommand(
EXECUTE_HELPER, "xpathSearch", param);
}
});
}
}
}
ValueEditorControl.STATE_NORMAL = 0;
ValueEditorControl.STATE_MISSING = 1;
ValueEditorControl.STATE_ERROR = 2;
class DummyVCE extends ValueEditorControl {
constructor(valueEditor, options) {
super(valueEditor, options);
const span = document.createElement("span");
ValueEditorControl.setControlStyle(span);
span.setAttribute("style", "padding: 0.125em 0.25em");
span.textContent = options["controlType"];
this.init(span, null, false);
}
}
class CheckBoxVCE extends ValueEditorControl {
constructor(valueEditor, options) {
super(valueEditor, options);
let opts = Object.assign({ "label": null,
"unchecked-value": null,
"checked-value": null,
"allow-empty-checked-value": false,
"remove-value": false }, options);
this._uncheckedValue = opts["unchecked-value"];
this._checkedValue = opts["checked-value"];
this._allowEmptyCheckedValue = opts["allow-empty-checked-value"];
this._removeValue = opts["remove-value"];
this._toggle = document.createElement("input");
this._toggle.setAttribute("type", "checkbox");
ValueEditorControl.setControlStyle(this._toggle);
let element = this._toggle;
if (opts["label"]) {
let label = document.createElement("label");
element = label;
ValueEditorControl.setControlStyle(label);
label.appendChild(this._toggle);
label.appendChild(document.createTextNode(opts["label"]));
}
this._disabled = true;
if (this._removeValue) {
if ((this._uncheckedValue === null &&
this._checkedValue !== null) ||
(this._checkedValue === null &&
this._uncheckedValue !== null)) {
this._disabled = false;
}
} else {
if (this._checkedValue !== null &&
this._uncheckedValue !== null) {
this._disabled = false;
}
}
if (!this._disabled &&
this._allowEmptyCheckedValue &&
this._checkedValue === null) {
this._disabled = true;
}
this._toggle.disabled = this._disabled;
this.init(element, this._toggle,  false);
}
getValue() {
if (this._disabled) {
return super.getValue();
}
return this._toggle.checked? this._checkedValue : this._uncheckedValue;
}
setValue(value) {
if (this._disabled) {
super.setValue(value);
return;
}
this._toggle.checked =
(value === this._checkedValue ||
(value !== null && value.length === 0 &&
this._checkedValue !== null && this._allowEmptyCheckedValue));
}
}
class ColorChooserVCE extends ValueEditorControl {
constructor(valueEditor, options) {
super(valueEditor, options);
const input = document.createElement("input");
input.setAttribute("type", "color");
ValueEditorControl.setControlStyle(input, "xxe-valed-ctrl2",
"xxe-valed-clrchsr");
let w = Number(options["swatch-width"]);
if (w >= 5 && w < 1000) {
input.style.width = `${w}px`;
}
let h = Number(options["swatch-height"]);
if (h >= 5 && h < 500) {
input.style.height = `${h}px`;
}
this.init(input, input, true);
}
getValue() {
return this._element.value;
}
setValue(value) {
this._element.value = (value === null)? "#000000" : value;
}
}
class DateTimePickerVCE extends ValueEditorControl {
constructor(valueEditor, options, type="datetime-local") {
super(valueEditor, options);
const input = document.createElement("input");
input.setAttribute("type", type);
if (type !== "date") {
input.setAttribute("step", "1");
}
ValueEditorControl.setControlStyle(input);
this.init(input, input, true);
}
getValue() {
return this._element.value;
}
setValue(value) {
this._element.value = (value === null)? "" : value;
}
}
class DatePickerVCE extends DateTimePickerVCE {
constructor(valueEditor, options) {
super(valueEditor, options, "date");
}
}
class TimePickerVCE extends DateTimePickerVCE {
constructor(valueEditor, options) {
super(valueEditor, options, "time");
}
}
class GaugeVCE extends ValueEditorControl {
constructor(valueEditor, options) {
super(valueEditor, options);
let opts = Object.assign({ "min": 0, "max": 1, "step": -1,
"thumb": false }, options);
let minValue = opts["min"];
let maxValue = opts["max"];
let stepValue = opts["step"];
this._disabled = false;
if (minValue >= maxValue) {
this._disabled = true;
} else {
if (stepValue > 0 &&
stepValue > (maxValue - minValue)) {
this._disabled = true;
}
}
if (this._disabled) {
minValue = 0;
maxValue = 1;
stepValue = -1;
}
const input = document.createElement("input");
input.setAttribute("type", "range");
this._minValue = String(minValue);
input.setAttribute("min", this._minValue);
input.setAttribute("max", String(maxValue));
input.setAttribute("step", (stepValue <= 0)? "any" : String(stepValue));
ValueEditorControl.setControlStyle(input, "xxe-valed-ctrl2",
opts["thumb"]? "xxe-valed-gauge" :
"xxe-valed-meter");
let w = Number(opts["width"]);
if (w >= 60 && w < 1000) {
input.style.width = `${w}px`;
}
let h = Number(opts["height"]);
if (h >= 10 && h < 500) {
input.style.height = `${h}px`;
}
this.init(input, input, true);
}
getValue() {
if (this._disabled) {
return super.getValue();
}
return this._element.value;
}
setValue(value) {
if (this._disabled) {
super.setValue(value);
return;
}
this._element.value = (value === null)? this._minValue : value;
}
}
class ComboBoxVCE extends ValueEditorControl {
constructor(valueEditor, options) {
super(valueEditor, options);
let opts = Object.assign({ "labels": null, "values": [] }, options);
let values = opts["values"];
if (!Array.isArray(values)) {
values = [];
}
const valueCount = values.length;
let labels = ValueEditorControl.labelsFromValues(opts["labels"],values);
let select = document.createElement("select");
ValueEditorControl.setControlStyle(select, "xxe-valed-ctrl2");
for (let i = 0; i < valueCount; ++i) {
const label = labels[i];
const value = values[i];
const option = document.createElement("option");
option.setAttribute("value", value);
option.appendChild(document.createTextNode(label));
select.appendChild(option);
}
this.init(select, select, true);
}
getValue() {
return this._element.value;
}
setValue(value) {
this._element.value = (value === null)? "" : value;
}
}
class ListVCE extends ComboBoxVCE {
constructor(valueEditor, options) {
super(valueEditor, options);
let opts = Object.assign({ "rows": 5,
"selection": "single",
"separator": " " }, options);
this._multiSelection = (opts["selection"] === "multiple");
let rows = opts["rows"];
if (typeof rows !== "number") {
rows = 5;
} else {
if (rows < 2) {
rows = 2;
} else if (rows > 10) {
rows = 10;
}
}
this._separator = opts["separator"];
if ((typeof this._separator !== "string") ||
this._separator.length !== 1) {
this._separator = " ";
}
this._element.classList.remove("xxe-valed-ctrl2");
if (this._multiSelection) {
this._element.setAttribute("multiple", "multiple");
}
this._element.setAttribute("size", String(rows));
}
getValue() {
if (!this._multiSelection) {
return super.getValue();
}
const options = this._element.selectedOptions;
const count = options.length;
let value = null;
if (count > 0) {
value = "";
for (let i = 0; i < count; ++i) {
if (i > 0) {
value += this._separator;
}
value += options[i].value;
}
}
return value;
}
setValue(value) {
if (!this._multiSelection) {
super.setValue(value);
return;
}
if (value === null) {
value = "";
}
if (this._separator === " ") {
value = value.trim();
}
let items;
if (value.length === 0) {
items = [];
} else {
if (this._separator === " ") {
items = value.split(/\s+/);
} else {
items = value.split(this._separator);
}
}
const options = this._element.options;
const count = options.length;
for (let i = 0; i < count; ++i) {
options[i].selected = (items.indexOf(options[i].value) >= 0);
}
}
}
class RadioButtonsVCE extends ValueEditorControl {
constructor(valueEditor, options) {
super(valueEditor, options);
let opts = Object.assign({ "labels": null, "values": [],
"selection": "single",
"separator": " " }, options);
this._multiSelection = (opts["selection"] === "multiple");
let values = opts["values"];
if (!Array.isArray(values)) {
values = [];
}
const valueCount = values.length;
let labels = ValueEditorControl.labelsFromValues(opts["labels"],values);
this._separator = opts["separator"];
if ((typeof this._separator !== "string") ||
this._separator.length !== 1) {
this._separator = " ";
}
let columns = Number(opts["columns"]);
let rows = Number(opts["rows"]);
let cols;
if (columns > 0) {
cols = columns;
rows = -1;
} else {
if (rows > 0) {
cols = -1;
} else {
cols = valueCount;
if (cols > 10) {
cols = 10;
}
rows = -1;
}
}
const div = document.createElement("div");
ValueEditorControl.setControlStyle(div, "xxe-valed-rbtns");
const changeHandler = this.onChange.bind(this);
const tabOutHandler = this.onTabOut.bind(this);
const radioButtonName = this.valueEditor.id + "-rbtn";
let x = 0;
let y = 0;
for (let i = 0; i < valueCount; ++i) {
const label = labels[i];
const value = values[i];
const toggle = document.createElement("input");
toggle.setAttribute("data-value", value);
if (this._multiSelection) {
toggle.setAttribute("type", "checkbox");
} else {
toggle.setAttribute("type", "radio");
toggle.setAttribute("name", radioButtonName);
}
ValueEditorControl.setControlStyle(toggle);
const toggleLabel = document.createElement("label");
ValueEditorControl.setControlStyle(toggleLabel);
toggleLabel.setAttribute(
"style",
`grid-column: ${1+x} / span 1; grid-row: ${1+y} / span 1;`);
toggleLabel.appendChild(toggle);
toggleLabel.appendChild(document.createTextNode(label));
div.appendChild(toggleLabel);
if (cols > 0) {
++x;
if (x === cols) {
++y;
x = 0;
}
} else {
++y;
if (y === rows) {
++x;
y = 0;
}
}
toggle.addEventListener("change", changeHandler);
toggle.addEventListener("keydown", tabOutHandler);
}
this.init(div, null, false);
}
getValue() {
let value = null;
let toggleLabel = this._element.firstElementChild;
while (toggleLabel !== null) {
const toggle = toggleLabel.firstElementChild;
const toggleValue = toggle.getAttribute("data-value");
if (this._multiSelection) {
if (toggle.checked) {
if (value === null) {
value = toggleValue;
} else {
value += this._separator + toggleValue;
}
}
} else {
if (toggle.checked) {
value = toggleValue;
break;
}
}
toggleLabel = toggleLabel.nextElementSibling;
}
return value;
}
setValue(value) {
let valueList;
if (this._multiSelection) {
if (value === null) {
value = "";
}
if (this._separator === " ") {
value = value.trim();
}
if (value.length === 0) {
valueList = [];
} else {
if (this._separator === " ") {
valueList = value.split(/\s+/);
} else {
valueList = value.split(this._separator);
}
}
} else {
if (value === null) {
valueList = [];
} else {
valueList = [ value ];
}
}
let toggleLabel = this._element.firstElementChild;
while (toggleLabel !== null) {
const toggle = toggleLabel.firstElementChild;
const toggleValue = toggle.getAttribute("data-value");
toggle.checked = (valueList.indexOf(toggleValue) >= 0);
toggleLabel = toggleLabel.nextElementSibling;
}
}
}
class SpinnerVCE extends ValueEditorControl {
constructor(valueEditor, options) {
super(valueEditor, options);
let opts = Object.assign({ "step": 1,
"data-type": "double" }, options);
let minValue = Number(opts["min"]);
let maxValue = Number(opts["max"]);
let isInteger = false;
switch (opts["data-type"]) {
case "byte":
isInteger = true;
if (Number.isNaN(maxValue) ||
maxValue < -128 || maxValue > 127) {
maxValue = 127;
}
if (Number.isNaN(minValue) ||
minValue < -128 || minValue > maxValue) {
minValue = -128;
}
break;
case "short":
isInteger = true;
if (Number.isNaN(maxValue) ||
maxValue < -32768 || maxValue > 32767) {
maxValue = 32767;
}
if (Number.isNaN(minValue) ||
minValue <  -32768 || minValue > maxValue) {
minValue = -32768;
}
break;
case "int":
isInteger = true;
if (Number.isNaN(maxValue) ||
maxValue < -2147483648 || maxValue > 2147483647) {
maxValue = 2147483647;
}
if (Number.isNaN(minValue) ||
minValue < -2147483648 || minValue > maxValue) {
minValue = -2147483648;
}
break;
case "long":
isInteger = true;
//FALLTHROUGH
default:
if (!Number.isNaN(minValue) && !Number.isNaN(maxValue) &&
minValue > maxValue) {
minValue = maxValue = Number.NaN;
}
break;
}
let fallbackValue;
if (Number.isNaN(minValue)) {
if (Number.isNaN(maxValue)) {
fallbackValue = 0;
} else {
if (maxValue >= 0) {
fallbackValue = 0;
} else {
fallbackValue = maxValue;
}
}
} else {
if (minValue <= 0) {
fallbackValue = 0;
} else {
fallbackValue = minValue;
}
}
this._fallbackValue = fallbackValue;
let stepValue = opts["step"];
if (stepValue <= 0) {
stepValue = 1;
}
if (isInteger && !Number.isInteger(stepValue)) {
stepValue = Math.ceil(stepValue);
}
const input = document.createElement("input");
input.setAttribute("type", "number");
if (!Number.isNaN(minValue)) {
input.setAttribute("min", String(minValue));
}
if (!Number.isNaN(maxValue)) {
input.setAttribute("max", String(maxValue));
}
input.setAttribute("step", String(stepValue));
ValueEditorControl.setControlStyle(input);
this.init(input, input, true);
}
getValue() {
return this._element.value;
}
setValue(value) {
this._element.value = (value === null)? this._fallbackValue : value;
}
}
class TextAreaVCE extends ValueEditorControl {
constructor(valueEditor, options) {
super(valueEditor, options);
let opts = Object.assign({ "columns": 40,
"rows": 3,
"wrap": "none" }, options);
const textarea = document.createElement("textarea");
textarea.setAttribute("cols", String(opts["columns"]));
textarea.setAttribute("rows", String(opts["rows"]));
textarea.setAttribute("wrap",
(opts["wrap"] === "line" ||
opts["wrap"] === "word")? "soft" : "off");
textarea.setAttribute("autocomplete", "off");
textarea.setAttribute("spellcheck", "false");
ValueEditorControl.setControlStyle(textarea);
this.init(textarea, textarea, true);
}
getValue() {
return this._element.value;
}
setValue(value) {
this._element.value = (value === null)? "" : value;
}
}
class TextFieldVCE extends ValueEditorControl {
constructor(valueEditor, options, type="text", cols=20) {
super(valueEditor, options);
let opts = Object.assign({ "columns": cols }, options);
const input = document.createElement("input");
input.setAttribute("type", type);
input.setAttribute("size", String(opts["columns"]));
input.setAttribute("autocomplete", "off");
input.setAttribute("spellcheck", "false");
ValueEditorControl.setControlStyle(input);
this.init(input, input, true);
}
getValue() {
return this._element.value;
}
setValue(value) {
this._element.value = (value === null)? "" : value;
}
}
class DateFieldVCE extends TextFieldVCE {
constructor(valueEditor, options) {
super(valueEditor, options, "text", 30);
}
}
class FileNameFieldVCE extends TextFieldVCE {
constructor(valueEditor, options) {
super(valueEditor, options, "text", 40);
}
}
class PasswordFieldVCE extends TextFieldVCE {
constructor(valueEditor, options) {
super(valueEditor, options, "password");
}
}
class NumberFieldVCE extends TextFieldVCE {
constructor(valueEditor, options) {
super(valueEditor, options, "text", 20);
}
}
class ValueEditor extends HTMLElement {
constructor() {
super();
this._docView = null;
this._attributeName = null;
this._control = null;
this._commitedValue = this._commitedState = undefined;
}
get documentView() {
return this._docView;
}
connectedCallback() {
this._docView = DOMUtil.lookupAncestorByTag(this, "xxe-document-view");
if (this._docView === null) {
return;
}
let opts = {};
let optsVal = this.getAttribute("options");
this.removeAttribute("options");
if (optsVal !== null) {
try {
opts = JSON.parse(optsVal);
} catch {}
}
let options =
Object.assign({ "controlType": null,
"attributeName": null,
"initialValue": null,
"initialState": ValueEditorControl.STATE_MISSING },
opts);
let controlType = options["controlType"];
this._attributeName = options["attributeName"];
if (!controlType || !this._attributeName) {
console.error(`"${optsVal}", invalid "options" attribute`);
return;
}
switch (controlType) {
case "check-box":
this._control = new CheckBoxVCE(this, options);
break;
case "color-chooser":
this._control = new ColorChooserVCE(this, options);
break;
case "combo-box":
this._control = new ComboBoxVCE(this, options);
break;
case "date-picker":
this._control = new DatePickerVCE(this, options);
break;
case "date-time-picker":
this._control = new DateTimePickerVCE(this, options);
break;
case "time-picker":
this._control = new TimePickerVCE(this, options);
break;
case "gauge":
this._control = new GaugeVCE(this, options);
break;
case "list":
this._control = new ListVCE(this, options);
break;
case "radio-buttons":
this._control = new RadioButtonsVCE(this, options);
break;
case "spinner":
this._control = new SpinnerVCE(this, options);
break;
case "text-area":
this._control = new TextAreaVCE(this, options);
break;
case "text-field":
this._control = new TextFieldVCE(this, options);
break;
case "date-field":
this._control = new DateFieldVCE(this, options);
break;
case "file-name-field":
this._control = new FileNameFieldVCE(this, options);
break;
case "password-field":
this._control = new PasswordFieldVCE(this, options);
break;
case "number-field":
this._control = new NumberFieldVCE(this, options);
break;
default:
this._control = new DummyVCE(this, options);
break;
}
this.appendChild(this._control.element);
if (!this._docView.canEdit()) {
ValueEditor.disableAll(this._control.element);
}
this.valueChanged(options["initialValue"], options["initialState"]);
}
static disableAll(tree) {
switch (tree.localName) {
case "input":
case "select":
case "textarea":
tree.disabled = true;
return;
}
let child = tree.firstElementChild;
while (child !== null) {
ValueEditor.disableAll(child);
child = child.nextElementSibling;
}
}
commitValue(value, state) {
const commitedValue = this._commitedValue;
const commitedState = this._commitedState;
if (value !== undefined &&
state !== ValueEditorControl.STATE_ERROR &&
(value !== commitedValue || state !== commitedState)) {
this.selectEditedElement()
.then((selected) => {
if (!selected) {
return Promise.resolve(CommandResult.FAILED);
} else {
const id = this.getAttribute("id");
let param = id? `[${id}]`: "";
if (this._attributeName !== "-") {
param += this._attributeName;
}
if (value !== null) {
param += "=" + value;
}
return this._docView.executeCommand(
EXECUTE_HELPER, "commitValue", param);
}
})
.then((result) => {
if (!CommandResult.isDone(result)) {
this._control.commitValueFailed();
} else {
if (result.value === "noop" &&
commitedValue !== undefined) {
this.valueChanged(commitedValue, commitedState);
}
}
});
}
}
selectEditedElement() {
if (this._docView === null) {
return Promise.resolve(false);
}
let view = NodeView.lookupView(this);
if (view === null || NodeView.uidIfElementView(view) === null) {
return Promise.resolve(false);
}
let selectElem;
if (!Object.is(this._docView.selected, view) ||
this._docView.selected2 !== null) {
selectElem = this._docView.selectNode(view,  false);
} else {
selectElem = Promise.resolve(true);
}
return selectElem;
}
valueChanged(value, state) {
this._commitedValue = value;
this._commitedState = state;
this._control.setValue(value);
this._control.setState(state);
}
}
window.customElements.define("xxe-value-editor", ValueEditor);
class GridCell {
constructor(td, rowPos, rowSpan, colPos, colSpan) {
this._tableCell = td;
this._rowPos = rowPos;
this._rowSpan = rowSpan;
this._colPos = colPos;
this._colSpan = colSpan;
}
get tableCell() {
return this._tableCell;
}
get rowPosition() {
return this._rowPos;
}
get rowSpan() {
return this._rowSpan;
}
get columnPosition() {
return this._colPos;
}
get columnSpan() {
return this._colSpan;
}
toString() {
return String(this._rowPos) + '/' + String(this._rowSpan) + ',' +
String(this._colPos) + '/' + String(this._colSpan);
}
}
class GridRow {
constructor() {
this._cells = [];
this._nextColumn = 0;
}
get nextColumn() {
const cellCount = this._cells.length;
let next = this._nextColumn;
while (next < cellCount && this._cells[next] !== null) {
++next;
}
return next;
}
set nextColumn(col) {
this._nextColumn = col;
}
checkCellCapacity(capacity) {
while (capacity > this._cells.length) {
this._cells.push(null);
}
}
get cells() {
return this._cells;
}
toString() {
let text = "";
for (let cell of this._cells) {
if (text.length > 0) {
text += '|';
}
let cellText;
if (cell === null) {
cellText = '-';
} else {
cellText = cell.toString();
}
text += cellText;
let pad = 11 - cellText.length;
while (pad > 0) {
text += ' ';
--pad;
}
}
return text;
}
}
class Grid {
constructor() {
this._rows = [];
this._nextRow = 0;
this._colCount = -1;
this._colWidths = null;
}
addRow() {
this.checkRowCapacity(this._nextRow+1);
++this._nextRow;
}
addCell(td, rowSpan, colSpan) {
const firstRow = this._nextRow-1;
const endRow = firstRow + rowSpan;
this.checkRowCapacity(endRow);
const row = this._rows[firstRow];
const firstCol = row.nextColumn;
const endCol = firstCol + colSpan;
for (let i = firstRow; i < endRow; ++i) {
const r = this._rows[i];
const rowPos = i - firstRow;
r.checkCellCapacity(endCol);
for (let j = firstCol; j < endCol; ++j) {
const colPos = j - firstCol;
r.cells[j] = new GridCell(td, rowPos, rowSpan, colPos, colSpan);
}
}
row.nextColumn = endCol;
}
checkRowCapacity(capacity) {
while (capacity > this._rows.length) {
this._rows.push(new GridRow());
}
}
finish(colSpacing) {
let colCount = 0;
for (let row of this._rows) {
const count = row.cells.length;
if (count > colCount) {
colCount = count;
}
}
for (let row of this._rows) {
const cells = row.cells;
let count = cells.length;
while (count < colCount) {
cells.push(null);
++count;
}
}
this._colCount = colCount;
this._colWidths = this.computeColumnWidths(colSpacing);
}
computeColumnWidths(colSpacing) {
const colCount = this._colCount;
let colLefts = new Array(colCount+1);
colLefts.fill(NaN);
const halfColSpacing = colSpacing / 2;
const rows = this._rows;
const rowCount = rows.length;
for (let i = 0; i < rowCount; ++i) {
const cells = rows[i].cells;
for (let j = 0; j < colCount; ++j) {
const cell = cells[j];
if (cell !== null && cell.columnPosition === 0) {
const k = j + cell.columnSpan;
const noColLeft = isNaN(colLefts[j]);
const noNextColLeft = isNaN(colLefts[k]);
if (noColLeft || noNextColLeft) {
const rect = cell.tableCell.getBoundingClientRect();
if (noColLeft) {
colLefts[j] = rect.left -
((j === 0)? colSpacing : halfColSpacing);
}
if (noNextColLeft) {
colLefts[k] = rect.right +
((k === colCount)? colSpacing : halfColSpacing);
}
}
}
}
}
let colWidths = new Array(colCount);
for (let j = 0; j < colCount; ++j) {
const colLeft = colLefts[j];
const nextColLeft = colLefts[j+1];
colWidths[j] = nextColLeft - colLeft;
if (isNaN(colWidths[j])) {
colWidths = null;
break;
}
if (colWidths[j] < 1) {
colWidths[j] = 1;
}
}
return colWidths;
}
get rows() {
return this._rows;
}
get rowCount() {
return this._rows.length;
}
get columnCount() {
return this._colCount;
}
get columnWidths() {
return this._colWidths;
}
findTableCell(td) {
const rows = this._rows;
const rowCount = rows.length;
const colCount = this._colCount;
for (let i = 0; i < rowCount; ++i) {
const cells = rows[i].cells;
for (let j = 0; j < colCount; ++j) {
const cell = cells[j];
if (cell !== null && cell.tableCell === td &&
cell.rowPosition === 0 && cell.columnPosition === 0) {
return [i, j, cell];
}
}
}
return [-1, -1, null];
}
toString() {
let text = "";
if (this._colWidths !== null) {
for (let colWidth of this._colWidths) {
if (text.length > 0) {
text += '|';
}
let colText = String(Math.round(colWidth)) + "px";
text += colText;
let pad = 11 - colText.length;
while (pad > 0) {
text += ' ';
--pad;
}
}
text += '\n';
let sep = text.length;
while (sep > 0) {
text += '-';
--sep;
}
}
for (let row of this._rows) {
if (text.length > 0) {
text += '\n';
}
text += row.toString();
}
return text;
}
}
class ColumnResizer extends HTMLElement {
constructor() {
super();
this._grid = null;
this._beginResize = this.beginResize.bind(this);
this._doResize = this.doResize.bind(this);
this._endResize = this.endResize.bind(this);
this.addEventListener("mousedown", this._beginResize);
this._resizedCol = this._right0 = this._minRight = this._maxRight =
this._dragX0 = -1;
this._colCursor = null;
}
set grid(grid) {
this._grid = grid;
}
beginResize(event) {
event.preventDefault();
event.stopImmediatePropagation();
this._right0 = XUI.Util.getPxProperty(this, "right");
if (isNaN(this._right0)) {
return;
}
const td = this.parentElement;
let [row, col, cell] = this._grid.findTableCell(td);
if (col < 0) {
return;
}
this._resizedCol = col + cell.columnSpan - 1;
const colWidths = this._grid.columnWidths;
const [pl, pr, fs] = XUI.Util.getPxProperties(td, "padding-left", 0,
"padding-right", 0,
"font-size", 0);
let tdMinW = pl + pr + fs;
tdMinW = Math.max(tdMinW, 2*Math.abs(this._right0));
this._maxRight = this._right0 + colWidths[this._resizedCol] - tdMinW;
this._minRight = this._right0 - colWidths[this._resizedCol+1] + tdMinW;
this._dragX0 = event.clientX;
if (this._colCursor === null) {
const table = DOMUtil.lookupAncestorByTag(td, "table");
if (table !== null) {
this._colCursor = document.createElement("div");
const tableBounds = table.getBoundingClientRect();
const bounds = this.getBoundingClientRect();
this._colCursor.style.setProperty(
"height", String(tableBounds.height) + "px");
this._colCursor.style.setProperty(
"top", String(tableBounds.top - bounds.top) + "px");
this.appendChild(this._colCursor);
}
}
document.addEventListener("mousemove", this._doResize);
document.addEventListener("mouseup", this._endResize);
document.addEventListener("mouseleave", this._endResize);
}
doResize(event) {
event.preventDefault();
event.stopImmediatePropagation();
let incrX = event.clientX - this._dragX0;
let right = this._right0 - incrX;
if (right < this._minRight) {
right = this._minRight;
} else if (right > this._maxRight) {
right = this._maxRight;
}
this.style.setProperty("right", String(right) + "px");
}
endResize(event) {
this.doResize(event);
let incrX = event.clientX - this._dragX0;
let right = this._right0 - incrX;
if (right < this._minRight) {
incrX = this._right0 - this._minRight;
} else if (right > this._maxRight) {
incrX = this._right0 - this._maxRight;
}
const resizedCol = this._resizedCol;
const oldColWidths = this._grid.columnWidths;
let newColWidths = [...oldColWidths];
newColWidths[resizedCol] += incrX;
newColWidths[resizedCol+1] -= incrX;
this.style.removeProperty("right");
this._resizedCol = this._right0 = this._minRight = this._maxRight =
this._dragX0 = -1;
if (this._colCursor !== null) {
this.removeChild(this._colCursor);
this._colCursor = null;
}
document.removeEventListener("mousemove", this._doResize);
document.removeEventListener("mouseup", this._endResize);
document.removeEventListener("mouseleave", this._endResize);
let tableView = null;
const table = DOMUtil.lookupAncestorByTag(this, "table");
if (table === null || !table.hasAttribute("data-cg") ||
(tableView = NodeView.lookupView(table)) === null) {
return;
}
const docView = DOMUtil.lookupAncestorByTag(this, "xxe-document-view");
if (docView === null) {
return;
}
let binding = docView.getAppEventBinding("resize-table-column");
if (binding === null) {
return;
}
let cmdName = binding.commandName;
let cmdParams = binding.commandParams;
if (cmdParams !== null) {
if (cmdParams.indexOf("%{resizedColumn}") >= 0) {
cmdParams = cmdParams.replaceAll("%{resizedColumn}",
String(resizedCol));
}
if (cmdParams.indexOf("%{columnCount}") >= 0) {
cmdParams = cmdParams.replaceAll("%{columnCount}",
String(oldColWidths.length));
}
const colWidthParam = w => String(Math.round(w));
if (cmdParams.indexOf("%{oldColumnWidths}") >= 0) {
cmdParams = cmdParams.replaceAll(
"%{oldColumnWidths}",
oldColWidths.map(colWidthParam).join(' '));
}
if (cmdParams.indexOf("%{newColumnWidths}") >= 0) {
cmdParams = cmdParams.replaceAll(
"%{newColumnWidths}",
newColWidths.map(colWidthParam).join(' '));
}
}
docView.selectNode(tableView)
.then((selected) => {
if (selected) {
docView.executeCommand(EXECUTE_NORMAL, cmdName, cmdParams);
}
})
}
static addResizers(tree) {
if (tree.localName === "table" && tree.hasAttribute("data-cg") &&
tree.getAttribute("data-resizable-columns") !== "false") {
ColumnResizer.processTable(tree);
}
let child = tree.firstElementChild;
while (child !== null) {
if (child.localName !== "span") {
ColumnResizer.addResizers(child);
}
child = child.nextElementSibling;
}
}
static processTable(table) {
let grid = new Grid();
let allCells = [];
const rowGroups = ColumnResizer.getChildrenByName(table, "tbody");
for (const rowGroup of rowGroups) {
const rows = ColumnResizer.getChildrenByName(rowGroup, "tr");
let rowIndex = 0;
for (const row of rows) {
grid.addRow();
const cells = ColumnResizer.getChildrenByName(row, "td");
allCells.push(...cells);
for (const cell of cells) {
let rowSpan =
ColumnResizer.getSpanAttribute(cell, "rowspan");
if (rowIndex + rowSpan > rows.length) {
rowSpan = rows.length - rowIndex;
}
let colSpan =
ColumnResizer.getSpanAttribute(cell, "colspan");
grid.addCell(cell, rowSpan, colSpan);
}
++rowIndex;
}
}
const colSpacing = ColumnResizer.getColumnSpacing(table);
grid.finish(colSpacing);
if (grid.columnWidths === null) {
return;
}
let colResizerRight = undefined;
let minCellPadding = undefined;
let minCellPaddingPx = undefined;
let colResizerRightPx = undefined;
let colResizerWidthPx = undefined;
const rowCount = grid.rowCount;
const colCount = grid.columnCount;
const rows = grid.rows;
for (let i = 0; i < rowCount; ++i) {
const cells = rows[i].cells;
for (let j = 0; j < colCount; ++j) {
const cell = cells[j];
if (cell === null) {
continue;
}
if (cell.rowPosition === 0 &&
cell.columnPosition === 0 &&
j + cell.columnSpan < colCount) {
const td = cell.tableCell;
td.style.setProperty("position", "relative");
let colResizer =
document.createElement("xxe-column-resizer");
colResizer.grid = grid;
td.appendChild(colResizer);
if (colResizerRight === undefined) {
colResizerRight =
XUI.Util.getPxProperty(colResizer, "right");
if (isNaN(colResizerRight)) {
colResizerRight = -2;
}
minCellPadding = Math.abs(colResizerRight) + 1;
minCellPaddingPx = String(minCellPadding) + "px";
const colResizerPad = Math.abs(colResizerRight) - 1;
const colResizerThickness = Math.max(1, colSpacing);
colResizerRightPx =
String(-colResizerThickness - colResizerPad) + "px";
colResizerWidthPx =
String(2*colResizerPad + colResizerThickness) + "px";
}
if (colSpacing > 0) {
const colResizerStyle = colResizer.style;
colResizerStyle.setProperty("right", colResizerRightPx);
colResizerStyle.setProperty("width", colResizerWidthPx);
}
}
}
}
if (minCellPadding !== undefined) {
for (const td of allCells) {
let padding = XUI.Util.getPxProperty(td, "padding-left");
if (isNaN(padding) || padding < minCellPadding) {
td.style.setProperty("padding-left", minCellPaddingPx);
}
padding = XUI.Util.getPxProperty(td, "padding-right");
if (isNaN(padding) || padding < minCellPadding) {
td.style.setProperty("padding-right", minCellPaddingPx);
}
}
}
}
static getChildrenByName(element, childName) {
let children = [];
let child = element.firstElementChild;
while (child !== null) {
if (child.localName === childName) {
children.push(child);
}
child = child.nextElementSibling;
}
return children;
}
static getSpanAttribute(cell, attrName) {
let value = cell.getAttribute(attrName);
if (value) {
let span = parseInt(value.trim());
if (!isNaN(span) && span > 0) {
return span;
}
}
return 1;
}
static getColumnSpacing(table) {
let colSpacing = 0;
const styles = window.getComputedStyle(table);
if (styles.getPropertyValue("border-collapse") === "separate") {
let propValue = styles.getPropertyValue("border-spacing");
let pos = propValue.indexOf("px");
if (pos > 0) {
colSpacing = parseFloat(propValue.substring(0, pos));
if (isNaN(colSpacing) || colSpacing < 0) {
colSpacing = 0;
}
}
}
return colSpacing;
}
}
window.customElements.define("xxe-column-resizer", ColumnResizer);
class NodeHighlight {
constructor(docView) {
this._nodeSelection =
docView.viewContainer.getElementsByClassName("xxe-node-sel");
}
contains(clientX, clientY) {
const highlights = this._nodeSelection;
const count = highlights.length;
for (let i = 0; i < count; ++i) {
let highlight = highlights.item(i);
const rects = highlight.getClientRects();
const rectCount = rects.length;
for (let i = 0; i < rectCount; ++i) {
let rect = rects[i];
if (rect !== null && rect.width > 0 && rect.height > 0 &&
clientX >= rect.left && clientX <= rect.right &&
clientY >= rect.top && clientY <= rect.bottom) {
return true;
}
}
}
return false;
}
erase() {
let nodeSel = this._nodeSelection;
while (nodeSel.length > 0) {
nodeSel.item(0).classList.remove("xxe-node-sel");
}
}
draw(sel, sel2) {
this.erase();
if (sel !== null) {
if (sel2 !== null && sel2 !== sel) {
if (NodeHighlight.isFollowingSibling(sel, sel2)) {
NodeHighlight.addClassToSiblings(sel, sel2, "xxe-node-sel");
} else if (NodeHighlight.isFollowingSibling(sel2, sel)) {
NodeHighlight.addClassToSiblings(sel2, sel, "xxe-node-sel");
}
} else {
sel.classList.add("xxe-node-sel");
}
}
}
static isFollowingSibling(anchor, node) {
let sibling = anchor.nextSibling;
while (sibling !== null) {
if (sibling === node) {
return true;
}
sibling = sibling.nextSibling;
}
return false;
}
static addClassToSiblings(first, last, className) {
let sibling = first;
while (sibling !== null) {
if (sibling.nodeType === Node.ELEMENT_NODE) {
sibling.classList.add(className);
}
if (sibling === last) {
break;
}
sibling = sibling.nextSibling;
}
}
}
class TextHighlight {
constructor(docView) {
this._textSelection =
docView.viewContainer.getElementsByClassName("xxe-text-sel");
this._start = null;
this._startOffset = -1;
this._end = null;
this._endOffset = -1;
this._reversed = false;
}
contains(clientX, clientY) {
const highlights = this._textSelection;
const count = highlights.length;
for (let i = 0; i < count; ++i) {
let highlight = highlights.item(i);
const rects = highlight.getClientRects();
const rectCount = rects.length;
for (let i = 0; i < rectCount; ++i) {
let rect = rects[i];
if (rect !== null && rect.width > 0 && rect.height > 0 &&
clientX >= rect.left && clientX <= rect.right &&
clientY >= rect.top && clientY <= rect.bottom) {
return true;
}
}
}
return false;
}
erase() {
let textSel = this._textSelection;
while (textSel.length > 0) {
let highlight = textSel.item(0);
highlight.removeAttribute("class");
let container = highlight.parentNode;
if (container !== null && container.isConnected) {
let node = highlight.firstChild;
while (node !== null) {
let next = node.nextSibling;
highlight.removeChild(node);
container.insertBefore(node,  highlight);
node = next;
}
container.removeChild(highlight);
container.normalize();
}
}
this._start = null;
this._startOffset = -1;
this._end = null;
this._endOffset = -1;
this._reversed = false;
}
draw(dot, dotOffset, mark, markOffset) {
if (dot !== null) {
if (mark !== null && (mark !== dot || markOffset !== dotOffset)) {
if (!this.extendHighlight(dot, dotOffset, mark, markOffset)) {
this.erase();
this.drawHighlight(dot, dotOffset, mark, markOffset);
}
} else {
this.erase();
}
TextHighlight.drawDot(dot, dotOffset);
} else {
this.erase();
}
}
extendHighlight(dot, dotOffset, mark, markOffset) {
let dotContent = NodeView.getTextualContent(dot);
if (dotContent === null) {
return false;
}
let markContent = NodeView.getTextualContent(mark);
if (markContent === null) {
return false;
}
if (!this._reversed &&
markContent === this._start && dotContent === this._end) {
let delta = dotOffset - this._endOffset;
if (markOffset === this._startOffset && Math.abs(delta) <= 1 &&
this.adjustTail(this._end, delta)) {
this._endOffset = dotOffset;
return true;
}
} else if (this._reversed &&
dotContent === this._start && markContent === this._end) {
let delta = dotOffset - this._startOffset;
if (Math.abs(delta) <= 1 && markOffset === this._endOffset &&
this.adjustHead(this._start, delta)) {
this._startOffset = dotOffset;
return true;
}
}
return false;
}
adjustHead(textContent, delta) {
let highlight = TextHighlight.getHighlight(textContent);
if (highlight !== null) {
if (delta === 1) {
let c = TextHighlight.removeFirstChar(highlight);
let head = highlight.previousSibling;
if (head === null) {
highlight.parentNode.insertBefore(document.createTextNode(c),
highlight);
} else {
assertOrError(head.nodeType === Node.TEXT_NODE);
TextHighlight.appendChar(head, c);
}
TextHighlight.markIfEmpty(textContent, highlight);
return true;
} else if (delta === -1) {
let head = highlight.previousSibling;
assertOrError(head !== null &&
head.nodeType === Node.TEXT_NODE);
let c = TextHighlight.removeLastChar(head);
TextHighlight.prependChar(highlight, c);
TextHighlight.removeIfEmpty(head);
TextHighlight.unmarkNotEmpty(highlight);
return true;
}
}
return false;
}
adjustTail(textContent, delta) {
let highlight = TextHighlight.getHighlight(textContent);
if (highlight !== null) {
if (delta === 1) {
let tail = highlight.nextSibling;
assertOrError(tail !== null &&
tail.nodeType === Node.TEXT_NODE);
let c = TextHighlight.removeFirstChar(tail);
TextHighlight.appendChar(highlight, c);
TextHighlight.removeIfEmpty(tail);
TextHighlight.unmarkNotEmpty(highlight);
return true;
} else if (delta === -1) {
let c = TextHighlight.removeLastChar(highlight);
let tail = highlight.nextSibling;
if (tail === null) {
highlight.parentNode.appendChild(document.createTextNode(c));
} else {
assertOrError(tail.nodeType === Node.TEXT_NODE);
TextHighlight.prependChar(tail, c);
}
TextHighlight.markIfEmpty(textContent, highlight);
return true;
}
}
return false;
}
static getHighlight(textContent) {
let child = textContent.firstChild;
while (child !== null) {
if (child.nodeType === Node.ELEMENT_NODE &&
child.classList.contains("xxe-text-sel")) {
return child;
}
child = child.nextSibling;
}
return null;
}
static removeFirstChar(node) {
let textNode = TextHighlight.toTextNode(node);
let c = textNode.data.charAt(0);
textNode.deleteData(0, 1);
return c;
}
static removeLastChar(node) {
let textNode = TextHighlight.toTextNode(node);
const last = textNode.length-1;
let c = textNode.data.charAt(last);
textNode.deleteData(last, 1);
return c;
}
static prependChar(node, c) {
let textNode = TextHighlight.toTextNode(node);
textNode.insertData(0, c);
}
static appendChar(node, c) {
let textNode = TextHighlight.toTextNode(node);
textNode.appendData(c);
}
static removeIfEmpty(node) {
let textNode = TextHighlight.toTextNode(node);
if (textNode.length === 0) {
let container = node.parentNode;
container.removeChild(node);
container.normalize();
}
}
static toTextNode(node) {
if (node !== null && node.nodeType === Node.ELEMENT_NODE) {
let child = node.firstChild;
node = null;
while (child !== null) {
if (child.nodeType === Node.TEXT_NODE) {
node = child;
break;
}
child = child.nextSibling;
}
}
assertOrError(node !== null && node.nodeType === Node.TEXT_NODE);
return node;
}
static markIfEmpty(textContent, highlight) {
if (textContent.textContent.length === 0) {
highlight.classList.add("xxe-text-sel-empty");
}
}
static unmarkNotEmpty(highlight) {
highlight.classList.remove("xxe-text-sel-empty");
}
drawHighlight(dot, dotOffset, mark, markOffset) {
let allTextContent = [];
let reversed = NodeView.collectTextualContent(mark, markOffset,
dot, dotOffset,
allTextContent);
if (allTextContent.length === 0) {
return;
}
let start, startOffset, end, endOffset;
if (reversed) {
start = NodeView.getTextualContent(dot);
startOffset = dotOffset;
end = NodeView.getTextualContent(mark);
endOffset = markOffset;
} else {
start = NodeView.getTextualContent(mark);
startOffset = markOffset;
end = NodeView.getTextualContent(dot);
endOffset = dotOffset;
}
if (start === null) {
start = allTextContent[0];
startOffset = 0;
}
if (end === null) {
end = allTextContent[allTextContent.length-1];
endOffset = end.textContent.length;
}
TextHighlight.doDrawHighlight(allTextContent,
start, startOffset, end, endOffset);
this._start = start;
this._startOffset = startOffset;
this._end = end;
this._endOffset = endOffset;
this._reversed = reversed;
}
static doDrawHighlight(allTextContent, start, startOffset, end, endOffset) {
for (let textContent of allTextContent) {
NodeView.normalizeTextualContent(textContent);
const textNode = textContent.firstChild;
const textNodeLength = textNode.length;
let wrapped = null;
if (textContent === start) {
if (startOffset >= 0) {
wrapped = textNode.splitText(startOffset);
}
}
if (textContent === end) {
if (endOffset <= textNodeLength) {
if (wrapped === null) {
wrapped = textNode;
textNode.splitText(endOffset);
} else {
wrapped.splitText(wrapped.length -
(textNodeLength - endOffset));
}
}
}
if (wrapped === null) {
wrapped = textNode;
}
let wrapper = document.createElement("mark");
wrapper.className = "xxe-text-sel";
let beforeNode = wrapped.nextSibling;
textContent.removeChild(wrapped);
wrapper.appendChild(wrapped);
textContent.insertBefore(wrapper, beforeNode);
TextHighlight.markIfEmpty(textContent, wrapper);
}
}
static drawDot(dot, dotOffset) {
if (dot !== null) {
let dotContent = NodeView.getTextualContent(dot);
if (dotContent !== null) {
let [charsNode, charOffset] =
NodeView.textualContentToCharOffset(dotContent, dotOffset);
if (charsNode !== null) {
dotContent.focus({ preventScroll: true });
window.getSelection().collapse(charsNode, charOffset);
} else {
console.error(`TextHighlight.drawDot: INTERNAL ERROR: \
cannot convert dot offset ${dotOffset} to char offset in \
${DOMUtil.dumpNode(dotContent)}`);
}
}
}
}
}
class CaretPlaceholder {
constructor(docView) {
this._docView = docView;
this._disabled = false;
this._caret = null;
const activeElem = document.activeElement;
this._docViewHasFocus = (activeElem !== null &&
docView.contains(activeElem));
this._onFocusin = this.onFocusin.bind(this);
docView.addEventListener("focusin", this._onFocusin);
this._onFocusout = this.onFocusout.bind(this);
docView.addEventListener("focusout", this._onFocusout);
}
dispose() {
if (this._onFocusin) {
this._docView.removeEventListener("focusin", this._onFocusin);
this._onFocusin = null;
}
if (this._onFocusout) {
this._docView.removeEventListener("focusout", this._onFocusout);
this._onFocusout = null;
}
}
onFocusin(event) {
if (!this._docViewHasFocus) {
this._docViewHasFocus = true;
if (!this._disabled) {
this.hide();
}
}
}
onFocusout(event) {
if (this._docViewHasFocus) {
let receivingFocus = event.relatedTarget;
if (receivingFocus === null ||
!this._docView.contains(receivingFocus)) {
this._docViewHasFocus = false;
if (!this._disabled) {
this.show();
}
}
}
}
get disabled() {
return this._disabled;
}
set disabled(disable) {
this._disabled = disable;
}
hide() {
this.erase();
TextHighlight.drawDot(this._docView.dot, this._docView.dotOffset);
}
erase() {
let caret = this._caret;
if (caret !== null) {
this._caret = null;
let container = caret.parentNode;
if (container !== null && container.isConnected) {
container.removeChild(caret);
container = NodeView.getTextualContent(container);
if (container !== null) {
NodeView.normalizeTextualContent(container);
}
}
}
}
show() {
const dot = this._docView.dot;
if (dot !== null && !this._docView.hasTextSelection()) {
this.erase();
this.draw(dot, this._docView.dotOffset);
}
}
draw(dot, dotOffset) {
let dotContent = NodeView.getTextualContent(dot);
if (dotContent !== null) {
let [charsNode, charOffset] =
NodeView.textualContentToCharOffset(dotContent, dotOffset);
if (charsNode !== null) {
this._caret = document.createElement("a");
this._caret.classList.add("xxe-caret");
if (charOffset === 0) {
charsNode.parentNode.insertBefore(this._caret, charsNode);
} else if (charOffset === charsNode.length) {
charsNode.parentNode.insertBefore(this._caret,
charsNode.nextSibling);
} else {
let beforeNode = charsNode.splitText(charOffset);
charsNode.parentNode.insertBefore(this._caret, beforeNode);
}
if (dotContent.textContent.length === 0) {
this._caret.classList.add("xxe-caret-empty");
}
} else {
console.error(`CaretPlaceholder.draw: INTERNAL ERROR: \
cannot convert dot offset ${dotOffset} to char offset in \
${DOMUtil.dumpNode(dotContent)}`);
}
}
}
}
if ( !Element.prototype.scrollIntoViewIfNeeded ) {
Element.prototype.scrollIntoViewIfNeeded = function ( centerIfNeeded = true ) {
const el = this;
new IntersectionObserver( function( [entry] ) {
const ratio = entry.intersectionRatio;
if (ratio < 1) {
let place = ratio <= 0 && centerIfNeeded ? 'center' : 'nearest';
el.scrollIntoView( {
block: place,
inline: place,
} );
}
this.disconnect();
} ).observe(this);
};
}
class MarkManager {
constructor(docView, marks) {
this._docView = docView;
this._contenteditableState = docView.contenteditableState;
this._dotUID = null;
this._dot = null;
this._dotOffset = -1;
this._markUID = null;
this._mark = null;
this._markOffset = -1;
this._selectedUID = null;
this._selected = null;
this._selected2UID = null;
this._selected2 = null;
this._textHighlight = new TextHighlight(docView);
this._nodeHighlight = new NodeHighlight(docView);
this._caretPlaceholder = new CaretPlaceholder(docView);
this._caretPlaceholder.disabled = true;
if (marks !== null) {
for (let mark of marks) {
if ("offset" in mark) {
mark.changeType = CHANGE_TEXT_LOCATION_ADDED;
} else {
mark.changeType = CHANGE_NODE_MARK_ADDED;
}
}
this.applyMarkChanges(marks);
this.makeSelectionVisible( true);
this.ensureDocumentViewHasFocus();
}
this._caretPlaceholder.disabled = false;
}
dispose() {
if (this._caretPlaceholder) {
this._caretPlaceholder.dispose();
this._caretPlaceholder = null;
}
}
get dot() {
return this._dot;
}
get dotOffset() {
return this._dotOffset;
}
set dotOffset(offset) {
this._dotOffset = offset;
}
get mark() {
return this._mark;
}
get markOffset() {
return this._markOffset;
}
get selected() {
return this._selected;
}
get selected2() {
return this._selected2;
}
selectionContains(clientX, clientY) {
if (this.hasTextSelection() &&
this._textHighlight.contains(clientX, clientY)) {
return true;
}
if (this._selected !== null &&
this._nodeHighlight.contains(clientX, clientY)) {
return true;
}
return false;
}
hasTextSelection() {
return (this._dot !== null &&
this._mark !== null &&
(this._mark !== this._dot ||
this._markOffset !== this._dotOffset));
}
documentMarksChanged(event) {
this._caretPlaceholder.disabled = true;
this._caretPlaceholder.erase();
this.applyMarkChanges(event.changes);
if (!event.dragging) {
this.makeSelectionVisible( event.showing);
}
this.ensureDocumentViewHasFocus();
this._caretPlaceholder.disabled = false;
}
applyMarkChanges(changes) {
let drawNodeSelection = false;
let drawTextSelection = false;
let dotChanged = false;
let node = null;
for (let change of changes) {
switch (change.changeType) {
case CHANGE_NODE_MARK_ADDED:
case CHANGE_NODE_MARK_MOVED:
if (change.mark === "SELECTED") {
node = this._docView.getNodeView(change.uid,
false);
if (node === null) {
this._selectedUID = null;
this._selected = null;
} else {
this._selectedUID = change.uid;
this._selected = node;
}
drawNodeSelection = true;
} else if (change.mark === "SELECTED2") {
node = this._docView.getNodeView(change.uid,
false);
if (node === null) {
this._selected2UID = null;
this._selected2 = null;
} else {
this._selected2UID = change.uid;
this._selected2 = node;
}
drawNodeSelection = true;
}
break;
case CHANGE_NODE_MARK_REMOVED:
if (change.mark === "SELECTED") {
this._selectedUID = null;
this._selected = null;
drawNodeSelection = true;
} else if (change.mark === "SELECTED2") {
this._selected2UID = null;
this._selected2 = null;
drawNodeSelection = true;
}
break;
case CHANGE_TEXT_LOCATION_ADDED:
case CHANGE_TEXT_LOCATION_MOVED:
if (change.mark === "DOT") {
node = this._docView.getNodeView(change.uid,
false);
if (node === null) {
this._dotUID = null;
this._dot = null;
this._dotOffset = -1;
} else {
this._dotUID = change.uid;
this._dot = node;
this._dotOffset = change.offset;
}
drawTextSelection = dotChanged = true;
} else if (change.mark === "MARK") {
node = this._docView.getNodeView(change.uid,
false);
if (node === null) {
this._markUID = null;
this._mark = null;
this._markOffset = -1;
} else {
this._markUID = change.uid;
this._mark = node;
this._markOffset = change.offset;
}
drawTextSelection = true;
}
break;
case CHANGE_TEXT_LOCATION_REMOVED:
if (change.mark === "DOT") {
this._dotUID = null;
this._dot = null;
this._dotOffset = -1;
drawTextSelection = dotChanged = true;
} else if (change.mark === "MARK") {
this._markUID = null;
this._mark = null;
this._markOffset = -1;
drawTextSelection = true;
}
break;
}
}
if (dotChanged) {
this._docView.magicX = -1;
}
if (drawNodeSelection || drawTextSelection) {
this._contenteditableState.reset(this._dot, this._dotOffset);
}
this.drawHighlights(drawNodeSelection, drawTextSelection);
}
drawHighlights(drawNodeSelection, drawTextSelection) {
if (this._selected2 !== null && this._selected === null) {
this._selected2UID = null;
this._selected2 = null;
drawNodeSelection = true;
}
if (this._mark !== null && this._dot === null) {
this._markUID = null;
this._mark = null;
this._markOffset = -1;
drawTextSelection = true;
}
if (drawNodeSelection) {
this._nodeHighlight.draw(this._selected, this._selected2);
}
if (drawTextSelection) {
this._textHighlight.draw(this._dot, this._dotOffset,
this._mark, this._markOffset);
}
}
makeSelectionVisible(expand) {
let selection = null;
let sel = this._selected;
let dot = this._dot;
if (sel !== null) {
selection = sel;
if (dot !== null && selection.contains(dot)) {
selection = dot;
}
} else {
if (dot !== null) {
selection = dot;
}
}
if (selection !== null) {
if (expand) {
NodeView.expandViewBranch(selection,
this._docView.viewContainer);
} else {
selection =
NodeView.lookupVisibleView(selection,
this._docView.viewContainer);
}
if (selection !== null) {
selection.scrollIntoViewIfNeeded( true);
}
}
}
ensureDocumentViewHasFocus() {
let focused = null;
const activeElem = document.activeElement;
const docViewContainer = this._docView.viewContainer;
if (activeElem === null ||
!this._docView.viewContainer.contains(activeElem) ||
!DOMUtil.isDisplayedNode(activeElem, docViewContainer)) {
let focused = null;
if (this._dot !== null &&
DOMUtil.isDisplayedNode(this._dot, docViewContainer)) {
focused = NodeView.getTextualContent(this._dot);
}
if (focused === null) {
focused = docViewContainer;
}
focused.focus({ preventScroll: true });
}
return focused;
}
changingDocumentView() {
this._caretPlaceholder.disabled = true;
this._caretPlaceholder.erase();
}
documentViewChanged(redrawMarks) {
if (redrawMarks) {
this.redrawMarks();
}
this._caretPlaceholder.disabled = false;
}
redrawMarks() {
let drawNodeSelection = false;
let node = null;
if (this._selectedUID !== null) {
node = this._docView.getNodeView(this._selectedUID,  false);
if (node === null) {
this._selectedUID = null;
this._selected = null;
drawNodeSelection = true;
} else {
if (node !== this._selected) {
this._selected = node;
drawNodeSelection = true;
}
}
}
if (this._selected2UID !== null) {
node = this._docView.getNodeView(this._selected2UID,  false);
if (node === null) {
this._selected2UID = null;
this._selected2 = null;
drawNodeSelection = true;
} else {
if (node !== this._selected2) {
this._selected2 = node;
drawNodeSelection = true;
}
}
}
let drawTextSelection = false;
let dotChanged = false;
if (this._dotUID !== null) {
node = this._docView.getNodeView(this._dotUID,  false);
if (node === null) {
this._dotUID = null;
this._dot = null;
this._dotOffset = -1;
drawTextSelection = dotChanged = true;
} else {
if (node !== this._dot) {
this._dot = node;
this._dotOffset =
MarkManager.checkTextualContentOffset(node,
this._dotOffset);
drawTextSelection = dotChanged = true;
}
}
if (drawTextSelection) {
this._contenteditableState.reset(this._dot, this._dotOffset);
}
}
if (this._markUID !== null) {
node = this._docView.getNodeView(this._markUID,  false);
if (node === null) {
this._markUID = null;
this._mark = null;
this._markOffset = -1;
drawTextSelection = true;
} else {
if (node !== this._mark) {
this._mark = node;
this._markOffset =
MarkManager.checkTextualContentOffset(node,
this._markOffset);
drawTextSelection = true;
}
}
}
if (dotChanged) {
this._docView.magicX = -1;
}
this.drawHighlights(drawNodeSelection, drawTextSelection);
if (!drawTextSelection && this._dot !== null) {
TextHighlight.drawDot(this._dot, this._dotOffset);
}
this.ensureDocumentViewHasFocus();
}
static checkTextualContentOffset(node, offset) {
let content = NodeView.getTextualContent(node);
if (content !== null) {
const textLength = content.textContent.length;
if (offset < 0) {
offset = 0;
} else if (offset > textLength) {
offset = textLength;
}
}
return offset;
}
redrawDot() {
this._caretPlaceholder.erase();
if (this._docView.dot !== null) {
TextHighlight.drawDot(this._docView.dot, this._docView.dotOffset);
}
this.ensureDocumentViewHasFocus();
}
}
class Controller {
constructor(docView, bindings) {
this._disabled = false;
this._docView = docView;
this._viewContainer = docView.viewContainer;
this._contenteditableState = docView.contenteditableState;
let mouseBindings = {};
let keyboardBindings = {};
for (let binding of bindings) {
let userInput = binding.userInput;
if (userInput instanceof AppEvent) {
continue;
}
if (userInput instanceof MouseInput) {
mouseBindings[userInput.id] = binding;
} else if ((userInput instanceof KeyDownInput) ||
(userInput instanceof KeyPressInput)) {
keyboardBindings[userInput.id] = binding;
} else if (Array.isArray(userInput)) {
let keyInputSequence = null;
let last = userInput.length-1;
for (let i = 0; i <= last; ++i) {
let keyInput = userInput[i];
if ((keyInput instanceof KeyDownInput) ||
(keyInput instanceof KeyPressInput)) {
if (keyInputSequence === null) {
keyInputSequence = keyInput.id;
} else {
keyInputSequence =
keyInputSequence + '\n' + keyInput.id;
}
keyboardBindings[keyInputSequence] =
(i === last)? binding : keyInputSequence;
} else {
console.error(`Controller.constructor: INTERNAL ERROR: \
invalid UserInput for command "${binding.commandString}"`);
break;
}
}
} else {
console.error(`Controller.constructor: INTERNAL ERROR: \
invalid UserInput for command "${binding.commandString}"`);
}
}
this._mouseBindings = mouseBindings;
this._keyboardBindings = keyboardBindings;
this._leftKeyDownId = KeyDownInput.computeId("ArrowLeft",
0x0);
this._rightKeyDownId = KeyDownInput.computeId("ArrowRight",
0x0);
this._deleteKeyDownId = KeyDownInput.computeId("Delete",
0x0);
this._backSpaceKeyDownId = KeyDownInput.computeId("Backspace",
0x0);
this._insertStringCmd = new RemoteCommand("insertString");
this._typedChars = null;
this._charsDeleted = false;
this._cancelSelectionCmd = new RemoteCommand("cancelSelection");
this._enableContenteditableState =
this.enableContenteditableState.bind(this);
this._handleEvent = this.handleEvent.bind(this);
for (const eventType of Controller.HANDLED_EVENT_TYPES) {
this._viewContainer.addEventListener(eventType, this._handleEvent);
}
this._buttonsPressed = 0x0;
this._buttonsClicked = 0x0;
this._keyInputSequence = null;
}
dispose() {
if (this._handleEvent) {
for (const eventType of Controller.HANDLED_EVENT_TYPES) {
this._viewContainer.removeEventListener(eventType,
this._handleEvent);
}
this._handleEvent = null;
}
}
get disabled() {
return this._disabled;
}
set disabled(disable) {
this._disabled = disable;
}
handleEvent(event) {
if (this._disabled) {
this._buttonsPressed = 0x0;
this._buttonsClicked = 0x0;
this._keyInputSequence = null;
Controller.consumeEvent(event);
return;
}
if (!Controller.isHTMLFormElement(event.target)) {
const method = this["on_" + event.type];
if (method) {
method.call(this, event);
}
}
}
static consumeEvent(event) {
event.preventDefault();
event.stopImmediatePropagation();
}
static isHTMLFormElement(elem) {
const name = elem.localName;
return (name &&
(name === "input" || name === "label" ||
name === "select" || name === "option" || name === "optgroup" ||
name === "textarea"));
}
on_mousedown(event) {
this._buttonsPressed = 0x0;
this._buttonsClicked = 0x0;
if (event.buttons !== 0x0 && this.insideViewDiv(event)) {
this._buttonsPressed = event.buttons;
let id = MouseInput.toId(USER_INPUT_MOUSE_DOWN,
this._buttonsPressed,
event.shiftKey, event.ctrlKey,
event.altKey, event.metaKey,
0);
if (!this.onMouseInput(event, id)) {
Controller.consumeEvent(event);
}
}
}
insideViewDiv(event) {
let rect = this._viewContainer.getBoundingClientRect();
let clientX = event.clientX - rect.left;
let clientY = event.clientY - rect.top;
return (clientX >= 0 && clientX < this._viewContainer.clientWidth &&
clientY >= 0 && clientY < this._viewContainer.clientHeight);
}
onMouseInput(event, id) {
let binding = this._mouseBindings[id];
if (binding) {
this.executeCommand(binding, event);
return true;
}
return false;
}
executeCommand(binding, event) {
this.doExecuteCommand(binding.command, binding.commandParams, event);
}
doExecuteCommand(command, commandParams, event) {
command.execute(EXECUTE_NORMAL, this._docView, commandParams, event)
.catch((error) => {
console.error(`Controller.doExecuteCommand: \
failed to execute command \
"${command.getCommandString(commandParams)}": ${error}`);
});
}
on_mousemove(event) {
if (this._buttonsPressed !== 0x0) {
let id = MouseInput.toId(USER_INPUT_MOUSE_DRAG,
this._buttonsPressed,
event.shiftKey, event.ctrlKey,
event.altKey, event.metaKey,
0);
this.onMouseInput(event, id);
}
}
on_mouseleave(event) {
if (this._buttonsPressed !== 0x0) {
this.on_mouseup(event);
}
}
on_mouseup(event) {
if (this._buttonsPressed !== 0x0) {
let id = MouseInput.toId(USER_INPUT_MOUSE_UP,
this._buttonsPressed,
event.shiftKey, event.ctrlKey,
event.altKey, event.metaKey,
0);
this._buttonsClicked = (event.type === "mouseup")?
this._buttonsPressed : 0x0;
this._buttonsPressed = 0x0;
this.onMouseInput(event, id);
}
}
on_click(event) {
if (this._buttonsClicked !== 0x0) {
let id = MouseInput.toId(USER_INPUT_MOUSE_CLICK,
this._buttonsClicked,
event.shiftKey, event.ctrlKey,
event.altKey, event.metaKey,
event.detail);
this._buttonsPressed = 0x0;
this._buttonsClicked = 0x0;
this.onMouseInput(event, id);
}
}
on_auxclick(event) {
if (this._buttonsClicked !== 0x0) {
let id = MouseInput.toId(USER_INPUT_MOUSE_CLICK,
this._buttonsClicked,
event.shiftKey, event.ctrlKey,
event.altKey, event.metaKey,
event.detail);
this._buttonsPressed = 0x0;
this._buttonsClicked = 0x0;
this.onMouseInput(event, id);
}
}
on_contextmenu(event) {
this._buttonsPressed = 0x0;
this._buttonsClicked = 0x0;
if (this.insideViewDiv(event)) {
let id = MouseInput.computeId(USER_INPUT_CONTEXT_MENU,  0,
0x0,  0);
this.onMouseInput(event, id);
} else {
Controller.consumeEvent(event);
}
}
on_keydown(event) {
let key = event.key;
if (key) {
let id = KeyDownInput.toId(key, event.shiftKey, event.ctrlKey,
event.altKey, event.metaKey);
if (this._keyInputSequence === null) {
if (id === this._leftKeyDownId ||
id === this._rightKeyDownId) {
let binding = this._keyboardBindings[id];
if (binding &&
binding.commandName === "moveDotTo") {
if (!this._contenteditableState.moveDotTo(
this._docView, binding.commandParams, event)) {
this.executeCommand(binding, event);
}
return;
}
} else if (id === this._deleteKeyDownId ||
id === this._backSpaceKeyDownId) {
let binding = this._keyboardBindings[id];
if (binding &&
binding.commandName.endsWith("eleteChar")) {
if (!this._contenteditableState.deleteChars(
this._docView, binding.commandParams, event)) {
this.executeCommand(binding, event);
}
this._charsDeleted = true;
return;
}
}
}
this.onKeyInput(event, id);
}
}
onKeyInput(event, id) {
let seq;
if (this._keyInputSequence !== null) {
seq = this._keyInputSequence + '\n' + id;
} else {
seq = id;
}
let binding = this._keyboardBindings[seq];
if (binding) {
if (typeof binding === "string") {
this._keyInputSequence = seq;
Controller.consumeEvent(event);
} else {
this._keyInputSequence = null;
this.executeCommand(binding, event);
}
return true;
}
return false;
}
on_keypress(event) {
let key = event.key;
if (key) {
let id = KeyPressInput.toId(key);
if (this.onKeyInput(event, id)) {
return;
}
}
}
on_beforeinput(event) {
this._typedChars = null;
const charsDeleted = this._charsDeleted;
this._charsDeleted = false;
switch (event.inputType) {
case "deleteContentBackward":
case "deleteContentForward":
case "deleteContent":
if (charsDeleted) {
return;
}
break;
case "insertText":
case "insertCompositionText":
case "insertReplacementText":
case "insertFromComposition":
case "deleteByComposition":
case "deleteCompositionText":
break;
default:
Controller.consumeEvent(event);
return;
}
this._typedChars = event.data;
if (this._typedChars === null &&
event.dataTransfer) {
this._typedChars = event.dataTransfer.getData("text/plain");
}
if (!this._typedChars) {
this._typedChars = "";
}
if (!this._contenteditableState.canTypeChars(this._docView,
this._typedChars,
event)) {
Controller.consumeEvent(event);
if (event.inputType === "insertText" &&
this._typedChars.length > 0) {
this.doExecuteCommand(this._insertStringCmd,
this._typedChars, event);
}
this._typedChars = null;
}
}
on_input(event) {
const typedChars = this._typedChars;
this._typedChars = null;
this._charsDeleted = false;
if (typedChars !== null) {
this._contenteditableState.typeChars(this._docView, typedChars,
event);
}
}
on_keyup(event) {
Controller.consumeEvent(event);
}
on_compositionupdate(event) {
if (event.data.length > 0) {
if (this._contenteditableState.canEdit()) {
if (this._docView.hasTextSelection() ||
this._docView.hasNodeSelection()) {
this._contenteditableState.setEnabled(false);
this._cancelSelectionCmd.execute(EXECUTE_NORMAL,
this._docView,
null, null)
.then(this._enableContenteditableState,
this._enableContenteditableState);
}
} else {
this._contenteditableState.setEnabled(false);
setTimeout(this._enableContenteditableState, 10 );
}
}
}
enableContenteditableState() {
this._contenteditableState.setEnabled(true);
this._docView.requestFocus();
}
}
Controller.HANDLED_EVENT_TYPES = [
"mousedown", "mousemove", "mouseleave", "mouseup",
"click", "auxclick", "contextmenu",
"keydown", "keypress",  "beforeinput", "input", "keyup",
"compositionupdate",
];
class ContenteditableState {
constructor() {
this._dot = null;
this._dotOffset = -1;
this._dotContent = null;
this._isEditable = false;
this._syncState = 0;
this._pos = [null, -1];
}
reset(dot, dotOffset) {
if (this._dot !== null) {
if (this._syncState > 0 && this._dotContent !== null) {
NodeView.normalizeTextualContent(this._dotContent);
}
this._dot = null;
this._dotOffset = -1;
this._dotContent = null;
this._isEditable = false;
this._syncState = 0;
}
if (dot !== null) {
this._dot = dot;
this._dotOffset = dotOffset;
this._dotContent = NodeView.getTextualContent(dot);
this._isEditable = (this._dotContent === null)? false :
ContenteditableState.isEditable(this._dotContent);
}
}
static isEditable(node) {
if (node === null) {
return false;
}
let elem = node;
if (node.nodeType !== Node.ELEMENT_NODE) {
elem = node.parentElement;
}
while (elem !== null) {
const classList = elem.classList;
if (classList.contains("xxe-rw")) {
return true;
} else if (classList.contains("xxe-ro")) {
return false;
} else if (classList.contains("xxe-re")) {
return true;
} else if (classList.contains("xxe-nr")) {
return false;
}
elem = elem.parentElement;
}
return true;
}
get dot() {
return this._dot;
}
get dotOffset() {
return this._dotOffset;
}
syncing(syncArgs) {
let proceed = false;
syncArgs[0] = null;
syncArgs[1] = null;
syncArgs[2] = -1;
const prevState = this._syncState;
switch (prevState) {
case 1: case 11:
syncArgs[0] = NodeView.getUID(this._dot);
syncArgs[2] = this._dotOffset;
proceed = true;
this._syncState = (prevState === 1)? 0 : 10;
break;
case 2: case 12:
syncArgs[0] = NodeView.getUID(this._dot);
syncArgs[1] = this._dotContent.textContent;
syncArgs[2] = this._dotOffset;
proceed = true;
this._syncState = 10;
break;
case 0: case 10:
default:
break;
}
return proceed;
}
updateSyncState(docView, incr) {
docView.magicX = -1;
const prevState = this._syncState;
this.incrSyncState(incr);
if (this._syncState === 2 && prevState < 2) {
setTimeout(() => { docView.sync(); }, 0 );
}
}
incrSyncState(incr) {
assertOrError(incr === 1 || incr === 2);
const prevState = this._syncState;
switch (prevState) {
case 0: case 10:
this._syncState = prevState + incr;
break;
case 1: case 11:
if (incr === 2) {
++this._syncState;
}
break;
case 2: case 12:
break;
}
}
resetAutoSync() {
const prevState = this._syncState;
if (prevState >= 10) {
this._syncState = prevState - 10;
}
}
moveDotTo(docView, params, event) {
if (this._dotContent !== null &&
!docView.hasTextSelection() && !docView.hasNodeSelection()) {
const prev = ("previousChar" === params);
if (prev || "nextChar" === params) {
const sel = window.getSelection();
if (sel.isCollapsed) {
const length = DOMUtil.textContentLength(this._dotContent);
if (prev && this._dotOffset > 0) {
--this._dotOffset;
this.updateSyncState(docView, 1);
return true;
} else if (!prev && this._dotOffset < length) {
if (BROWSER_ENGINE_IS_GECKO &&
this._dotOffset+1 === length) {
return false;
} else {
++this._dotOffset;
this.updateSyncState(docView, 1);
return true;
}
}
} else {
this.updateDotOffset(sel, docView, 1);
return true;
}
}
}
return false;
}
deleteChars(docView, params, event) {
if (this._dotContent !== null &&
!docView.hasTextSelection() && !docView.hasNodeSelection()) {
if (this._isEditable) {
const sel = window.getSelection();
if (sel.isCollapsed) {
const backwards = ("backwards" === params);
const length = DOMUtil.textContentLength(this._dotContent);
if (backwards && this._dotOffset > 0) {
--this._dotOffset;
this.updateSyncState(docView, 2);
return true;
} else if (!backwards && this._dotOffset < length) {
this.updateSyncState(docView, 2);
return true;
}
} else {
this.updateDotOffset(sel, docView, 2);
return true;
}
} else {
ContenteditableState.consumeEvent(event);
return true;
}
}
return false;
}
static consumeEvent(event) {
assertOrError(event !== null);
event.preventDefault();
event.stopImmediatePropagation();
}
canTypeChars(docView, chars, event) {
if (this._dotContent !== null &&
!docView.hasTextSelection() && !docView.hasNodeSelection()) {
if (this._isEditable) {
return true;
} else {
ContenteditableState.consumeEvent(event);
return true;
}
}
return false;
}
typeChars(docView, chars, event) {
this.updateDotOffset(window.getSelection(), docView, 2);
return true;
}
updateDotOffset(sel, docView, incr) {
setTimeout(() => {
this.doUpdateDotOffset(sel, docView, incr);
}, 0 );
}
doUpdateDotOffset(sel, docView, incr) {
const prevDotOffset = this._dotOffset;
this.dotOffsetFromSel(sel);
if (incr > 1 || this._dotOffset !== prevDotOffset) {
this.updateSyncState(docView, incr);
}
}
dotOffsetFromSel(sel) {
let dotOffset = -1;
this._pos[0] = sel.focusNode;
this._pos[1] = sel.focusOffset;
if (NodeView.charToTextualContentOffset(this._pos) &&
this._pos[0] === this._dotContent) {
dotOffset = this._pos[1];
}
if (!sel.isCollapsed && dotOffset >= 0) {
this._pos[0] = sel.anchorNode;
this._pos[1] = sel.anchorOffset;
if (NodeView.charToTextualContentOffset(this._pos) &&
this._pos[0] === this._dotContent) {
dotOffset = Math.min(this._pos[1], dotOffset);
}
}
if (dotOffset < 0) {
this._dotOffset = 0;
console.error(`ContenteditableState.dotOffsetFromSel: \
INTERNAL ERROR: cannot convert \
focus offset ${sel.focusOffset} in ${DOMUtil.dumpNode(sel.focusNode)} \
to dot offset in ${DOMUtil.dumpNode(this._dotContent)}`);
} else {
const length = DOMUtil.textContentLength(this._dotContent);
if (dotOffset > length) {
dotOffset = length;
}
this._dotOffset = dotOffset;
}
}
moveDotAt(docView, event) {
this.updateDotOffset(window.getSelection(), docView, 1);
return true;
}
canEdit() {
return (this._dotContent !== null && this._isEditable);
}
setEnabled(enable) {
if (this._dotContent !== null) {
if (enable) {
if (!this._dotContent.isContentEditable) {
this._dotContent.contentEditable = "true";
}
} else {
if (this._dotContent.isContentEditable) {
this._dotContent.contentEditable = "false";
}
}
}
}
}
export class DocumentView extends HTMLElement {
constructor() {
super();
this._xmlEditor = null;
this._button2PastesText = false;
this._contenteditableState = null;
this._markManager = null;
this._bindings = null;
this._cmdToBinding = null;
this._appEventToBinding = null;
this._controller = null;
this._magicX = -1;
this._addColumnResizers = false;
this._syncArgs = [ null,  null,  -1];
this._domTree = document;
this._styles = null;
this._view = null;
}
connectedCallback() {
if (this.firstChild === null) {
this._styles = document.createElement("style");
let head = document.querySelector("head");
if (head !== null) {
head.appendChild(this._styles);
}
this.setAttribute("tabindex", "0");
}
}
disconnectedCallback() {
let styles = this._styles;
this._styles = null;
if (styles !== null && styles.parentNode !== null) {
styles.parentNode.removeChild(styles);
}
}
get xmlEditor() {
return this._xmlEditor;
}
set xmlEditor(editor) {
assertOrError(editor !== null);
this._xmlEditor = editor;
this._button2PastesText = editor.button2PastesText;
const clipboardIntegration = editor.clipboardIntegration;
clipboardIntegration.getLevel()
.then((level) => {
if (level === ClipboardIntegration.CAN_READ_WRITE) {
clipboardIntegration.autoUpdatePrivateClipboard(this);
}
});
}
get button2PastesText() {
return this._button2PastesText;
}
set button2PastesText(pastes) {
this._button2PastesText = pastes;
}
setView(settings) {
this._styles.textContent = "";
this.innerHTML = "";
this._contenteditableState = null;
if (this._markManager !== null) {
this._markManager.dispose();
this._markManager = null;
}
if (this._controller !== null) {
this._controller.dispose();
this._controller = null;
this._bindings = null;
this._cmdToBinding = null;
this._appEventToBinding = null;
}
this._magicX = -1;
this._addColumnResizers = false;
if (settings !== null) {
this._styles.textContent = settings.styles? settings.styles : "";
this.innerHTML = settings.view? settings.view : "";
this._contenteditableState = new ContenteditableState();
this._markManager =
new MarkManager(this, settings.marks? settings.marks : null);
this._bindings =
Bindings.compile(settings.bindings? settings.bindings : null);
this._cmdToBinding = null;
this._appEventToBinding = null;
this._controller = new Controller(this, this._bindings);
this._addColumnResizers =
(this.getAppEventBinding("resize-table-column") !== null &&
this.canEdit());
this.processNewViews(this.firstChild);
}
}
processNewViews(tree) {
if (this._addColumnResizers) {
ColumnResizer.addResizers(tree);
}
}
get viewContainer() {
return this;
}
get magicX() {
return this._magicX;
}
set magicX(x) {
this._magicX = x;
}
clientToView(clientX, clientY) {
let coords = [clientX, clientY];
this.convertClientToView(coords);
return coords;
}
convertClientToView(coords) {
const docView = this.viewContainer;
const docViewRect = docView.getBoundingClientRect();
coords[0] = coords[0] - docViewRect.left + docView.scrollLeft;
coords[1] = coords[1] - docViewRect.top + docView.scrollTop;
}
viewToClient(viewX, viewY) {
let coords = [viewX, viewY];
this.converViewToClient(coords);
return coords;
}
converViewToClient(coords) {
const docView = this.viewContainer;
const docViewRect = docView.getBoundingClientRect();
coords[0] = coords[0] - docView.scrollLeft + docViewRect.left;
coords[1] = coords[1] - docView.scrollTop + docViewRect.top;
}
get contenteditableState() {
return this._contenteditableState;
}
onDocumentViewChanged(event) {
this._markManager.changingDocumentView();
let redrawMarks = false;
for (let change of event.changes) {
switch (change.changeType) {
case CHANGE_INSERT_TEXT:
case CHANGE_REPLACE_TEXT:
case CHANGE_DELETE_TEXT:
this.changeText(change);
break;
case CHANGE_REPLACE_VIEW:
redrawMarks = true;
//FALLTHROUGH
case CHANGE_REMOVE_VIEW:
this.replaceView(change);
break;
case CHANGE_INSERT_VIEW:
this.insertView(change);
break;
case CHANGE_PATCH_TEXT:
this.patchText(change);
break;
case CHANGE_UPDATE_CUSTOM_PART:
this.updateCustomPart(change);
break;
}
}
this._markManager.documentViewChanged(redrawMarks);
}
patchText(change) {
let element = this._domTree.getElementById(change.id);
if (element !== null) {
element.textContent = change.text;
}
}
updateCustomPart(change) {
let element = this._domTree.getElementById(change.id);
if (element !== null) {
let name = change.invoke[0];
let args = (change.invoke.length > 1)?
change.invoke.slice(1) : null;
element[name].apply( element, args);
}
}
changeText(change) {
let textContent = this.getNodeViewTextualContent(change.uid);
if (textContent === null) {
return;
}
NodeView.normalizeTextualContent(textContent);
let chars = textContent.firstChild;
let offset = change.offset;
if (offset >= 0 && offset <= chars.length) {
switch (change.changeType) {
case CHANGE_INSERT_TEXT:
chars.insertData(offset, change.insertedText);
break;
case CHANGE_DELETE_TEXT:
chars.deleteData(offset, change.deletedCount);
break;
case CHANGE_REPLACE_TEXT:
chars.replaceData(offset, change.deletedCount,
change.insertedText);
break;
}
} else {
console.error(`DocumentView.changeText: INTERNAL ERROR: \
${offset}, invalid offset in "${chars.data}" (length=${chars.length})`);
}
}
getNodeViewContent(uid, reportError=true) {
return NodeView.content(this._domTree, uid, reportError);
}
getNodeViewTextualContent(uid, reportError=true) {
return NodeView.textualContent(this._domTree, uid, reportError);
}
getNodeView(uid, reportError=true) {
return NodeView.view(this._domTree, uid, reportError);
}
replaceView(change) {
const viewUID = change.uid;
let [replacedElem, oldView] = this.getAnchorView(viewUID);
if (oldView === null) {
return;
}
let parent = replacedElem.parentNode;
if (parent === null) {
console.error(`DocumentView.replaceView: INTERNAL ERROR: \
node view "${viewUID}" has no parent`);
return;
}
if (change.view) {
if (change.styles !== null) {
this.updateStyles(change.styles);
}
let newView = DOMUtil.createElementFromHTML(change.view);
let replacementElem = newView;
let parentView = NodeView.getParentView(oldView);
if (parentView !== null) {
let parentUID = NodeView.getUID(parentView);
let parentContent = replacedElem.parentNode;
replacementElem =
DocumentView.wrapChildView(parentContent, parentUID, newView);
}
replacedElem.parentNode.insertBefore( replacementElem,
replacedElem);
if (!change.deep) {
DocumentView.processSlot(oldView, newView, viewUID);
}
this.processNewViews(replacementElem);
}
parent.removeChild(replacedElem);
}
updateStyles(rules) {
const ruleCount = rules.length;
if (ruleCount > 0) {
let stylesheet = this._styles.sheet;
if (rules[0].startsWith("#xxe---")) {
let index = -1;
const cssRules = stylesheet.cssRules;
const cssRuleCount = cssRules.length;
for (let i = 0; i < cssRuleCount; ++i) {
if (cssRules[i].cssText.startsWith("#xxe---")) {
index = i;
break;
}
}
if (index >= 0) {
for (let i = cssRuleCount-1; i >= index; --i) {
stylesheet.deleteRule(i);
}
}
}
for (let rule of rules) {
stylesheet.insertRule(rule, stylesheet.cssRules.length);
}
}
}
getAnchorView(viewUID) {
let anchorView = this.getNodeView(viewUID);
if (anchorView === null) {
return [null, null];
}
let anchorElem = anchorView;
let parentView = NodeView.getParentView(anchorView);
if (parentView !== null) {
let parentUID = NodeView.getUID(parentView);
let wrapper = anchorView.parentElement;
while (wrapper !== null &&
wrapper.getAttribute("data-cw") === parentUID) {
anchorElem = wrapper;
wrapper = wrapper.parentElement;
}
}
return [anchorElem, anchorView];
}
static wrapChildView(parentContent, parentUID, newView) {
let wrapper = newView;
const viewTag = newView.localName;
const parentTag = parentContent.localName;
if (parentTag === "table") {
if (viewTag !== "tbody") {
if (viewTag !== "tr") {
if (viewTag !== "td") {
wrapper = DocumentView.doWrapChildView(
wrapper, "td", parentUID);
}
wrapper = DocumentView.doWrapChildView(
wrapper, "tr", parentUID);
}
wrapper = DocumentView.doWrapChildView(
wrapper, "tbody", parentUID);
}
} else if (parentTag === "tbody") {
if (viewTag !== "tr") {
if (viewTag !== "td") {
wrapper = DocumentView.doWrapChildView(
wrapper, "td", parentUID);
}
wrapper = DocumentView.doWrapChildView(
wrapper, "tr", parentUID);
}
} else if (parentTag === "tr") {
if (viewTag !== "td") {
wrapper = DocumentView.doWrapChildView(
newView, "td", parentUID);
}
}
return wrapper;
}
static doWrapChildView(view, wrapperTag, parentUID) {
let wrapper = document.createElement(wrapperTag);
wrapper.setAttribute("data-cw", parentUID);
wrapper.appendChild(view);
return wrapper;
}
static processSlot(oldView, newView, viewUID) {
let oldViewContent = NodeView.getContent(oldView);
let newViewContent = NodeView.getContent(newView);
let slot = newViewContent.getElementsByTagName("slot").item(0);
if (slot !== null) {
if (!NodeView.isReplacedContent(newViewContent)) {
let child = oldViewContent.firstChild;
while (child !== null) {
let nextChild = child.nextSibling;
if (!NodeView.isContentBefore(child, viewUID) &&
!NodeView.isContentAfter(child, viewUID)) {
oldViewContent.removeChild(child);
newViewContent.insertBefore( child,
slot);
}
child = nextChild;
}
}
newViewContent.removeChild(slot);
}
}
insertView(change) {
let [parentContent, beforeSibling] =
this.getInsertPosition(change.parentUID, change.beforeUID,
change.afterUID, change.viewTag);
if (parentContent === null) {
return;
}
if (change.styles !== null) {
this.updateStyles(change.styles);
}
let newView = DOMUtil.createElementFromHTML(change.view);
let insertedElem =
DocumentView.wrapChildView(parentContent, change.parentUID, newView);
if (beforeSibling !== null) {
beforeSibling.parentNode.insertBefore( insertedElem,
beforeSibling);
} else {
beforeSibling = parentContent.lastChild;
if (beforeSibling !== null &&
NodeView.isContentAfter(beforeSibling, change.parentUID)) {
beforeSibling.parentNode.insertBefore(insertedElem,
beforeSibling);
} else {
parentContent.appendChild(insertedElem);
}
}
this.processNewViews(insertedElem);
}
getInsertPosition(parentUID, beforeUID, afterUID, viewTag) {
let parentContent = this.getNodeViewContent(parentUID);
if (parentContent === null) {
return [null, null];
}
let beforeSibling = null;
let beforeView = null;
if (beforeUID !== null) {
[beforeSibling, beforeView] = this.getAnchorView(beforeUID);
if (beforeSibling === null) {
return [null, null];
}
}
let tableChildGroup =
parentContent.querySelector(`table[data-cg='${parentUID}']`);
if (tableChildGroup === null) {
return [parentContent, beforeSibling];
}
let headerChildGroup =
parentContent.querySelector(`header[data-cg='${parentUID}']`);
if (headerChildGroup === null) {
console.error(`DocumentView.getInsertionParent: \
INTERNAL ERROR: \
cannot find the <header> child group inside table view '${parentUID}'`);
return [null, null];
}
let tbodyChildGroup =
tableChildGroup.querySelector(`tbody[data-cg='${parentUID}']`);
if (tbodyChildGroup === null) {
console.error(`DocumentView.getInsertionParent: \
INTERNAL ERROR: \
cannot find the <tbody> child group inside <table data-cg='${parentUID}'>`);
return [null, null];
}
let afterSibling = null;
let afterView = null;
if (afterUID !== null) {
[afterSibling, afterView] = this.getAnchorView(afterUID);
if (afterSibling === null) {
return [null, null];
}
}
if (viewTag === "nav" ||
viewTag === "tbody[header]" || viewTag === "tbody" ||
viewTag === "tr" || viewTag === "td") {
let childGroup;
if (viewTag === "nav") {
childGroup = headerChildGroup;
} else if (viewTag === "tbody[header]") {
childGroup = tableChildGroup;
if (beforeSibling !== null &&
beforeSibling.parentNode !== tableChildGroup) {
beforeSibling = tbodyChildGroup;
}
} else if (viewTag === "tbody") {
childGroup = tableChildGroup;
} else {
if (afterSibling !== null &&
afterSibling.localName === "tbody" &&
afterSibling.getAttribute("role") !== "header") {
childGroup = tableChildGroup;
} else {
childGroup = tbodyChildGroup;
}
}
if (beforeSibling !== null &&
beforeSibling.parentNode !== childGroup) {
beforeSibling = null;
}
return [childGroup, beforeSibling];
}
let whichParent = null;
let whichBefore = null;
if (beforeSibling === null) {
if (afterSibling === null) {
whichParent = parentContent;
whichBefore = headerChildGroup;
} else {
whichParent = parentContent;
whichBefore = DocumentView.ensureAnchorIsChild(
parentContent, afterSibling).nextElementSibling;
if (whichBefore === tableChildGroup) {
whichBefore = null;
}
}
} else {
if (afterSibling === null) {
whichParent = parentContent;
whichBefore = DocumentView.ensureAnchorIsChild(parentContent,
beforeSibling);
if (whichBefore === tableChildGroup) {
whichBefore = headerChildGroup;
}
} else {
if (beforeSibling.parentNode === afterSibling.parentNode) {
whichParent = beforeSibling.parentNode;
whichBefore = beforeSibling;
} else {
whichParent = parentContent;
whichBefore =
DocumentView.ensureAnchorIsChild(parentContent,
beforeSibling);
}
}
}
return [whichParent, whichBefore];
}
static ensureAnchorIsChild(parentContent, anchor) {
if (anchor !== null && anchor.parentNode !== parentContent) {
let ancestor = anchor.parentNode;
while (ancestor !== null) {
if (ancestor === parentContent) {
break;
}
anchor = ancestor;
ancestor = ancestor.parentNode;
}
}
return anchor;
}
onDocumentMarksChanged(event) {
this._markManager.documentMarksChanged(event);
}
saveStateChanged() {
if (!this._xmlEditor.saveNeeded) {
this._contenteditableState.resetAutoSync();
}
}
get bindings() {
return this._bindings;
}
getAppEventBinding(name) {
if (this._appEventToBinding === null && this._bindings !== null) {
this._appEventToBinding = {};
for (let binding of this._bindings) {
let userInput = binding.userInput;
if (userInput instanceof AppEvent) {
this._appEventToBinding[userInput.name] = binding;
}
}
}
if (this._appEventToBinding === null) {
return null;
} else {
let binding = this._appEventToBinding[name];
return !binding? null : binding;
}
}
getBindingForCommand(cmdName, cmdParams) {
if (this._cmdToBinding === null && this._bindings !== null) {
this._cmdToBinding = {};
for (let binding of this._bindings) {
let key = DocumentView.cmdToBindingKey(binding.commandName,
binding.commandParams);
this._cmdToBinding[key] = binding;
}
}
if (this._cmdToBinding === null) {
return null;
} else {
let key = DocumentView.cmdToBindingKey(cmdName, cmdParams);
let binding = this._cmdToBinding[key];
return !binding? null : binding;
}
}
static cmdToBindingKey(cmdName, cmdParams) {
let key = cmdName;
if (cmdParams !== null && (cmdParams = cmdParams.trim()).length > 0) {
key += " " + cmdParams;
}
return key;
}
get namespacePrefixes() {
return this._xmlEditor.namespacePrefixes;
}
showStatus(text, autoErase=true) {
this._xmlEditor.showStatus(text, autoErase);
}
getCommandState(cmdName, cmdParam) {
return this._xmlEditor.getCommandState(cmdName, cmdParam);
}
get dot() {
return ((this._contenteditableState === null)?
null : this._contenteditableState.dot);
}
get dotOffset() {
return ((this._contenteditableState === null)?
-1 : this._contenteditableState.dotOffset);
}
get mark() {
return (this._markManager === null)? null : this._markManager.mark;
}
get markOffset() {
return (this._markManager === null)? -1 : this._markManager.markOffset;
}
get selected() {
return (this._markManager === null)? null : this._markManager.selected;
}
get selected2() {
return (this._markManager === null)? null : this._markManager.selected2;
}
hasTextSelection() {
const dot = this.dot;
const mark = this.mark;
return (dot !== null && mark !== null &&
(mark !== dot || this.markOffset !== this.dotOffset));
}
getSelectedText() {
if (!this.hasTextSelection()) {
return null;
}
let sel = null;
const highlights = this.getElementsByClassName("xxe-text-sel");
const count = highlights.length;
for (let i = 0; i < count; ++i) {
let highlight = highlights.item(i);
let span = highlight.textContent;
if (span !== null && span.length > 0) {
if (sel === null) {
sel = span;
} else {
sel += span;
}
}
}
return sel;
}
requestFocus() {
if (this._markManager !== null) {
this._markManager.redrawDot();
}
}
makeSelectionVisible(expand=false) {
if (this._markManager !== null) {
this._markManager.makeSelectionVisible(expand);
}
}
hasNodeSelection() {
return (this.selected !== null);
}
selectNode(view, show=false) {
let uid = NodeView.getUID(view);
assertOrError(uid !== null);
let marks = { mark: "", selected2: "", selected: uid };
if (show) {
marks.reason = "showing";
}
this.ensureDotIsInside(view, marks);
return this.sendSetMarks(marks);
}
ensureDotIsInside(view, marks) {
let dot = this.dot;
if (dot === null || !view.contains(dot)) {
let text = NodeView.findTextualContent(view);
if (text !== null) {
marks.dot = text.id;
marks.dotOffset = 0;
}
}
}
selectNodeRange(view1, view2) {
let uid1 = NodeView.getUID(view1);
assertOrError(uid1 !== null);
let uid2 = NodeView.getUID(view2);
assertOrError(uid2 !== null);
assertOrError(view1.parentNode === view2.parentNode);
if (view1 === view2) {
return this.selectNode(view1);
}
let marks = { mark: "", selected: uid1, selected2: uid2 };
this.ensureDotIsInside(view2, marks);
return this.sendSetMarks(marks);
}
pickTextualView(mouseEvent, pickedText=null) {
let picked;
if (pickedText !== null) {
picked = pickedText;
picked[0] = null;
picked[1] = -1;
} else {
picked = [null, -1];
}
if (DocumentView.pickTextualContent(mouseEvent, picked)) {
let textContent = picked[0];
picked[0] = NodeView.contentToView(textContent, textContent.id);
}
return picked;
}
static pickTextualContent(mouseEvent, picked) {
let part = NodeView.lookupViewPart(mouseEvent.target);
if (part !== null && NodeView.isActualContent(part)) {
const clientX = mouseEvent.clientX;
const clientY = mouseEvent.clientY;
let textContent =
NodeView.pickNearestTextualContent(part, clientX, clientY);
if (textContent !== null) {
let chars = textContent.textContent;
if (chars === null || chars.length === 0) {
picked[0] = textContent;
picked[1] = 0;
return true;
}
const rects = textContent.getClientRects();
const lastRect = rects.length-1;
for (let i = 0; i <= lastRect; ++i) {
const rect = rects[i];
if (rect !== null &&
rect.height > 0 &&
clientY >= rect.top && clientY < rect.bottom) {
if (clientX <= rect.left) {
if (i === 0) {
picked[0] = textContent;
picked[1] = 0;
return true;
} else {
if (DocumentView.caretPositionFromPoint(
rect.left, clientY, picked)) {
let done =
NodeView.charToTextualContentOffset(
picked);
return done;
}
}
} else if (clientX >= rect.right) {
if (i === lastRect) {
picked[0] = textContent;
picked[1] = chars.length;
return true;
} else {
if (DocumentView.caretPositionFromPoint(
rect.right, clientY, picked)) {
let done =
NodeView.charToTextualContentOffset(
picked);
return done;
}
}
} else {
if (DocumentView.caretPositionFromPoint(
clientX, clientY, picked)) {
let done =
NodeView.charToTextualContentOffset(
picked);
return done;
}
}
}
}
}
}
return false;
}
static caretPositionFromPoint(clientX, clientY, pos) {
if (document.caretPositionFromPoint) {
let caretPos = document.caretPositionFromPoint(clientX, clientY);
if (caretPos && caretPos.offsetNode !== null) {
switch (caretPos.offsetNode.nodeType) {
case Node.TEXT_NODE:
pos[0] = caretPos.offsetNode;
pos[1] = caretPos.offset;
return true;
case Node.ELEMENT_NODE:
if (NodeView.isTextualContent(caretPos.offsetNode)) {
let child = caretPos.offsetNode.firstChild;
if (child === null) {
pos[0] = caretPos.offsetNode;
pos[1] = 0;
return true;
} else if (child.nodeType === Node.TEXT_NODE &&
child.nextSibling === null &&
child.length === 0) {
pos[0] = child;
pos[1] = 0;
return true;
}
}
break;
}
}
} else if (document.caretRangeFromPoint) {
let range = document.caretRangeFromPoint(clientX, clientY);
if (range && range.startContainer !== null) {
switch (range.startContainer.nodeType) {
case Node.TEXT_NODE:
pos[0] = range.startContainer;
pos[1] = range.startOffset;
return true;
case Node.ELEMENT_NODE:
if (NodeView.isTextualContent(range.startContainer)) {
let child = range.startContainer.firstChild;
if (child === null) {
pos[0] = range.startContainer;
pos[1] = 0;
return true;
} else if (child.nodeType === Node.TEXT_NODE &&
child.nextSibling === null &&
child.length === 0) {
pos[0] = child;
pos[1] = 0;
return true;
}
}
break;
}
}
}
return false;
}
selectionContains(mouseEvent) {
return ((this._markManager === null)? false :
this._markManager.selectionContains(mouseEvent.clientX,
mouseEvent.clientY));
}
sendSetMarks(marks) {
return this.sync()
.then((synced) => {
if (synced) {
return this.doSendSetMarks(marks);
} else {
return false;
}
});
}
doSendSetMarks(marks) {
const dragging = (marks.reason === "dragging");
if (!dragging) {
this.disableController(true);
}
return this._xmlEditor.sendRequest("setMarks", marks)
.then((result) => {
if (!dragging) {
this.disableController(false);
}
return result;
})
.catch((error) => {
if (!dragging) {
this.disableController(false);
}
console.error(`Request "setMarks" marks=${marks} \
has failed: ${error}`);
return false;
});
}
disableController(disable) {
if (this._controller !== null) {
this._controller.disabled = disable;
}
}
sendSetClipboard(copied) {
return this.sync()
.then((synced) => {
if (synced) {
return this.doSendSetClipboard(copied);
} else {
return false;
}
});
}
doSendSetClipboard(copied) {
this.disableController(true);
return this._xmlEditor.sendRequest("setClipboard", copied)
.then((result) => {
this.disableController(false);
return result;
})
.catch((error) => {
this.disableController(false);
console.error(`Request "setClipboard" copied="${copied}" \
has failed: ${error}`);
return false;
});
}
updatePrimarySelection() {
if (this._button2PastesText) {
let copied = this.getSelectedText();
if (copied) {
this.sendSetPrimarySelection(copied);
}
}
}
sendSetPrimarySelection(copied) {
return this.sync()
.then((synced) => {
if (synced) {
return this.doSendSetPrimarySelection(copied);
} else {
return false;
}
});
}
doSendSetPrimarySelection(copied) {
this.disableController(true);
return this._xmlEditor.sendRequest("setPrimarySelection", copied)
.then((result) => {
this.disableController(false);
return result;
})
.catch((error) => {
this.disableController(false);
console.error(`Request "setPrimarySelection" \
copied="${copied}" has failed: ${error}`);
return false;
});
}
canEdit() {
let rootView = this.firstChild;
if (rootView !== null && rootView.nodeType === Node.ELEMENT_NODE &&
rootView.classList.contains("xxe-re")) {
return true;
} else {
return false;
}
}
executeCommand(mode, commandName, commandParams, event=null) {
let command = this.getCommand(commandName);
return command.execute(mode, this, commandParams, event);
}
getCommand(commandName) {
let command = ALL_LOCAL_COMMANDS[commandName];
if (!command) {
command = new RemoteCommand(commandName);
}
return command;
}
sendExecuteCommand(mode, commandName, commandParams) {
return this.sync()
.then((synced) => {
if (synced) {
return this.doSendExecuteCommand(mode, commandName,
commandParams);
} else {
return null;
}
});
}
doSendExecuteCommand(mode, commandName, commandParams) {
this.disableController(true);
return this._xmlEditor.sendRequest("executeCommand",
mode, commandName, commandParams)
.then((result) => {
this.disableController(false);
if (mode === EXECUTE_TEST) {
return result;
} else {
return CommandResult.deserialize(result);
}
})
.catch((error) => {
this.disableController(false);
console.error(`Request "executeCommand" \
mode=${mode} commandName="${commandName}" commandParams="${commandParams}" \
has failed: ${error}`);
return null;
});
}
sync() {
const cs = this._contenteditableState;
if (cs !== null && cs.syncing(this._syncArgs)) {
assertOrError(this._markManager.dot === cs.dot);
this._markManager.dotOffset = cs.dotOffset;
return this._xmlEditor.sendRequest("sync", ...this._syncArgs)
.catch((error) => {
console.error(`Request "sync" args=${this._syncArgs} \
has failed: ${error}`);
return false;
});
} else {
return Promise.resolve(true);
}
}
}
window.customElements.define("xxe-document-view", DocumentView);
class CenterCmd extends Command {
constructor() {
super();
}
execute(mode, docView, params, event=null) {
const dot = docView.dot;
let canExecute = (dot !== null || docView.selected !== null);
if (mode === EXECUTE_TEST) {
return Promise.resolve(canExecute);
} else {
if (!canExecute) {
Command.consumeEvent(event);
return Promise.resolve(null);
}
}
Command.consumeEvent(event);
let center = null;
let cls = null;
let textSel = false;
if (docView.hasTextSelection()) {
cls = "xxe-text-sel";
textSel = true;
} else if (docView.hasNodeSelection()) {
cls = "xxe-node-sel";
}
if (cls !== null) {
const highlights =
docView.viewContainer.getElementsByClassName(cls);
const count = highlights.length;
for (let i = 0; i < count; ++i) {
let highlight = highlights.item(i);
if ((textSel && dot.contains(highlight)) ||
(!textSel && highlight.contains(dot))) {
center = highlight;
break;
}
}
if (center === null && count > 0) {
center = highlights.item(0);
}
}
if (center === null) {
center = dot;
}
if (center !== null) {
center.scrollIntoView({ block: "center", inline: "center" });
}
return Promise.resolve(CommandResult.DONE);
}
}
ALL_LOCAL_COMMANDS.center = new CenterCmd();
export class OpenDocumentCmd extends Command {
constructor() {
super();
}
execute(mode, docView, params, event=null) {
let readOnly = false;
let remoteURI = null;
let localURI = null;
if (params !== null) {
params = params.trim();
if (params.startsWith("[readWrite]")) {
readOnly = false;
params = params.substring(11);
} else if (params.startsWith("[readOnly]")) {
readOnly = true;
params = params.substring(10);
}
let docURIs = splitTrimString(params, /\s+/);
if (docURIs.length > 0) {
remoteURI = docURIs[0];
}
if (remoteURI !== null) {
if (remoteURI.startsWith("csri:")) {
localURI = URIUtil.csriURLToURI(remoteURI);
remoteURI = null;
}
}
}
if (mode === EXECUTE_TEST) {
return Promise.resolve(remoteURI !== null);
}
Command.consumeEvent(event);
if (remoteURI === null) {
if (localURI !== null) {
XUI.Alert.showError(`Don't know how open local files such as
"${localURI}"`,
docView);
}
return Promise.resolve(CommandResult.FAILED);
}
let readOnlyMode = readOnly? " read-only" : "";
docView.showStatus(
`Opening remote file "${remoteURI}"${readOnlyMode}...`,
true);
return this.openRemoteFile(docView, remoteURI, readOnly);
}
openRemoteFile(docView, remoteURI, readOnly) {
const newWin = window.open(window.location.href, "_blank");
if (newWin === null) {
XUI.Alert.showError(`Blocked by the browser pop-up blocker?
If this is the case, please allow pop-ups for ${window.location.host}`,
docView);
return Promise.resolve(CommandResult.FAILED);
}
newWin.name = JSON.stringify([ "openRemoteFile", remoteURI, readOnly ]);
return Promise.resolve(CommandResult.DONE);
}
}
ALL_LOCAL_COMMANDS.openDocument = new OpenDocumentCmd();
class DeclareNamespaceDialog extends XUI.Dialog {
static showDialog(nsPrefixes, editable, reference=null) {
return new Promise((resolve, reject) => {
let dialog = new DeclareNamespaceDialog(nsPrefixes, editable,
resolve);
dialog.open("center", reference);
});
}
constructor(nsPrefixes, editable, resolve) {
super({ title: (editable? "Declare Namespace" :
"Declared Namespaces (cannot be modified)"),
movable: true, closeable: true, resizable: true,
template: DeclareNamespaceDialog.TEMPLATE,
buttons: [ { label: "Cancel", action: "cancelAction" },
{ label: "OK", action: "okAction",
default: true } ]});
this._editable = editable;
this._resolve = resolve;
let content = this.contentPane.firstElementChild;
let scroller = content.firstElementChild;
this._table = scroller.firstElementChild;
this._rows = this._table.lastElementChild;
let addButton = this._table.querySelector(".xxe-declns-add");
if (!editable) {
addButton.parentNode.removeChild(addButton);
this._addButton = null;
} else {
addButton.setAttribute("title", "Add namespace/prefix");
addButton.textContent = XUI.StockIcon["plus"];
addButton.onclick = this.addAction.bind(this);
this._addButton = addButton;
}
this._onFocusRow = this.onFocusRow.bind(this);
this._onBlurRow = this.onBlurRow.bind(this);
this._onKeydownCell = this.onKeydownCell.bind(this);
this._removeAction = this.removeAction.bind(this);
for (let nsPrefix of nsPrefixes) {
let match = nsPrefix.match("xmlns(:[^=]+)?=('[^']+'|\"[^\"]+\")");
if (match.length < 3) {
continue;
}
let prefix = match[1];
if (!prefix) {
prefix = "";
} else {
prefix = prefix.substring(1);
}
let namespace = match[2].substring(1);
namespace = namespace.substring(0, namespace.length-1);
namespace = DOMUtil.unescapeXML(namespace);
let row = this.newRow(editable, namespace, prefix);
this._rows.appendChild(row);
}
}
newRow(editable, namespace, prefix) {
let row = DeclareNamespaceDialog.ROW_TEMPLATE.content.firstElementChild
.cloneNode(true);
let namespaceCell = row.firstElementChild;
let prefixCell = namespaceCell.nextElementSibling;
if (!editable) {
namespaceCell.removeAttribute("contenteditable");
prefixCell.removeAttribute("contenteditable");
} else {
namespaceCell.onfocus = this._onFocusRow;
namespaceCell.onblur = this._onBlurRow;
namespaceCell.onkeydown = this._onKeydownCell;
prefixCell.onfocus = this._onFocusRow;
prefixCell.onblur = this._onBlurRow;
prefixCell.onkeydown = this._onKeydownCell;
}
namespaceCell.textContent = namespace;
prefixCell.textContent = prefix;
let removeButton = row.querySelector(".xxe-declns-remove");
if (!editable) {
removeButton.parentNode.removeChild(removeButton);
} else {
removeButton.setAttribute("title", "Remove namespace/prefix");
removeButton.textContent = XUI.StockIcon["minus"];
removeButton.onblur = this._onBlurRow;
removeButton.onclick = this._removeAction;
}
return row;
}
onFocusRow(event) {
const focused = event.target;
const blurred = event.relatedTarget;
if (!blurred ||
DeclareNamespaceDialog.lookupRow(blurred) !==
DeclareNamespaceDialog.lookupRow(focused)) {
let removeButton = DeclareNamespaceDialog.getRowButton(focused);
if (removeButton !== null) {
removeButton.style.visibility = "visible";
}
}
}
static lookupRow(element) {
while (element !== null) {
if (element.localName === "tr") {
return element;
}
element = element.parentElement;
}
return null;
}
static getRowButton(element) {
let row = DeclareNamespaceDialog.lookupRow(element);
if (row === null) {
return null;
} else {
let toolsCell = row.lastElementChild;
return toolsCell.firstElementChild;
}
}
onBlurRow(event) {
const blurred = event.target;
const focused = event.relatedTarget;
if (!focused ||
DeclareNamespaceDialog.lookupRow(focused) !==
DeclareNamespaceDialog.lookupRow(blurred)) {
let removeButton = DeclareNamespaceDialog.getRowButton(blurred);
if (removeButton !== null) {
removeButton.style.visibility = "hidden";
}
}
}
onKeydownCell(event) {
if (event.key === "Enter") {
XUI.Util.consumeEvent(event);
this.okAction();
}
}
addAction() {
let row = this.newRow( true, "", "");
this._rows.appendChild(row);
this._table.scrollIntoView({ block: "end", inline: "start" });
row.firstElementChild.focus();
}
removeAction(event) {
const button = event.target;
let row = DeclareNamespaceDialog.lookupRow(button);
if (row !== null) {
row.parentNode.removeChild(row);
this._addButton.focus();
}
}
dialogClosed(result) {
this._resolve(result);
}
cancelAction() {
this.close(null);
}
okAction() {
let result = null;
if (this._editable) {
let prefixToNS = {};
let nsPrefixes = [];
for (let row of this._rows.children) {
let namespaceCell = row.firstElementChild;
let prefixCell = namespaceCell.nextElementSibling;
let namespace = namespaceCell.textContent.trim();
let prefix = prefixCell.textContent.trim();
if (namespace.length === 0 && prefix.length === 0) {
continue;
}
let error = null;
let badCell = null;
if (namespace === Name.NS_NONE ||
namespace === Name.NS_XML) {
error =
`Namespace "${namespace}" may not be bound to a prefix.`;
badCell = namespaceCell;
}
if (error === null && prefix.length > 0 &&
(!Name.isNCName(prefix) ||
prefix.toLowerCase().startsWith("xml"))) {
error = `"${prefix}", invalid namespace prefix.`;
badCell = prefixCell;
}
if (error === null) {
let ns = prefixToNS[prefix];
if (ns) {
error =
`Prefix "${prefix}" already used for namespace "${ns}".`;
badCell = prefixCell;
}
}
if (error !== null) {
badCell.focus();
window.getSelection().selectAllChildren(badCell);
XUI.Alert.showError(error,  this.form);
return;
}
prefixToNS[prefix] = namespace;
let nsPrefix = "xmlns";
if (prefix.length > 0) {
nsPrefix += ":" + prefix;
}
nsPrefix += "=\"";
nsPrefix += DOMUtil.escapeXML(namespace);
nsPrefix += "\"";
nsPrefixes.push(nsPrefix);
}
result = (nsPrefixes.length === 0)? "-" : nsPrefixes.join("\n");
}
this.close(result);
}
}
DeclareNamespaceDialog.TEMPLATE = document.createElement("template");
DeclareNamespaceDialog.TEMPLATE.innerHTML = `
<div class="xxe-declns-content">
  <div class="xxe-declns-scroller">
    <table class="xxe-declns-table">
      <colgroup class="xxe-declns-col1"></colgroup>
      <colgroup class="xxe-declns-col2"></colgroup>
      <colgroup class="xxe-declns-col3"></colgroup>
      <thead>
        <th>Namespace</th>
        <th>Prefix</th>
        <th class="xxe-declns-tools"><button type="button"
          class="xui-control xui-small-icon xxe-declns-add"></button></th>
      </thead>
      <tbody></tbody>
    </table>
  </div>
  <div class="xxe-declns-hint">An empty prefix may be used to specify
  the <i>default namespace</i>.</div>
</div>
`;
DeclareNamespaceDialog.ROW_TEMPLATE = document.createElement("template");
DeclareNamespaceDialog.ROW_TEMPLATE.innerHTML = `
<tr>
  <td contenteditable="true" spellcheck="false"></td>
  <td contenteditable="true" spellcheck="false"></td>
  <td class="xxe-declns-tools"><button type="button" style="visibility:hidden"
    class="xui-control xui-small-icon xxe-declns-remove"></button></td>
</tr>
`;
class DeclareNamespaceCmd extends InteractiveRemoteCommand {
constructor() {
super("declareNamespace");
}
resumeExecution(mode, docView, params,
stoppedCommandInfo, resolve, reject) {
let editable = true;
let nsPrefixes = splitTrimString(stoppedCommandInfo, '\n');
if (nsPrefixes.length > 0 && nsPrefixes[0] === "read-only") {
editable = false;
nsPrefixes = nsPrefixes.splice(1);
}
this.editNSPrefixes(nsPrefixes, editable, mode, docView, resolve);
}
editNSPrefixes(nsPrefixes, editable, mode, docView, resolve) {
DeclareNamespaceDialog.showDialog(nsPrefixes, editable, docView)
.then((editedNSPrefixes) => {
if (editedNSPrefixes === null) {
resolve(CommandResult.CANCELED);
} else {
let result = this.execute(mode, docView, editedNSPrefixes);
resolve(result);
}
});
}
}
ALL_LOCAL_COMMANDS.declareNamespace = new DeclareNamespaceCmd();
class SetStyleSheetDialog extends XUI.Dialog {
static showDialog(styleSheetTitles, reference=null) {
return new Promise((resolve, reject) => {
let dialog = new SetStyleSheetDialog(styleSheetTitles, resolve);
dialog.open("center", reference);
});
}
constructor(styleSheetTitles, resolve) {
super({ movable: true, title: "Change Stylesheet", closeable: true,
template: SetStyleSheetDialog.TEMPLATE,
buttons: [ { label: "Cancel", action: "cancelAction" },
{ label: "OK", action: "okAction",
default: true } ]});
this._resolve = resolve;
this._select = this.contentPane.querySelector(".xxe-itemsel-list");
let optionCount = 0;
for (let title of styleSheetTitles) {
let option = document.createElement("option");
option.setAttribute("class", "xxe-itemsel-item");
option.value = String(optionCount++);
if (title.startsWith("\u2022")) {
option.selected = "selected";
title = title.substring(1);
}
option.textContent = title;
this._select.appendChild(option);
}
this._select.addEventListener("dblclick", (e) => {
XUI.Util.consumeEvent(e);
this.okAction();
});
this._select.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
XUI.Util.consumeEvent(e);
this.okAction();
}
});
}
dialogClosed(result) {
if (result === null) {
result = -1;
}
this._resolve(result);
}
cancelAction() {
this.close(-1);
}
okAction() {
let selected = this._select.selectedIndex;
if (selected < 0) {
return;
}
this.close(selected);
}
}
SetStyleSheetDialog.TEMPLATE = document.createElement("template");
SetStyleSheetDialog.TEMPLATE.innerHTML = `
<div class="xxe-itemsel-pane">
  <div class="xxe-itemsel-icon xui-styles-32"></div>
  <div class="xxe-itemsel-selector">
    <select size="10" class="xxe-itemsel-list">
    </select>
  </div>
</div>
`;
class SetStyleSheetCmd extends InteractiveRemoteCommand {
constructor() {
super("setStyleSheet");
}
resumeExecution(mode, docView, params,
stoppedCommandInfo, resolve, reject) {
const styleSheetTitles = splitTrimString(stoppedCommandInfo, '\n');
this.selectStyleSheet(styleSheetTitles, mode, docView, resolve);
}
selectStyleSheet(styleSheetTitles, mode, docView, resolve) {
SetStyleSheetDialog.showDialog(styleSheetTitles, docView)
.then((selected) => {
if (selected < 0) {
resolve(CommandResult.CANCELED);
} else {
let result = this.execute(mode, docView, String(selected));
resolve(result);
}
});
}
executionDone(mode, docView, params, resultValue) {
docView.makeSelectionVisible();
}
}
ALL_LOCAL_COMMANDS.setStyleSheet = new SetStyleSheetCmd();
export class OnMouseEventCmd extends Command {
constructor(alwaysConsumeEvent) {
super();
this._alwaysConsumeEvent = alwaysConsumeEvent;
}
execute(mode, docView, params, event) {
let canExecute = this.canExecuteImpl(docView, params, event);
if (mode === EXECUTE_TEST) {
return Promise.resolve(canExecute);
} else {
if (!canExecute) {
Command.consumeEvent(event);
return Promise.resolve(null);
}
}
if (this._alwaysConsumeEvent) {
Command.consumeEvent(event);
}
return this.executeImpl(docView, params, event)
.catch((error) => {
console.error(`Command "${this.commandName}" has failed: \
${error}`);
return null;
});
}
executeImpl(docView, params, event) {
if (!this._alwaysConsumeEvent) {
Command.consumeEvent(event);
}
console.error(`OnMouseEventCmd.executeImpl: NOT IMPLEMENTED: \
command "${this.commandName}" \
(params="${params}", event=${event})`);
return Promise.resolve(CommandResult.FAILED);
}
canExecuteImpl(docView, params, event) {
return (event !== null && (event instanceof MouseEvent));
}
}
export class ContextualMenuCmd extends OnMouseEventCmd {
constructor() {
super( false);
}
executeImpl(docView, params, event) {
if (docView.selectionContains(event)) {
Command.consumeEvent(event);
let targetElem = event.target;
const clicked = NodeView.lookupView(targetElem);
if (clicked === null ||
!Object.is(clicked, docView.selected) ||
docView.selected2 !== null) {
targetElem = null;
}
this.showContextualMenu(docView, targetElem,
event.clientX, event.clientY);
return Promise.resolve(CommandResult.DONE);
}
let [text, offset] = docView.pickTextualView(event);
if (text !== null) {
if (docView.dot !== text ||
docView.hasTextSelection() ||
docView.hasNodeSelection()) {
Command.consumeEvent(event);
let marks = {
mark: "", selected2: "", selected: "",
dot: NodeView.getUID(text), dotOffset: offset
};
return docView.sendSetMarks(marks)
.then((dotMoved) => {
return dotMoved? CommandResult.DONE :
CommandResult.FAILED;
});
} else {
const ces = docView.contenteditableState;
if (ces !== null) {
ces.moveDotAt(docView, event);
}
return Promise.resolve(CommandResult.DONE);
}
} else {
Command.consumeEvent(event);
const clicked = NodeView.lookupView(event.target);
if (clicked !== null) {
let marks = { mark: "", selected2: "",
selected: NodeView.getUID(clicked) };
docView.ensureDotIsInside(clicked, marks);
return docView.sendSetMarks(marks)
.then((nodeSelected) => {
if (nodeSelected) {
this.showContextualMenu(docView, event.target,
event.clientX, event.clientY);
return CommandResult.DONE;
} else {
return CommandResult.FAILED;
}
});
} else {
return Promise.resolve(CommandResult.FAILED);
}
}
}
showContextualMenu(docView, targetElem, clientX, clientY) {
let extraMenuItems = null;
let customControl = null;
let ancestor = targetElem;
while (ancestor !== null) {
if (ancestor.localName.startsWith("xxe-")) {
const menuItems = ancestor.contextualMenuItems;
if (Array.isArray(menuItems)) {
customControl = ancestor;
let done = new CommandResult(COMMAND_RESULT_DONE,
JSON.stringify(menuItems));
extraMenuItems = Promise.resolve(done);
}
break;
}
ancestor = ancestor.parentElement;
}
if (extraMenuItems === null) {
extraMenuItems = docView.executeCommand(EXECUTE_HELPER,
"contextualMenuItems", null);
}
extraMenuItems.then((result) => {
let menuItems = ContextualMenuCmd.MENU_ITEMS;
if (CommandResult.isDone(result)) {
let entries = null;
try {
entries = JSON.parse(result.value);
if (!Array.isArray(entries)) {
entries = null;
}
} catch {}
if (entries !== null) {
menuItems = [...menuItems];
menuItems[0].separator = true;
for (let i = entries.length-1; i >= 0; --i) {
const entry = entries[i];
if (entry === null ||
!entry.label || !entry.cmdName) {
menuItems[0].separator = true;
continue;
}
let menuItem = {};
menuItem.text = entry.label;
if (entry.iconName) {
menuItem.icon = entry.iconName;
} else if (entry.iconData) {
menuItem.icon = "url(" + entry.iconData + ")";
}
const cmdName = entry.cmdName;
menuItem.name = cmdName;
if ("cmdParam" in entry) {
menuItem.name += "\n" + entry.cmdParam;
}
menuItem.enabled = ("enabled" in entry);
if (cmdName.endsWith("()") &&
customControl !== null) {
const action = customControl[
cmdName.substring(0, cmdName.length-2)];
if (action) {
menuItem.customControlAction = action;
}
}
menuItems.unshift(menuItem);
}
}
}
this.openContextualMenu(docView, menuItems, clientX, clientY);
});
}
openContextualMenu(docView, menuItems, clientX, clientY) {
ContextualMenuCmd.addKeyboardShortcuts(docView);
const menu = XUI.Menu.create(menuItems);
menu.addEventListener("menuitemselected", (event) => {
this.menuItemSelected(docView, event);
});
this.enableMenuItems(docView, menu);
menu.open([clientX, clientY]);
}
static addKeyboardShortcuts(docView) {
if (!ContextualMenuCmd.MENU_ITEMS[0].detail) {
for (let item of ContextualMenuCmd.MENU_ITEMS) {
let [cmdName, cmdParam] = ContextualMenuCmd.ACTION[item.name];
let binding = docView.getBindingForCommand(cmdName, cmdParam);
if (binding !== null) {
let keyboardShortcut = binding.getUserInputLabel();
if ("text" in item) {
item.detail = keyboardShortcut;
} else if ("tooltip" in item) {
let tooltipLines = null;
let tooltip = item.tooltip;
let pos = tooltip.indexOf('\n');
if (pos > 0 && pos+1 < tooltip.length) {
tooltipLines = tooltip.substring(pos);
tooltip = tooltip.substring(0, pos);
}
tooltip = tooltip +
"\u00A0\u00A0\u00A0\u00A0[" + keyboardShortcut + "]";
if (tooltipLines !== null) {
tooltip += tooltipLines;
}
item.tooltip = tooltip;
}
}
}
}
}
menuItemSelected(docView, event) {
const name = event.detail.menuItem.name;
if (name.endsWith("()")) {
const customControlAction =
event.detail.menuItem.getOption("customControlAction");
if (customControlAction) {
try {
customControlAction(event);
} catch (error) {
console.error(`Could not execute pseudo-command "${name}": \
${error}`);
}
}
return;
}
const action = ContextualMenuCmd.ACTION[name];
let cmdName, cmdParam;
if (!action) {
let pos = name.indexOf('\n');
if (pos < 0) {
cmdName = name;
cmdParam = null;
} else {
cmdName = name.substring(0, pos);
cmdParam = name.substring(pos+1);
}
} else {
cmdName = action[0];
cmdParam = action[1];
}
docView.executeCommand(EXECUTE_NORMAL, cmdName, cmdParam, event);
}
enableMenuItems(docView, menu) {
if (menu !== null) {
for (let item of menu.getAllItems()) {
const action = ContextualMenuCmd.ACTION[item.name];
if (!action) {
continue;
}
let enabled = false;
let tooltip = null;
let state = docView.getCommandState(action[0], action[1]);
if (state !== null) {
enabled = state[0];
tooltip = state[1];
}
item.setOption("enabled", enabled);
if (item.name === "repeat") {
item.setOption("tooltip", tooltip);
}
}
}
}
}
ContextualMenuCmd.MENU_ITEMS = [
{ name: "repeat", icon: "repeat", text: "Repeat", enabled: false },
{ name: "cut", icon: "cut", tooltip: "Cut", enabled: false,
separator: true },
{ name: "copy", icon: "copy",
tooltip: "Copy\n\n\u2022 Shift-click to copy as text.",
enabled: false, separator: true },
{ name: "pasteBefore", icon: "pasteBefore",
tooltip: "Paste Before", enabled: false,
separator: true },
{ name: "paste", icon: "paste", tooltip: "Paste", enabled: false },
{ name: "pasteAfter", icon: "pasteAfter",
tooltip: "Paste After", enabled: false },
{ name: "delete", icon: "delete", tooltip: "Delete", enabled: false,
separator: true },
{ name: "replace", icon: "replace", text: "Replace...",
enabled: false, separator: true },
{ name: "insertBefore", icon: "insertBefore",
text: "Insert Before...", enabled: false },
{ name: "insert", icon: "insert", text: "Insert Into...",
enabled: false },
{ name: "insertAfter", icon: "insertAfter",
text: "Insert After...", enabled: false },
{ name: "convert", icon: "convert", text: "Convert...",
enabled: false },
{ name: "wrap", icon: "wrap", text: "Wrap...", enabled: false },
{ name: "editAttributes", icon: "editAttributes",
text: "Edit Attributes...", enabled: false,
separator: true }
];
ContextualMenuCmd.ACTION = {
"repeat": [ "repeat", null ],
"cut": [ "cut", "[implicitElement]" ],
"copy": [ "copy", "[implicitElement]" ],
"pasteBefore": [ "paste", "before[implicitElement]" ],
"paste": [ "paste", "toOrInto" ],
"pasteAfter": [ "paste", "after[implicitElement]" ],
"delete": [ "delete", "[implicitElement]" ],
"replace": [ "replace", "[implicitElement]" ],
"insertBefore": [ "insert", "before[implicitElement]" ],
"insert": [ "insert", "into" ],
"insertAfter": [ "insert", "after[implicitElement]" ],
"convert": [ "convert", "[implicitElement]" ],
"wrap": [ "wrap", "[implicitElement]" ],
"editAttributes": [ "editAttributes", "[implicitElement]" ]
};
ALL_LOCAL_COMMANDS.contextualMenu = new ContextualMenuCmd();
class EditAttributesDialog extends XUI.Dialog {
constructor(stoppedCommandInfo, docView) {
super({ title: "Edit Attributes",
movable: true, resizable: true, closeable: true,
template: EditAttributesDialog.TEMPLATE,
buttons: [ { label: "Close", action: "closeAction" } ] });
this._attrInfos = {};
this._docView = docView;
let content = this.contentPane.firstElementChild;
let scroller = content.firstElementChild;
let form = scroller.nextElementSibling;
this._infos = content.lastElementChild;
this._table = scroller.firstElementChild;
this._rows = this._table.lastElementChild;
this._rows.onclick = this.rowClicked.bind(this);
let label = form.firstElementChild;
this._nameText = label.nextElementSibling;
this._nameText.onkeydown = this.nameAccepted.bind(this);
this._nameText.onchange = this.nameChanged.bind(this);
this._clearButton = this._nameText.nextElementSibling;
this._clearButton.setAttribute("title", "Clear form");
this._clearButton.textContent = XUI.StockIcon["backspace"];
this._clearButton.onclick = this.clearAction.bind(this);
label = this._clearButton.nextElementSibling;
this._valueText = label.nextElementSibling;
this._valueText.onkeydown = this.valueAccepted.bind(this);
this._removeButton = this._valueText.nextElementSibling;
this._removeButton.setAttribute("title", "Remove attribute");
this._removeButton.textContent = XUI.StockIcon["trash"];
this._removeButton.onclick = this.removeAction.bind(this);
this._setButton = this._removeButton.nextElementSibling;
this._setButton.setAttribute("title", "Add or replace attribute");
this._setButton.textContent = XUI.StockIcon["ok"];
this._setButton.onclick = this.setAction.bind(this);
this.setContent(stoppedCommandInfo);
}
rowClicked(event) {
if (event.button === 0 && event.detail === 1) {
XUI.Util.consumeEvent(event);
let elem = event.target;
while (elem !== null) {
if ("xxeAttributeInfo" in elem) {
this.setForm(elem.xxeAttributeInfo);
break;
}
if (elem.localName === "tbody") {
break;
}
elem = elem.parentElement;
}
}
}
nameAccepted(event) {
if (event.key === "Enter") {
XUI.Util.consumeEvent(event);
this._valueText.focus();
this._valueText.select();
}
}
nameChanged(event) {
XUI.Util.consumeEvent(event);
let qName = event.target.value.trim();
if (qName.length === 0) {
return;
}
if ((qName in this._attrInfos) &&
this._attrInfos[qName].name === qName) {
this.setForm(this._attrInfos[qName]);
} else {
this.setForm({ label: qName, name: null, value: null,
type: "string", valueListId: null, required: false,
fixedValue: null, defaultValue: null });
}
}
valueAccepted(event) {
if (event.key === "Enter") {
XUI.Util.consumeEvent(event);
this.setAction();
}
}
clearAction() {
this.setForm(null);
}
setForm(attrInfo) {
this._valueText.removeAttribute("placeholder");
this._valueText.removeAttribute("list");
if (attrInfo === null) {
this._nameText.value = "";
this._valueText.value = "";
this._infos.textContent = "";
EditAttributesDialog.enableButton(this._removeButton, false);
EditAttributesDialog.enableButton(this._setButton, false);
} else {
this._nameText.value = attrInfo.label;
let value;
if (attrInfo.value === null) {
value = "";
} else if (attrInfo.value.length === 0) {
value = "";
this._valueText.setAttribute("placeholder", "(empty string)");
} else {
value = attrInfo.value;
}
this._valueText.value = value;
if (attrInfo.valueListId !== null) {
this._valueText.setAttribute("list", attrInfo.valueListId);
}
let info = attrInfo.type;
if (attrInfo.required) {
info += " | REQUIRED";
}
if (attrInfo.fixedValue !== null) {
if (info.indexOf("|") < 0) {
info += " |";
}
info += " FIXED \"" + attrInfo.fixedValue + "\"";
} else if (attrInfo.defaultValue !== null) {
if (info.indexOf("|") < 0) {
info += " |";
}
info += " \"" + attrInfo.defaultValue + "\"";
}
this._infos.textContent = info;
EditAttributesDialog.enableButton(this._removeButton, true);
EditAttributesDialog.enableButton(this._setButton, true);
}
}
static enableButton(button, enabled) {
if (enabled) {
button.removeAttribute("disabled");
button.classList.remove("xui-control-disabled");
} else {
button.setAttribute("disabled", "disabled");
button.classList.add("xui-control-disabled");
}
}
removeAction() {
let name = this.getName();
if (name === null) {
return;
}
if (this.getAttributeByName(name) === null) {
this.clearAction();
return;
}
this._docView.sendExecuteCommand(EXECUTE_HELPER, "removeAttribute",
`[implicitElement] ${name}`)
.then((result) => {
if (CommandResult.isDone(result)) {
this.updateContent();
} else {
let reason = CommandResult.formatCommandResult(
`removeAttribute [implicitElement] ${name}`, result);
XUI.Alert.showError(
`Cannot remove attribute "${name}":\n${reason}`,
this._docView);
this.clearAction();
}
});
}
getName() {
let qName = this._nameText.value.trim();
if (qName.length === 0) {
XUI.Util.badTextField(this._nameText);
return null;
}
let name = Name.parse(qName,  true,
this._docView.namespacePrefixes);
if (name === null) {
XUI.Util.badTextField(this._nameText);
return null;
}
return name;
}
getAttributeByName(attrName) {
let attrInfo = this.getAttributeInfoByName(attrName);
return (attrInfo !== null && attrInfo.value !== null)? attrInfo : null;
}
getAttributeInfoByName(attrName) {
if (attrName !== null) {
attrName = attrName.toString();
for (let attrInfo of Object.values(this._attrInfos)) {
if (attrInfo.name === attrName) {
return attrInfo;
}
}
}
return null;
}
getAttributeByValue(attrValue) {
if (attrValue !== null) {
for (let attrInfo of Object.values(this._attrInfos)) {
if (attrInfo.value === attrValue) {
return attrInfo;
}
}
}
return null;
}
setAction() {
let name = this.getName();
if (name === null) {
return;
}
let value = this._valueText.value;
let attrInfo = this.getAttributeByName(name);
if (attrInfo !== null && attrInfo.value === value) {
this.clearAction();
return;
}
if (value.indexOf("\"") >= 0) {
value = value.replace(/"/g, "\\\"");
        }
        value = "\"" + value + "\"";
this._docView.sendExecuteCommand(EXECUTE_HELPER, "putAttribute",
`[implicitElement] ${name} ${value}`)
.then((result) => {
if (CommandResult.isDone(result)) {
this.updateContent();
} else {
let reason = CommandResult.formatCommandResult(
`putAttribute [implicitElement] ${name} ${value}`,
result);
XUI.Alert.showError(
`Cannot add or replace attribute "${name}":\n${reason}`,
this._docView);
this.clearAction();
}
});
}
updateContent() {
this._docView.sendExecuteCommand(EXECUTE_HELPER,
"editAttributes", "[implicitElement]")
.then((result) => {
if (result === null ||
result.status !== COMMAND_RESULT_STOPPED) {
let reason = CommandResult.formatCommandResult(
"editAttributes [implicitElement]", result);
XUI.Alert.showError(
`INTERNAL ERROR: cannot refresh the attributes table:
${reason}`,
this._docView);
} else {
this.setContent(result.value);
setTimeout(() => { this._nameText.focus(); }, 0);
}
});
}
setContent(stoppedCommandInfo) {
XUI.Util.removeAllChildren(this._rows);
this._attrInfos = {};
let nameList = [];
let valueListsMap = {};
let rowCount = 0;
let lines = splitTrimString(stoppedCommandInfo, '\n');
for (let line of lines) {
let fields = line.split(';');
if (fields.length !== 7 || fields[0].length === 0) {
continue;
}
let attrInfo = {};
attrInfo.label = EditAttributesDialog.unescapeField(fields[0]);
attrInfo.name = (fields[1].length === 0)? null :
EditAttributesDialog.unescapeField(fields[1]);
if (attrInfo.name !== null) {
nameList.push(attrInfo.label);
}
EditAttributesDialog.parseAttrType(fields[2], valueListsMap,
attrInfo);
attrInfo.required = (fields[3] === "true")? true : false;
attrInfo.fixedValue = (fields[4].length === 0)? null :
EditAttributesDialog.unescapeField(fields[4]);
attrInfo.defaultValue = (fields[5].length === 0)? null :
EditAttributesDialog.unescapeField(fields[5]);
attrInfo.value = (fields[6].length === 0)? null :
EditAttributesDialog.unescapeField(fields[6]);
this._attrInfos[attrInfo.label] = attrInfo;
if (attrInfo.name !== null && attrInfo.value !== null) {
let row = document.createElement("tr");
row.setAttribute("class",
"xxe-edattr-row" + String(rowCount % 2));
++rowCount;
row.xxeAttributeInfo = attrInfo;
let cell = document.createElement("td");
if (attrInfo.required) {
cell.setAttribute("class", "xxe-edattr-required");
}
cell.textContent = attrInfo.label;
row.appendChild(cell);
cell = document.createElement("td");
let value;
if (attrInfo.value === null) {
value = "\u00A0";
} else if (attrInfo.value.length === 0) {
value = "(empty string)";
} else {
value = attrInfo.value;
}
cell.textContent = value;
row.appendChild(cell);
this._rows.appendChild(row);
}
}
this._table.scrollIntoView({ block: "start", inline: "start" });
if (nameList.length > 0) {
let nameListId = XUI.Util.uid();
XUI.Util.appendDatalist(nameListId, nameList, this.contentPane);
this._nameText.setAttribute("list", nameListId);
}
for (let listSpec in valueListsMap) {
let entry = valueListsMap[listSpec];
XUI.Util.appendDatalist(entry[0], entry[1], this.contentPane);
}
this.setForm(null);
}
static unescapeField(field) {
if (field === "\uEDCB") {
return "";
} else {
return field.replace(/\\u([0-9A-F]{4})/gi,
(match, p1) => {
return String.fromCharCode(parseInt(p1, 16));
});
}
}
static parseAttrType(field, valueListsMap, attrInfo) {
attrInfo.type = field;
attrInfo.valueListId = null;
let listSpec = null;
if (field.endsWith(']')) {
let pos = field.indexOf('[');
if (pos <= 0) {
attrInfo.type = "";
} else {
attrInfo.type = field.substring(0, pos);
listSpec = field.substring(pos+1, field.length-1);
}
} else if (field.endsWith(')')) {
let pos = field.indexOf('(');
if (pos <= 0) {
attrInfo.type = "";
} else {
attrInfo.type = field.substring(0, pos);
listSpec = field.substring(pos+1, field.length-1);
}
}
if (listSpec !== null) {
let entry = null;
if (listSpec in valueListsMap) {
entry = valueListsMap[listSpec];
} else {
let list = EditAttributesDialog.parseValues(listSpec);
if (list.length > 0) {
entry = [ XUI.Util.uid(), list ];
valueListsMap[listSpec] = entry;
}
}
if (entry !== null) {
attrInfo.valueListId = entry[0];
}
}
if (attrInfo.type.length === 0) {
attrInfo.type = "string";
} else {
attrInfo.type = EditAttributesDialog.unescapeField(attrInfo.type);
}
}
static parseValues(list) {
let parsed = [];
let items = list.split(',');
for (let item of items) {
parsed.push(EditAttributesDialog.unescapeField(item));
}
return parsed;
}
closeAction() {
this.close(null);
}
selectAttribute(attrName) {
let attrInfo = this.getAttributeInfoByName(attrName);
if (attrInfo === null) {
return false;
}
this.setForm(attrInfo);
this._valueText.focus();
this._valueText.select();
return true;
}
}
EditAttributesDialog.TEMPLATE = document.createElement("template");
EditAttributesDialog.TEMPLATE.innerHTML = `
<div class="xxe-edattr-content">
  <div class="xxe-edattr-scroller">
    <table class="xxe-edattr-table">
      <colgroup class="xxe-edattr-col1"></colgroup>
      <colgroup class="xxe-edattr-col2"></colgroup>
      <thead><th>Name</th><th>Value</th></thead>
      <tbody></tbody>
    </table>
  </div>
  <div class="xxe-edattr-form">
    <span class="xxe-edattr-nlabel">Name:</span>
    <input type="text" class="xui-control xxe-edattr-name"
           spellcheck="false" autocomplete="off" />
    <button type="button"
            class="xui-control xui-small-icon xxe-edattr-clear"></button>
    <span class="xxe-edattr-vlabel">Value:</span>
    <input type="text" class="xui-control xxe-edattr-value"
           spellcheck="false" autocomplete="off" />
    <button type="button"
            class="xui-control xui-small-icon xxe-edattr-remove"></button>
    <button type="button"
            class="xui-control xui-small-icon xui-dlg-default-button
                   xxe-edattr-set"></button>
  </div>
  <div class="xxe-edattr-infos"></div>
</div>
`;
class EditAttributesCmd extends InteractiveRemoteCommand {
constructor() {
super("editAttributes");
}
resumeExecution(mode, docView, params,
stoppedCommandInfo, resolve, reject) {
let selectAttrName = null;
if (params !== null) {
params = params.trim();
if (params.startsWith("[implicitElement]")) {
params = params.substring(17).trim();
}
if (params.length > 0) {
selectAttrName = params;
}
}
this.showDialog(stoppedCommandInfo, selectAttrName, docView);
resolve(CommandResult.DONE);
}
showDialog(stoppedCommandInfo, selectAttrName, docView) {
let dialog = new EditAttributesDialog(stoppedCommandInfo, docView);
if (selectAttrName === null) {
let attrInfo = dialog.getAttributeByValue("???");
if (attrInfo !== null) {
selectAttrName = attrInfo.name;
}
}
if (selectAttrName !== null) {
setTimeout(() => {
dialog.selectAttribute(selectAttrName);
}, 250 );
}
dialog.open("center", docView);
}
}
ALL_LOCAL_COMMANDS.editAttributes = new EditAttributesCmd();
class ExtendSelectionAtCmd extends OnMouseEventCmd {
constructor() {
super( false);
}
executeImpl(docView, params, event) {
return this.changeSelection(docView, params, event)
.then((done) => {
return CommandResult.DONE;
});
}
changeSelection(docView, params, event) {
let done = Promise.resolve(true);
let selected = docView.selected;
if (selected !== null) {
let clicked = NodeView.lookupView(event.target);
if (clicked !== null) {
Command.consumeEvent(event);
done = ExtendSelectionAtCmd.extendNodeSelection(
docView, selected, clicked);
}
} else {
if (docView.dot !== null) {
done = docView.executeCommand(EXECUTE_HELPER,
"selectAt", null, event);
} else {
done = docView.executeCommand(EXECUTE_HELPER,
"selectNodeAt", null, event);
}
}
return done;
}
static extendNodeSelection(docView, selected, clicked) {
if (clicked.parentNode !== selected.parentNode) {
if (clicked.contains(selected)) {
return docView.selectNode(clicked)
.then((changed) => {
return true;
});
}
let sibling = null;
let n = selected;
while (n !== null) {
if (n.contains(clicked)) {
sibling = n;
break;
}
let n2 = NodeView.getNextViewSibling(n);
if (n2 === null) {
if (DOMUtil.traverseAfter(n, (traversed, enterElement) => {
if (enterElement && traversed === clicked) {
return true;
} else {
return null;
}
}) === true) {
sibling = n;
break;
}
}
n = n2;
}
if (sibling === null) {
n = NodeView.getPreviousViewSibling(selected);
while (n !== null) {
if (n.contains(clicked)) {
sibling = n;
break;
}
let n2 = NodeView.getPreviousViewSibling(n);
if (n2 === null) {
sibling = n;
break;
}
n = n2;
}
}
clicked = sibling;
}
if (clicked !== null && clicked !== selected &&
clicked.parentNode === selected.parentNode) {
return docView.selectNodeRange(selected, clicked)
.then((changed) => {
return true;
});
}
return Promise.resolve(false);
}
}
ALL_LOCAL_COMMANDS.extendSelectionAt = new ExtendSelectionAtCmd();
class PastePrimarySelectionCmd extends OnMouseEventCmd {
constructor() {
super( true);
}
executeImpl(docView, params, event) {
const [text, offset] = docView.pickTextualView(event);
if (text === null) {
return Promise.resolve(null);
}
let marks = { mark: "", selected2: "", selected: "",
dot: NodeView.getUID(text), dotOffset: offset };
return docView.sendSetMarks(marks)
.then((caretMoved) => {
if (!caretMoved ||
!docView.canEdit() || !docView.button2PastesText) {
return CommandResult.DONE;
}
return docView.executeCommand(EXECUTE_HELPER, "paste",
"into[systemSelection]");
})
.then((pasteResult) => {
return CommandResult.DONE;
});
}
canExecuteImpl(docView, params, event) {
if (!super.canExecuteImpl(docView, params, event)) {
return false;
}
let [text, offset] = docView.pickTextualView(event);
return (text !== null);
}
}
ALL_LOCAL_COMMANDS.pastePrimarySelection = new PastePrimarySelectionCmd();
class BoundingRect {
constructor(x=0, y=0, width=0, height=0) {
this.set(x, y, width, height);
}
set(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
setFrom(rect) {
this.x = rect.x;
this.y = rect.y;
this.width = rect.width;
this.height = rect.height;
}
unionWith(rect) {
if (rect.height <= 0) {
return;
}
if (this.width <= 0 || this.height <= 0) {
this.setFrom(rect);
return;
}
let x1 = Math.min(this.x, rect.x);
let x2 = Math.max(this.x + this.width, rect.x + rect.width);
let y1 = Math.min(this.y, rect.y);
let y2 = Math.max(this.y + this.height, rect.y + rect.height);
this.x = x1;
this.y = y1;
this.width = x2 - x1;
this.height = y2 - y1;
}
toString() {
return `BoundingRect(${this.x},${this.y}, \
${this.width}x${this.height})`;
}
}
class TextLineRect extends BoundingRect {
constructor(textualContent, rectIndex, rectCount, x, y, width, height) {
super(x, y, width, height);
this.textualContent = textualContent;
this.rectIndex = rectIndex;
this.rectCount = rectCount;
}
toString() {
let text = this.textualContent.textContent;
const textLength = text.length;
if (textLength > 40) {
text = text.substring(0, 19) + "\u2026" +
text.substring(textLength-20);
}
if (text.indexOf('\n') >= 0) {
text = text.replaceAll('\n', ' ');
}
return `TextLineRect("${text}", ${this.rectIndex}/${this.rectCount}, \
${this.x},${this.y}, ${this.width}x${this.height})`;
}
}
class TextLine {
static get(dot, dotOffset, whichLine, bounds=null, dotClientCoords=null) {
return (new TextLine()).getRects(dot, dotOffset, whichLine,
bounds, dotClientCoords);
}
constructor() {
this._textualContentHandler = this.textualContentHandler.bind(this);
}
getRects(dot, dotOffset, whichLine, bounds, dotClientCoords) {
this._textLineRects = [];
this._crossLine = false;
this._backwards = false;
if (bounds === null) {
bounds = new BoundingRect();
} else {
bounds.set(0, 0, 0, 0);
}
this._bounds = bounds;
let [rects, dotRectIndex] = TextLine.getDotRect(dot, dotOffset,
dotClientCoords);
if (rects === null) {
return null;
}
switch (whichLine) {
case -1:
{
this._crossLine = false;
let i;
for (i = dotRectIndex; i < rects.length; ++i) {
if (!this.processRect(rects[i])) {
break;
}
}
if (i >= rects.length) {
this._backwards = false;
DOMUtil.traverseAfter(dot, this._textualContentHandler);
}
this._crossLine = true;
for (i = dotRectIndex-1; i >= 0; --i) {
if (!this.processRect(rects[i])) {
break;
}
}
if (i < 0) {
this._backwards = true;
DOMUtil.traverseBefore(dot, this._textualContentHandler);
}
if (this._crossLine) {
this._textLineRects = [];
this._bounds.set(0, 0, 0, 0);
}
}
break;
case 1:
{
this._crossLine = false;
let i;
for (i = dotRectIndex; i >= 0; --i) {
if (!this.processRect(rects[i])) {
break;
}
}
if (i < 0) {
this._backwards = true;
DOMUtil.traverseBefore(dot, this._textualContentHandler);
}
this._crossLine = true;
for (i = dotRectIndex+1; i < rects.length; ++i) {
if (!this.processRect(rects[i])) {
break;
}
}
if (i >= rects.length) {
this._backwards = false;
DOMUtil.traverseAfter(dot, this._textualContentHandler);
}
if (this._crossLine) {
this._textLineRects = [];
this._bounds.set(0, 0, 0, 0);
}
}
break;
default:
{
this._crossLine = false;
let i;
for (i = dotRectIndex; i < rects.length; ++i) {
if (!this.processRect(rects[i])) {
break;
}
}
if (i >= rects.length) {
this._backwards = false;
DOMUtil.traverseAfter(dot, this._textualContentHandler);
}
for (i = dotRectIndex-1; i >= 0; --i) {
if (!this.processRect(rects[i])) {
break;
}
}
if (i < 0) {
this._backwards = true;
DOMUtil.traverseBefore(dot, this._textualContentHandler);
}
}
break;
}
return this._textLineRects;
}
static getDotRect(dot, dotOffset, dotClientCoords) {
let result = [null, -1];
const dotContainer = NodeView.getTextualContent(dot);
if (dotContainer === null) {
return result;
}
if (dotContainer.textContent.length === 0) {
let r = dotContainer.getBoundingClientRect();
if (r.height <= 0) {
return result;
}
result[0] = [ new TextLineRect(dotContainer,  0, 1,
r.x, r.y,  0, r.height) ];
result[1] = 0;
return result;
}
let dotCoords = TextLine.getDotCoords(dotContainer, dotOffset);
if (dotCoords === null) {
return result;
}
let [dotX, dotY] = dotCoords;
if (dotClientCoords !== null) {
dotClientCoords[0] = dotX;
dotClientCoords[1] = dotY;
}
let dotRectIndex = -1;
const rects = dotContainer.getClientRects();
const rectCount = rects.length;
let closest = Number.MAX_SAFE_INTEGER;
for (let i = rectCount-1; i >= 0; --i) {
let r = rects[i];
if (dotY >= r.top && dotY <= r.bottom) {
let distance = 0;
if (dotX < r.left) {
distance = r.left - dotX;
} else if (dotX > r.right) {
distance = dotX - r.right;
}
if (distance === 0) {
dotRectIndex = i;
break;
}
if (dotRectIndex < 0 || distance < closest) {
dotRectIndex = i;
closest = distance;
}
}
}
if (dotRectIndex >= 0) {
let textLineRects = [];
for (let i = 0; i < rectCount; ++i) {
let r = rects[i];
textLineRects.push(new TextLineRect(dotContainer, i, rectCount,
r.x, r.y, r.width, r.height));
}
result[0] = textLineRects;
result[1] = dotRectIndex;
}
return result;
}
static getDotCoords(dotContainer, dotOffset) {
let [node, offset] =
NodeView.textualContentToCharOffset(dotContainer, dotOffset);
if (node === null) {
return null;
}
let range = new Range();
let dotAtRight = false;
if (offset+1 <= node.length) {
range.setStart(node, offset);
range.setEnd(node, offset+1);
} else if (offset-1 >= 0) {
range.setStart(node, offset-1);
range.setEnd(node, offset);
dotAtRight = true;
} else {
range.setStart(node, offset);
range.collapse();
}
let dotRect = range.getBoundingClientRect();
if (dotRect.height <= 0 && node.length === 0) {
let textSibling = null;
let textSiblings = DOMUtil.getAllTextNodes(dotContainer);
let index = textSiblings.indexOf(node);
if (index >= 0) {
for (let k = index-1; k >= 0; --k) {
if (textSiblings[k].length > 0) {
textSibling = textSiblings[k];
dotAtRight = true;
break;
}
}
if (textSibling === null) {
for (let k = index+1; k < textSiblings.length; ++k) {
if (textSiblings[k].length > 0) {
textSibling = textSiblings[k];
dotAtRight = false;
break;
}
}
}
}
if (textSibling !== null) {
range = new Range();
range.selectNodeContents(textSibling);
let dotRects = range.getClientRects();
if (dotRects !== null && dotRects.length > 0) {
dotRect = dotRects[dotAtRight? dotRects.length-1 : 0];
}
}
}
if (dotRect.height <= 0) {
return null;
}
let dotX = dotRect.x;
if (dotAtRight) {
dotX += dotRect.width;
}
let dotY = dotRect.y + (dotRect.height / 2);
return [dotX, dotY];
}
processRect(rect) {
if (rect.height <= 0) {
return true;
}
if (this._bounds.height <= 0) {
this._bounds.setFrom(rect);
this._textLineRects.push(rect);
return true;
}
const minOverlap = this._bounds.height / 10;
let bottom = this._bounds.y+this._bounds.height;
let intersects = (rect.y >= this._bounds.y &&
(bottom - rect.y) >= minOverlap);
if (!intersects) {
bottom = rect.y+rect.height;
intersects = (this._bounds.y >= rect.y &&
(bottom - this._bounds.y) >= minOverlap);
if (!intersects) {
bottom = rect.y+rect.height;
intersects = ((bottom - this._bounds.y) >= minOverlap &&
bottom <= this._bounds.y+this._bounds.height);
if (!intersects) {
bottom = this._bounds.y+this._bounds.height;
intersects = ((bottom - rect.y) >= minOverlap &&
bottom <= rect.y+rect.height);
}
}
}
if (intersects) {
this._bounds.unionWith(rect);
this._textLineRects.push(rect);
return true;
}
if (!this._crossLine) {
return false;
}
this._bounds.setFrom(rect);
this._textLineRects = [ rect ];
this._crossLine = false;
return true;
}
textualContentHandler(traversed, enterElement) {
if (enterElement &&
NodeView.isTextualContent(traversed)) {
const rects = traversed.getClientRects();
const rectCount = rects.length;
if (this._backwards) {
for (let i = rectCount-1; i >= 0; --i) {
let r = rects[i];
if (!this.processRect(
new TextLineRect(traversed, i, rectCount,
r.x, r.y, r.width, r.height))) {
return traversed;
}
}
} else {
for (let i = 0; i < rectCount; ++i) {
let r = rects[i];
if (!this.processRect(
new TextLineRect(traversed, i, rectCount,
r.x, r.y, r.width, r.height))) {
return traversed;
}
}
}
}
return null;
}
}
class MoveDotToCmd extends RemoteCommand {
constructor() {
super("moveDotTo");
}
execute(mode, docView, params, event=null) {
if (mode === EXECUTE_TEST) {
return super.execute(mode, docView, params, event);
}
if ("previousLine" === params ||
"nextLine" === params ||
"lineBegin" === params ||
"lineEnd" === params) {
Command.consumeEvent(event);
return MoveDotToCmd.moveDotToLine(docView, params);
} else {
return super.execute(mode, docView, params, event);
}
}
static moveDotToLine(docView, params) {
let nextMagicX = [-1];
let [dot, dotOffset] = MoveDotToCmd.dotToLine(docView, params,
nextMagicX);
if (dot === null) {
return Promise.resolve(CommandResult.FAILED);
}
let marks = { mark: "", markOffset: -1, selected: "", selected2: "" };
marks.dot = NodeView.getUID(dot);
marks.dotOffset = dotOffset;
return docView.sendSetMarks(marks)
.then((done) => {
if ("previousLine" === params || "nextLine" === params) {
docView.magicX = nextMagicX[0];
}
return done? CommandResult.DONE : CommandResult.FAILED;
});
}
static dotToLine(docView, where, nextMagicX=null) {
let result = [null, -1];
const currentDot = docView.dot;
if (currentDot === null) {
return result;
}
const currentDotOffset = docView.dotOffset;
let whichLine = 0;
if (where === "previousLine") {
whichLine = -1;
} else if (where === "nextLine") {
whichLine = 1;
}
let bounds = new BoundingRect();
let dotClientCoords = [-1, -1];
let rects = TextLine.get(currentDot, currentDotOffset,
whichLine, bounds, dotClientCoords);
if (rects === null) {
switch (where) {
case "previousLine":
case "lineBegin":
{
let nextDot =
DOMUtil.traverseBefore(currentDot,
MoveDotToCmd.textualViewFinder);
if (nextDot !== null) {
result[0] = nextDot;
result[1] =
NodeView.getTextualContent(nextDot).textContent.length;
}
}
break;
case "nextLine":
case "lineEnd":
{
let nextDot =
DOMUtil.traverseAfter(currentDot,
MoveDotToCmd.textualViewFinder);
if (nextDot !== null) {
result[0] = nextDot;
result[1] = 0;
}
}
break;
}
return result;
} else if (rects.length === 0) {
return result;
}
let textLineRect = null;
let caretX = -1;
switch (where) {
case "previousLine":
case "nextLine":
{
if (nextMagicX === null) {
nextMagicX = [-1];
} else {
nextMagicX[0] = -1;
}
let magicClientX = MoveDotToCmd.magicClientX(docView,
dotClientCoords[0],
nextMagicX);
let closest = Number.MAX_SAFE_INTEGER;
for (let r of rects) {
let distance = 0;
if (magicClientX < r.x) {
distance = r.x - magicClientX;
} else if (magicClientX > (r.x + r.width)) {
distance = magicClientX - (r.x + r.width);
}
if (distance === 0) {
textLineRect = r;
break;
}
if (textLineRect === null || distance < closest) {
textLineRect = r;
closest = distance;
}
}
let x = magicClientX - textLineRect.x;
if (magicClientX < textLineRect.x) {
caretX = textLineRect.x;
} else if (magicClientX > textLineRect.x + textLineRect.width) {
caretX = textLineRect.x + textLineRect.width;
} else {
caretX = magicClientX;
}
}
break;
case "lineBegin":
{
let leftmost = Number.MAX_SAFE_INTEGER;
for (let r of rects) {
if (r.x < leftmost) {
textLineRect = r;
caretX = r.x;
leftmost = r.x;
}
}
}
break;
case "lineEnd":
{
let rightmost = Number.MIN_SAFE_INTEGER;
for (let r of rects) {
if (r.x > rightmost) {
textLineRect = r;
caretX = r.x + r.width;
rightmost = r.x;
}
}
}
break;
}
if (textLineRect !== null) {
const textualContent = textLineRect.textualContent;
textualContent.scrollIntoViewIfNeeded( false);
const textualContentLength = textualContent.textContent.length;
if (textualContentLength === 0) {
result[0] = NodeView.contentToView(textualContent,
textualContent.id);
result[1] = 0;
} else {
if (textLineRect.rectIndex === 0 &&
caretX === textLineRect.x) {
result[0] = NodeView.contentToView(textualContent,
textualContent.id);
result[1] = 0;
} else if (textLineRect.rectIndex === textLineRect.rectCount-1
&& caretX === textLineRect.x + textLineRect.width) {
result[0] = NodeView.contentToView(textualContent,
textualContent.id);
result[1] = textualContentLength;
} else {
let caretY = textLineRect.y + (textLineRect.height / 2);
let pos = [null, -1];
if (DocumentView.caretPositionFromPoint(caretX, caretY,
pos)) {
if (NodeView.charToTextualContentOffset(pos)) {
result[0] = NodeView.contentToView(pos[0],
pos[0].id);
result[1] = pos[1];
} else {
console.warn(`MoveDotToCmd.dotToLine: \
NO TEXTUAL CONTENT AT CHAR OFFSET ${pos}`);
}
} else {
console.warn(`MoveDotToCmd.dotToLine: \
NO CARET POSITION AT ${caretX},${caretY}`);
}
}
}
}
return result;
}
static textualViewFinder(traversed, enterElement) {
if (enterElement &&
NodeView.isTextualContent(traversed)  &&
traversed.getBoundingClientRect().height > 0) {
return NodeView.contentToView(traversed, traversed.id);
} else {
return null;
}
}
static magicClientX(docView, dotClientX, nextMagicX) {
let magicClientX;
if (docView.magicX < 0) {
magicClientX = dotClientX;
} else {
let [x, y] = docView.viewToClient(docView.magicX, 0);
magicClientX = x;
}
let [x, y] = docView.clientToView(magicClientX, 0);
nextMagicX[0] = x;
return magicClientX;
}
}
ALL_LOCAL_COMMANDS.moveDotTo = new MoveDotToCmd();
class SelectAtCmd extends OnMouseEventCmd {
constructor() {
super( true);
this._extendX = this._extendY = -1;
this._selectionChanged = false;
this._pickedText = [null, -1];
this._marks = { reason: "dragging",
selected2: null, selected: null,
mark: null, markOffset: -1,
dot: null, dotOffset: -1 };
}
executeImpl(docView, params, event) {
return this.changeSelection(docView, params, event)
.then((done) => {
if ("extend" !== params) {
if (this._selectionChanged) {
this._selectionChanged = false;
docView.updatePrimarySelection();
} else {
docView.requestFocus();
}
}
return CommandResult.DONE;
});
}
changeSelection(docView, params, event) {
let done = Promise.resolve(true);
if (docView.dot === null) {
if ("end" === params) {
done = docView.executeCommand(EXECUTE_HELPER,
"selectNodeAt", null, event);
}
} else {
if ("begin" === params) {
this._extendX = this._extendY = -1;
this._selectionChanged = false;
docView.pickTextualView(event, this._pickedText);
if (this._pickedText[0] !== null) {
this._marks.selected2 = "";
this._marks.selected = "";
this._marks.mark = "";
this._marks.markOffset = -1;
this._marks.dot = NodeView.getUID(this._pickedText[0]);
this._marks.dotOffset = this._pickedText[1];
done = this.setMarks(docView, this._marks);
this._extendX = event.clientX;
this._extendY = event.clientY;
}
} else if ("extend" === params) {
if (this._extendX >= 0 &&
(this._extendX !== event.clientX ||
this._extendY !== event.clientY)) {
this._extendX = event.clientX;
this._extendY = event.clientY;
done = this.selectTo(docView, event);
}
} else if ("end" === params) {
if (this._extendX >= 0) {
this._extendX = this._extendY = -1;
} else {
done = docView.executeCommand(EXECUTE_HELPER,
"selectNodeAt", null, event);
}
} else {
this._extendX = this._extendY = -1;
this._selectionChanged = false;
done = this.selectTo(docView, event);
}
}
return done;
}
selectTo(docView, event) {
docView.pickTextualView(event, this._pickedText);
if (this._pickedText[0] === null) {
return Promise.resolve(false);
}
if (docView.mark === null) {
this._marks.selected2 = "";
this._marks.selected = "";
this._marks.mark = NodeView.getUID(docView.dot);
this._marks.markOffset = docView.dotOffset;
} else {
this._marks.selected2 = null;
this._marks.selected = null;
this._marks.mark = null;
this._marks.markOffset = -1;
}
this._marks.dot = NodeView.getUID(this._pickedText[0]);
this._marks.dotOffset = this._pickedText[1];
if (this._marks.mark === this._marks.dot &&
this._marks.markOffset === this._marks.dotOffset) {
this._marks.mark = "";
this._marks.markOffset = -1;
}
return this.setMarks(docView, this._marks);
}
setMarks(docView, marks) {
let changed = false;
if (marks.dot !== null) {
if (!((docView.dot === null && marks.dot === "") ||
(docView.dot !== null &&
marks.dotOffset === docView.dotOffset &&
marks.dot === NodeView.getUID(docView.dot)))) {
changed = true;
}
}
if (!changed && marks.mark !== null) {
if (!((docView.mark === null && marks.mark === "") ||
(docView.mark !== null &&
marks.markOffset === docView.markOffset &&
marks.mark === NodeView.getUID(docView.mark)))) {
changed = true;
}
}
if (!changed && marks.selected !== null) {
if (!((docView.selected === null && marks.selected === "") ||
(docView.selected !== null &&
marks.selected === NodeView.getUID(docView.selected)))) {
changed = true;
}
}
if (!changed && marks.selected2 !== null) {
if (!((docView.selected2 === null && marks.selected2 === "") ||
(docView.selected2 !== null &&
marks.selected2 === NodeView.getUID(docView.selected2)))) {
changed = true;
}
}
if (changed) {
this._selectionChanged = true;
return docView.sendSetMarks(marks);
} else {
return Promise.resolve(false);
}
}
}
ALL_LOCAL_COMMANDS.selectAt = new SelectAtCmd();
class SelectNodeAtCmd extends OnMouseEventCmd {
constructor() {
super( true);
this._lastTarget = null;
this._lastOffsetX = null;
this._lastOffsetY = null;
}
executeImpl(docView, params, event) {
return this.changeSelection(docView, params, event)
.then((done) => {
return CommandResult.DONE;
});
}
changeSelection(docView, params, event) {
const selected = docView.selected;
if (selected !== null &&
"orParent" === params &&
event.target === this._lastTarget &&
event.offsetX === this._lastOffsetX &&
event.offsetY === this._lastOffsetY) {
let parent = NodeView.getParentView(selected);
if (parent !== null) {
return docView.selectNode(parent);
}
}
let done = Promise.resolve(true);
this._lastTarget = null;
this._lastOffsetX = null;
this._lastOffsetY = null;
let clicked = NodeView.lookupView(event.target);
if (clicked !== null) {
let [text, offset] = docView.pickTextualView(event);
let marks = { mark: "", selected2: "",
selected: NodeView.getUID(clicked) };
if (text !== null &&
clicked.contains(text)) {
marks.dot = NodeView.getUID(text);
marks.dotOffset = offset;
} else {
docView.ensureDotIsInside(clicked, marks);
}
done = docView.sendSetMarks(marks);
if ("orParent" === params) {
this._lastTarget = event.target;
this._lastOffsetX = event.offsetX;
this._lastOffsetY = event.offsetY;
}
}
return done;
}
}
ALL_LOCAL_COMMANDS.selectNodeAt = new SelectNodeAtCmd();
class OpenExternalLinkCmd extends Command {
constructor() {
super();
}
execute(mode, docView, params, event=null) {
let href = null;
if (params !== null && params.match(/^http[s]?:\/\//)) {
href = params;
}
if (mode === EXECUTE_TEST) {
return Promise.resolve(href !== null);
}
Command.consumeEvent(event);
if (href === null) {
if (params !== null) {
XUI.Alert.showError(`Don't know how open URLs like
"${params}"`,
docView);
}
return Promise.resolve(CommandResult.FAILED);
}
docView.showStatus(`Opening "${href}"...`,  true);
const newWin = window.open(href, "_blank");
if (newWin === null) {
XUI.Alert.showError(`Blocked by the browser pop-up blocker?
If this is the case, please allow pop-ups for ${window.location.host}`,
docView);
return Promise.resolve(CommandResult.FAILED);
}
return Promise.resolve(CommandResult.DONE);
}
}
ALL_LOCAL_COMMANDS.openExternalLink = new OpenExternalLinkCmd();
class FollowLinkAtCmd extends OnMouseEventCmd {
constructor() {
super( true);
}
executeImpl(docView, params, event) {
return docView.executeCommand(EXECUTE_HELPER,
"selectAt", "begin", event)
.then((result) => {
if (CommandResult.isDone(result)) {
return docView.executeCommand(EXECUTE_HELPER,
"selectAt", "end", event);
} else {
return CommandResult.FAILED;
}
})
.then((result) => {
if (CommandResult.isDone(result)) {
return this.followLinkAt(docView);
} else {
return CommandResult.FAILED;
}
});
}
followLinkAt(docView) {
const configName = docView.xmlEditor.configurationName;
if (configName === null) {
return Promise.resolve(null);
} else {
return docView.executeCommand(EXECUTE_HELPER,
`{${configName}}followLinkAt`,
null);
}
}
}
ALL_LOCAL_COMMANDS.followLinkAt = new FollowLinkAtCmd();
class SelectTextCmd extends RemoteCommand {
constructor() {
super("selectText");
}
execute(mode, docView, params, event=null) {
if (mode === EXECUTE_TEST) {
return super.execute(mode, docView, params, event);
}
let executed;
if ("line" === params) {
Command.consumeEvent(event);
executed = SelectTextCmd.selectLine(docView);
} else {
executed = super.execute(mode, docView, params, event);
}
return executed
.then((result) => {
if (CommandResult.isDone(result)) {
docView.updatePrimarySelection();
}
return result;
});
}
static selectLine(docView) {
let [mark, markOffset] = MoveDotToCmd.dotToLine(docView, "lineBegin");
if (mark === null) {
return Promise.resolve(CommandResult.FAILED);
}
let [dot, dotOffset] = MoveDotToCmd.dotToLine(docView, "lineEnd");
if (dot === null) {
return Promise.resolve(CommandResult.FAILED);
}
let marks = { selected: "", selected2: "" };
marks.mark = NodeView.getUID(mark);
marks.markOffset = markOffset;
marks.dot = NodeView.getUID(dot);
marks.dotOffset = dotOffset;
if (marks.mark === marks.dot &&
marks.markOffset === marks.dotOffset) {
marks.mark = "";
marks.markOffset = -1;
}
return docView.sendSetMarks(marks)
.then((done) => {
return done? CommandResult.DONE : CommandResult.FAILED;
});
}
}
ALL_LOCAL_COMMANDS.selectText = new SelectTextCmd();
class SelectToCmd extends RemoteCommand {
constructor() {
super("selectTo");
}
execute(mode, docView, params, event=null) {
if (mode === EXECUTE_TEST) {
return super.execute(mode, docView, params, event);
}
let executed;
if ("previousLine" === params ||
"nextLine" === params ||
"lineBegin" === params ||
"lineEnd" === params) {
Command.consumeEvent(event);
executed = SelectToCmd.selectToLine(docView, params);
} else {
executed = super.execute(mode, docView, params, event);
}
return executed
.then((result) => {
if (CommandResult.isDone(result)) {
docView.updatePrimarySelection();
}
return result;
});
}
static selectToLine(docView, params) {
let nextMagicX = [-1];
let [dot, dotOffset] = MoveDotToCmd.dotToLine(docView, params,
nextMagicX);
if (dot === null) {
return Promise.resolve(CommandResult.FAILED);
}
let marks = {};
if (docView.mark === null) {
marks.selected2 = "";
marks.selected = "";
marks.mark = NodeView.getUID(docView.dot);
marks.markOffset = docView.dotOffset;
} else {
marks.selected2 = null;
marks.selected = null;
marks.mark = null;
marks.markOffset = -1;
}
marks.dot = NodeView.getUID(dot);
marks.dotOffset = dotOffset;
if (marks.mark === marks.dot &&
marks.markOffset === marks.dotOffset) {
marks.mark = "";
marks.markOffset = -1;
}
return docView.sendSetMarks(marks)
.then((done) => {
if ("previousLine" === params || "nextLine" === params) {
docView.magicX = nextMagicX[0];
}
return done? CommandResult.DONE : CommandResult.FAILED;
});
}
}
ALL_LOCAL_COMMANDS.selectTo = new SelectToCmd();
class ListRepeatableDialog extends XUI.Dialog {
static showDialog(commandLabels, reference=null) {
return new Promise((resolve, reject) => {
let dialog = new ListRepeatableDialog(commandLabels, resolve);
dialog.open("center", reference);
});
}
constructor(commandLabels, resolve) {
super({ movable: true, title: "Command History", closeable: true,
template: ListRepeatableDialog.TEMPLATE,
buttons: [ { label: "Cancel", action: "cancelAction" },
{ label: "OK", action: "okAction",
default: true } ]});
this._resolve = resolve;
this._select = this.contentPane.querySelector(".xxe-itemsel-list");
let optionCount = 0;
let selectFirst = true;
for (let label of commandLabels) {
let option = document.createElement("option");
option.setAttribute("class", "xxe-itemsel-item");
option.value = String(optionCount++);
if (label.startsWith("-")) {
option.disabled = "disabled";
label = label.substring(1);
} else {
if (selectFirst) {
option.selected = "selected";
selectFirst = false;
}
}
option.textContent = label;
this._select.appendChild(option);
}
this._select.addEventListener("dblclick", (e) => {
XUI.Util.consumeEvent(e);
this.okAction();
});
this._select.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
XUI.Util.consumeEvent(e);
this.okAction();
}
});
}
dialogClosed(result) {
if (result === null) {
result = -1;
}
this._resolve(result);
}
cancelAction() {
this.close(-1);
}
okAction() {
let selected = this._select.selectedIndex;
if (selected < 0) {
return;
}
this.close(selected);
}
}
ListRepeatableDialog.TEMPLATE = document.createElement("template");
ListRepeatableDialog.TEMPLATE.innerHTML = `
<div class="xxe-itemsel-pane">
  <div class="xxe-itemsel-icon xui-commandHistory-32"></div>
  <div class="xxe-itemsel-selector">
    <select size="10" class="xxe-itemsel-list">
    </select>
  </div>
</div>
`;
class ListRepeatableCmd extends InteractiveRemoteCommand {
constructor() {
super("listRepeatable");
}
resumeExecution(mode, docView, params,
stoppedCommandInfo, resolve, reject) {
const commandLabels = splitTrimString(stoppedCommandInfo, '\n');
this.selectCommand(commandLabels, mode, docView, resolve);
}
selectCommand(commandLabels, mode, docView, resolve) {
ListRepeatableDialog.showDialog(commandLabels, docView)
.then((selected) => {
if (selected < 0) {
resolve(CommandResult.CANCELED);
} else {
let result = this.execute(mode, docView, String(selected));
resolve(result);
}
});
}
}
ALL_LOCAL_COMMANDS.listRepeatable = new ListRepeatableCmd();
class CopyCmd extends RemoteCommand {
constructor() {
super("copy");
}
execute(mode, docView, params, event=null) {
if (mode === EXECUTE_TEST) {
return super.execute(mode, docView, params, event);
}
let cmdName = this._commandName;
let cmdParams = params;
if (event !== null) {
Command.consumeEvent(event);
if (((event instanceof MouseEvent) && event.shiftKey) ||
(Array.isArray(event.detail.inputEventModifiers) &&
event.detail.inputEventModifiers.includes("shift"))) {
cmdName = "copyChars";
cmdParams = "[implicitElement][separateParagraphs]";
}
}
return docView.sendExecuteCommand(mode, cmdName, cmdParams)
.then((result) => {
return this.commandExecuted(mode, docView, params, result);
});
}
}
ALL_LOCAL_COMMANDS.copy = new CopyCmd();
class EditPITargetCmd extends InteractiveRemoteCommand {
constructor() {
super("editPITarget");
}
resumeExecution(mode, docView, params,
stoppedCommandInfo, resolve, reject) {
const suggestedTarget = stoppedCommandInfo;
this.setPITarget(suggestedTarget, mode, docView, params, resolve);
}
setPITarget(suggestedTarget, mode, docView, params, resolve) {
const checkInputValue = (inputValue) => {
if (inputValue !== null &&
!EditPITargetCmd.isPITarget(inputValue)) {
return false;
} else {
return true;
}
}
XUI.Prompt.showPrompt("Change PI Target",
"Processing instruction target:",
40, suggestedTarget,
null, checkInputValue, docView)
.then((newTarget) => {
if (newTarget === null ||
(newTarget = newTarget.trim()).length === 0) {
resolve(CommandResult.CANCELED);
} else {
params = (params === null)? newTarget :
`${params} ${newTarget}`;
let result = this.execute(mode, docView, params);
resolve(result);
}
});
}
static isPITarget(target) {
if (target === null || target.length === 0) {
return false;
}
return (Name.isName(target) && !target.toLowerCase().startsWith("xml"));
}
}
ALL_LOCAL_COMMANDS.editPITarget = new EditPITargetCmd();
class InsertCharByNameCmd extends InteractiveRemoteCommand {
constructor() {
super("insertCharByName");
}
resumeExecution(mode, docView, params,
stoppedCommandInfo, resolve, reject) {
let paramTemplate = null;
let charItems = null;
let lines = splitTrimString(stoppedCommandInfo, '\n');
if (lines.length > 0 && lines[0].indexOf("%N") >= 0) {
paramTemplate = lines[0];
lines.shift();
charItems = [];
for (let line of lines) {
let [name, code] = line.split('=');
let char = null;
if (!name ||
(char = InsertCharByNameCmd.parseUnicode(code)) === null) {
paramTemplate = null;
break;
}
charItems.push([name, char]);
}
}
if (paramTemplate === null) {
let msg = `InsertCharByNameCmd: invalid stopped command info:
${stoppedCommandInfo}`;
console.error(msg);
resolve(CommandResult.failed(msg));
return;
}
this.chooseChar(charItems, mode, docView, paramTemplate, resolve);
}
static parseUnicode(code) {
if ((typeof code !== "string") || !code.startsWith("\\u")) {
return null;
}
let char = null;
try {
char = String.fromCodePoint(parseInt(code.substring(2), 16));
} catch (error) {
console.error(`Cannot parse Unicode ${code}: ${error}`);
}
return char;
}
chooseChar(charItems, mode, docView, paramTemplate, resolve) {
const chooserOptions = {
"rows": 15,
"completionKey": "Space",
"singleClickToAccept": true,
"labelMaker": (item) => {
return item[0];
},
"htmlRenderer": (item, index) => {
return `${XUI.Util.escapeHTML(item[0])}\
<span style="margin-left: 2em; \
font-weight: bold; font-size: 1.1em; padding: 0.125em; \
color: white; background-color: navy;">${XUI.Util.escapeHTML(item[1])}</span>`;
},
};
XUI.ItemChooser.showChooser("Insert Character by Name", charItems,
null, chooserOptions,
docView)
.then((choice) => {
if (choice === null) {
resolve(CommandResult.CANCELED);
} else {
let param = paramTemplate.replace("%N", choice[0]);
let result = this.execute(mode, docView, param);
resolve(result);
}
});
}
}
ALL_LOCAL_COMMANDS.insertCharByName = new InsertCharByNameCmd();
class RemarkEditor extends XUI.Dialog {
static edit(info, reference=null) {
return new Promise((resolve, reject) => {
let editor = new RemarkEditor(info, resolve);
editor.open("center", reference);
});
}
constructor(info, resolve) {
super({ title: (info === null)? "Insert Remark" : "Edit Remark",
movable: true, closeable: true, resizable: true,
template: RemarkEditor.TEMPLATE,
buttons: [ { label: "Cancel", action: "cancelAction" },
{ label: "OK", action: "okAction" } ]});
this._info = info;
this._resolve = resolve;
const content = this.contentPane.firstElementChild;
const userBar = content.firstElementChild;
this._userLabel = userBar.lastElementChild;
this._setUserButton = userBar.firstElementChild;
this._setUserButton.textContent = XUI.StockIcon["user"];
this._setUserButton.setAttribute("title", "Specify your username");
this._setUserButton.onclick = this.setUserAction.bind(this);
this._remarkScroller = document.createElement("div");
this._remarkScroller.setAttribute("class",
"xui-control xxe-remed-scroller");
content.appendChild(this._remarkScroller);
this._collapseAction = this.collapseAction.bind(this);
this._deleteAction = this.deleteAction.bind(this);
this._replyAction = this.replyAction.bind(this);
this._knownUserFg = {};
this._knownUserBg = {};
this._remarkCounter = [0];
this._remarkIdToInfo = {};
if (info !== null) {
this.addRemarkDiv(info, this._remarkScroller);
}
this._username =
window.localStorage.getItem(RemarkEditor.USERNAME_KEY);
this.userChanged(this._username);
}
addRemarkDiv(info, parent) {
if (!(info.editor in this._knownUserFg)) {
this._knownUserFg[info.editor] = info.fg;
this._knownUserBg[info.editor] = info.bg;
}
let div = document.createElement("div");
div.setAttribute("class", "xxe-remed-remark");
const id = String(this._remarkCounter[0]++);
this._remarkIdToInfo[id] = { editor: info.editor, date: info.date,
bg: info.bg };
div.setAttribute("data-remark-id", id);
parent.appendChild(div);
let head = document.createElement("div");
head.setAttribute("class", "xxe-remed-head");
div.appendChild(head);
let collapser = document.createElement("span");
collapser.setAttribute("class", "xxe-tool-button xxe-remed-collapser");
collapser.setAttribute("title", "Collapse/expand this remark");
collapser.setAttribute("data-remark-collapsed", "0");
collapser.textContent = XUI.StockIcon["down-dir"];
collapser.onclick = this._collapseAction;
head.appendChild(collapser);
let editor = document.createElement("span");
editor.setAttribute("class", "xxe-remed-editor");
editor.textContent =
RemarkIndicator.remarkHeader(info.editor, info.date);
head.appendChild(editor);
let reply = document.createElement("span");
reply.setAttribute("class", "xxe-tool-button xxe-remed-reply");
reply.setAttribute("title", "Reply to this remark");
reply.textContent = XUI.StockIcon["reply"];
reply.onclick = this._replyAction;
head.appendChild(reply);
let del = document.createElement("span");
del.setAttribute("class", "xxe-tool-button xxe-remed-delete");
del.setAttribute("title", "Delete this remark and all its replies");
del.textContent = XUI.StockIcon["trash"];
del.onclick = this._deleteAction;
head.appendChild(del);
let body = document.createElement("div");
body.setAttribute("class", "xxe-remed-body");
div.appendChild(body);
let text = document.createElement("textarea");
text.setAttribute("class", "xui-control xxe-remed-text");
text.setAttribute("style", `border-color: ${info.fg};`);
text.rows = 5;
text.cols = 60;
text.spellcheck = true;
text.value = (info.text === null)? "" : info.text;
body.appendChild(text);
if (Array.isArray(info.replies)) {
for (const reply of info.replies) {
this.addRemarkDiv(reply, body);
}
}
return div;
}
setUserAction() {
const checkUsername = (value) => {
if (value !== null) {
value = value.trim();
const length = value.length;
if (length >= 2 && length <= 64 &&
/^[\p{L}_]+[\p{L}_\p{N}.-]*$/u.test(value)) {
return true;
}
}
return false;
}
XUI.Prompt.prompt({ title: "Specify Your Username",
prompt: `Your username when adding a remark \
or replying to a remark:`,
columns: 60,
hint: `<html>2 to 64 characters long;<br> \
starts with a letter or "<b>_</b>"; \
contains letters, numbers, "<b>.</b>", "<b>-</b>", "<b>_</b>".`,
initialValue: this.username,
inputValueChecker: checkUsername
}, this.form)
.then((username) => {
if (username !== null &&
(username = username.trim()).length > 0) {
this.username = username;
}
});
}
get username() {
return this._username;
}
set username(username) {
if (username !== this._username) {
this._username = username;
window.localStorage.setItem(RemarkEditor.USERNAME_KEY, username);
this.userChanged(username);
}
}
userChanged(username) {
if (username) {
this._setUserButton.classList.remove("xxe-remed-no-user");
this._userLabel.classList.remove("xxe-remed-no-user");
this._userLabel.textContent = username;
} else {
this._setUserButton.classList.add("xxe-remed-no-user");
this._userLabel.classList.add("xxe-remed-no-user");
this._userLabel.textContent = "???";
}
this.applyUserChange(username);
}
applyUserChange(username) {
let remarkDiv = this._remarkScroller.firstElementChild;
let newRemark = false;
if (remarkDiv === null && username) {
let info = this.newRemarkInfo(username);
remarkDiv = this.addRemarkDiv(info, this._remarkScroller);
newRemark = true;
}
if (remarkDiv === null) {
return;
}
this.setEditableFor(username, remarkDiv, newRemark);
}
newRemarkInfo(username) {
let fg = this._knownUserFg[username];
if (!fg) {
fg = "#9BBCD8";
}
let bg = this._knownUserBg[username];
if (!bg) {
bg = "#F0F8FF";
}
return { editor: username, date: Date.now(), text: null,
fg: fg, bg: bg };
}
setEditableFor(username, remarkDiv, newRemark) {
const textAreas = remarkDiv.querySelectorAll("textarea.xxe-remed-text");
const count = textAreas.length;
for (let i = 0; i < count; ++i) {
const textArea = textAreas[i];
let info = this.lookupRemarkInfo(textArea);
if (info !== null) {
if (info.editor === username) {
textArea.readOnly = false;
textArea.style.removeProperty("background-color");
} else {
textArea.readOnly = true;
textArea.style.setProperty("background-color", info.bg);
}
}
const body = textArea.parentElement;
const head = body.previousElementSibling;
const del = head.lastElementChild;
const reply = del.previousElementSibling;
if (!username || newRemark) {
reply.style.setProperty("display", "none");
del.style.setProperty("display", "none");
} else {
if (info !== null && info.editor === username) {
reply.style.setProperty("display", "none");
del.style.removeProperty("display");
} else {
reply.style.removeProperty("display");
del.style.setProperty("display", "none");
}
}
}
}
lookupRemarkInfo(elem) {
while (elem !== null) {
if (elem.hasAttribute("data-remark-id")) {
return this.getRemarkInfo(elem);
}
elem = elem.parentElement;
if (elem === this._remarkScroller) {
break;
}
}
return null;
}
getRemarkInfo(elem) {
const id = elem.getAttribute("data-remark-id");
if (id) {
const info = this._remarkIdToInfo[id];
return (typeof info === "object")? info : null;
} else {
return null;
}
}
collapseAction(event) {
const collapser = event.target;
const head = collapser.parentElement;
const body = head.nextElementSibling;
let icon, state;
const collapsed =
(collapser.getAttribute("data-remark-collapsed") === "1");
if (collapsed) {
body.style.removeProperty("display");
icon = XUI.StockIcon["down-dir"];
state = "0";
} else {
body.style.setProperty("display", "none");
icon = XUI.StockIcon["right-dir"];
state = "1";
}
collapser.textContent = icon;
collapser.setAttribute("data-remark-collapsed", state);
}
replyAction(event) {
const button = event.target;
const head = button.parentElement;
const body = head.nextElementSibling;
let info = this.newRemarkInfo(this.username);
const reply = this.addRemarkDiv(info, body);
this.setEditableFor(this.username, reply,  true);
reply.scrollIntoView({ block: "start", inline: "start" });
const textArea = reply.querySelector("textarea.xxe-remed-text");
if (textArea !== null) {
textArea.focus();
}
}
deleteAction(event) {
const button = event.target;
const head = button.parentElement;
const remark = head.parentElement;
const remarkParent = remark.parentElement;
let show = remark.nextElementSibling;
if (show === null) {
show = remark.previousElementSibling;
if (show === null) {
show = remarkParent;
}
}
remarkParent.removeChild(remark);
const id = remark.getAttribute("data-remark-id");
if (id) {
this._remarkIdToInfo[id] = false;
}
show.scrollIntoView({ block: "start", inline: "start" });
}
dialogClosed(result) {
this._resolve(result);
}
cancelAction() {
this.close(null);
}
okAction() {
let info = null;
let remarkDiv = this._remarkScroller.firstElementChild;
if (remarkDiv === null) {
info = RemarkEditor.NOT_A_REMARK_INFO;
} else {
try {
info = this.collectInfo(remarkDiv);
} catch (error) {
if (error.cause instanceof Element) {
let div = error.cause;
const textArea =
div.querySelector("textarea.xxe-remed-text");
if (textArea !== null) {
textArea.scrollIntoView({ block: "start",
inline: "start" });
textArea.focus();
textArea.select();
}
} else {
XUI.Alert.showError(error.message,  this.form);
}
return;
}
if (RemarkEditor.sameInfo(info, this._info)) {
info = null;
}
}
this.close(info);
}
collectInfo(remarkDiv) {
const info = this.getRemarkInfo(remarkDiv);
if (info === null) {
throw new Error("Internal error.");
}
let collected = Object.assign({}, info);
const head = remarkDiv.firstElementChild;
const body = head.nextElementSibling;
const text = body.firstElementChild;
collected.text = text.value;
if (collected.text.indexOf("\r\n") >= 0) {
collected.text = collected.text.replaceAll("\r\n", "\n");
}
collected.text = collected.text.trim();
if (collected.text.length === 0) {
throw new Error("A remark may not be empty.",
{ cause: remarkDiv });
}
collected.replies = [];
let replyDiv = text.nextElementSibling;
while (replyDiv !== null) {
let reply = this.collectInfo(replyDiv);
collected.replies.push(reply);
replyDiv = replyDiv.nextElementSibling;
}
return collected;
}
static sameInfo(info1, info2) {
if (info1 === null || info2 === null) {
return (info1 === null && info2 === null);
}
if (info1.editor !== info2.editor ||
info1.date !== info2.date ||
info1.text !== info2.text) {
return false;
}
const replies1 = Array.isArray(info1.replies)? info1.replies : [];
const replies2 = Array.isArray(info2.replies)? info2.replies : [];
const count = replies1.length;
if (count !== replies2.length) {
return false;
}
for (let i = 0; i < count; ++i) {
if (!RemarkEditor.sameInfo(replies1[i], replies2[i])) {
return false;
}
}
return true;
}
}
RemarkEditor.USERNAME_KEY = "XXE.RemarkEditor.username";
RemarkEditor.NOT_A_REMARK_INFO = { editor: "???", date: 0, text: "-",
replies: [] };
RemarkEditor.TEMPLATE = document.createElement("template");
RemarkEditor.TEMPLATE.innerHTML = `
<div class="xxe-remed-content">
  <div class="xui-control xxe-remed-user-bar">
    <button type="button" class="xui-small-icon xxe-remed-set-user">
    </button><span class="xxe-remed-user"></span>
  </div>
</div>
`;
class RemarkCmd extends InteractiveRemoteCommand {
constructor() {
super("remark");
}
resumeExecution(mode, docView, params,
stoppedCommandInfo, resolve, reject) {
let info = null;
if (stoppedCommandInfo.length > 0) {
try {
info = JSON.parse(stoppedCommandInfo);
} catch {
let msg = `RemarkCmd: invalid stopped command info:
${stoppedCommandInfo}`;
console.error(msg);
resolve(CommandResult.failed(msg));
return;
}
}
RemarkEditor.edit(info, docView)
.then((edited) => {
if (edited === null) {
resolve(CommandResult.CANCELED);
} else {
let result = this.execute(
mode, docView,
"edit " + RemarkCmd.formatRemarkInfo(edited));
resolve(result);
}
});
}
static formatRemarkInfo(info) {
let formatted = info.editor;
formatted += '\n';
let date = (new Date(info.date)).toISOString();
formatted += date.replace(/\.\d\d\dZ$/, "Z");
formatted += '\n';
let text = info.text;
if (text.indexOf('>') >= 0) {
text = text.replaceAll(">", "<gt/>");
}
formatted += text;
if (Array.isArray(info.replies)) {
for (const reply of info.replies) {
formatted += "\n<re>";
formatted += RemarkCmd.formatRemarkInfo(reply);
formatted += "</re>";
}
}
return formatted;
}
}
ALL_LOCAL_COMMANDS.remark = new RemarkCmd();
class ChooseFieldCmd extends InteractiveRemoteCommand {
constructor(commandName) {
super(commandName);
}
resumeExecution(mode, docView, params,
stoppedCommandInfo, resolve, reject) {
let cmdLabel = "???";
switch (this.commandName) {
case "replace":
cmdLabel = "Replace";
break;
case "insert":
cmdLabel = "Insert";
if (params !== null) {
if (params.startsWith("before")) {
cmdLabel += " Before";
} else if (params.startsWith("after")) {
cmdLabel += " After";
} else {
cmdLabel += " Into";
}
}
break;
case "convert":
cmdLabel = "Convert";
break;
case "wrap":
cmdLabel = "Wrap";
break;
}
let cmdParamItems = [];
let hasWildcards = false;
const lines = splitTrimString(stoppedCommandInfo, '\n');
const lineCount = lines.length;
for (let i = 0; i < lineCount; i += 2) {
let label = lines[i];
let param = lines[i+1];
cmdParamItems.push([ label, param ]);
if (param.indexOf("%N") >= 0) {
hasWildcards = true;
}
}
this.chooseField(cmdLabel, cmdParamItems, hasWildcards,
mode, docView, resolve);
}
chooseField(cmdLabel, cmdParamItems, hasWildcards,
mode, docView, resolve) {
const chooserOptions = {
"rows": 15,
"completionKey": "Space",
"singleClickToAccept": true,
"acceptMode": hasWildcards? "lenient" : "strict",
"labelMaker": (item) => {
return item[0];
},
"disabledChecker": (item) => {
return (item[1].indexOf("%N") >= 0);
},
"htmlRenderer": (item, index) => {
let html = XUI.Util.escapeHTML(item[0]);
if (item[1].indexOf("%N") >= 0) {
if (html.length > 60) {
html = XUI.Util.wordWrap(html, 60);
if (html.indexOf('\n') >= 0) {
html = html.replaceAll('\n', "<br>");
}
}
} else {
if (html.endsWith(')')) {
let pos = html.indexOf('(');
if (pos === 0) {
html = "<b>" + html + "</b>";
} else if (pos > 0) {
html = "<i>" + html + "</i>";
}
}
}
return html;
},
"lenientTextChecker": (text) => {
return (Name.parse(text,  false,
docView.namespacePrefixes) !== null);
},
};
const uniqueChoice =
(cmdParamItems.length === 1 &&
cmdParamItems[0][1].indexOf("%N") < 0)?
cmdParamItems[0][0] : null;
XUI.ItemChooser.showChooser(cmdLabel, cmdParamItems, uniqueChoice,
chooserOptions, docView)
.then((choice) => {
if (choice === null) {
resolve(CommandResult.CANCELED);
} else {
let acceptedText = choice[0];
let selectedItem = choice[2];
if (selectedItem === null) {
for (let cmdParamItem of cmdParamItems) {
if (cmdParamItem[1].indexOf("%N") >= 0) {
selectedItem = cmdParamItem;
break;
}
}
}
let result = CommandResult.FAILED;
if (selectedItem !== null) {
let param = selectedItem[1];
if (param.indexOf("%N") >= 0) {
let name = Name.parse(acceptedText,  false,
docView.namespacePrefixes);
if (name === null) {
param = null;
} else {
param = param.replace("%N", name.toString());
}
}
if (param !== null) {
result = this.execute(mode, docView, param);
}
}
resolve(result);
}
});
}
}
class ReplaceCmd extends ChooseFieldCmd {
constructor() {
super("replace");
}
}
ALL_LOCAL_COMMANDS.replace = new ReplaceCmd();
class InsertCmd extends ChooseFieldCmd {
constructor() {
super("insert");
}
}
ALL_LOCAL_COMMANDS.insert = new InsertCmd();
class ConvertCmd extends ChooseFieldCmd {
constructor() {
super("convert");
}
}
ALL_LOCAL_COMMANDS.convert = new ConvertCmd();
class WrapCmd extends ChooseFieldCmd {
constructor() {
super("wrap");
}
}
ALL_LOCAL_COMMANDS.wrap = new WrapCmd();
class PasteFromWordCmd extends Command {
constructor() {
super();
}
execute(mode, docView, params, event=null) {
const isHTML = [false];
return PasteFromWordCmd.readSource(ClipboardIntegration.MAX_READ_SIZE,
isHTML)
.then((source) => {
if (source === null) {
if (mode === EXECUTE_TEST) {
return Promise.resolve(false);
} else {
Command.consumeEvent(event);
return Promise.resolve(null);
}
} else {
const cmd = new LengthyRemoteCommand("pasteFromWord",
`Processing system clipboard content.\n\
\n\
Please wait...`);
return cmd.execute(mode, docView,
params +
(isHTML[0]? " --html=" : " --text=") +
source,
event);
}
});
}
static readSource(maxReadSize, isHTML) {
isHTML[0] = false;
return navigator.clipboard.read()
.then((items) => {
let item = null;
for (let i of items) {
const types = i.types;
if (types.includes("text/html")) {
item = i;
isHTML[0] = true;
break;
}
}
if (item === null) {
for (let i of items) {
const types = i.types;
if (types.includes("text/plain")) {
item = i;
break;
}
}
}
return item;
})
.then((item) => {
let blob = null;
if (item !== null) {
blob = item.getType(isHTML[0]? "text/html": "text/plain");
}
return blob;
})
.then((blob) => {
let text = null;
if (blob !== null) {
if (maxReadSize > 0 && blob.size > maxReadSize) {
throw new Error(`system clipboard content is \
too large (size=${blob.size}b > max=${maxReadSize}b)`);
}
text = blob.text();
}
return text;
})
.catch((error) => {
const msg = `Could not read system clipboard: ${error}`;
if (error.name === "SecurityError") {
console.warn(msg);
} else {
console.error(msg);
}
return null;
});
}
}
ALL_LOCAL_COMMANDS.pasteFromWord = new PasteFromWordCmd();
class ShowContentModelDialog extends XUI.Dialog {
constructor(stoppedCommandInfo, docView, dialogHandle) {
super({ title: "Show Content Model",
movable: true, resizable: true, closeable: true,
template: ShowContentModelDialog.TEMPLATE });
this._docView = docView;
this._dialogHandle = dialogHandle;
let content = this.contentPane.firstElementChild;
let buttons = content.firstElementChild;
this._backButton = buttons.firstElementChild;
this._backButton.setAttribute("title", "Previous page");
this._backButton.textContent = XUI.StockIcon["left-bold"];
this._backButton.onclick = this.backAction.bind(this);
this._forwardButton = this._backButton.nextElementSibling;
this._forwardButton.setAttribute("title", "Next page");
this._forwardButton.textContent = XUI.StockIcon["right-bold"];
this._forwardButton.onclick = this.forwardAction.bind(this);
this._indexButton = this._forwardButton.nextElementSibling;
this._indexButton.setAttribute("title", "Index");
this._indexButton.textContent = XUI.StockIcon["up-bold"];
this._indexButton.onclick = this.indexAction.bind(this);
let scroller = content.lastElementChild;
this._info = scroller.firstElementChild;
this._goAction = this.goAction.bind(this);
this.showContentModel(stoppedCommandInfo);
}
backAction(event) {
this.updateContentModel("back");
}
forwardAction(event) {
this.updateContentModel("forward");
}
indexAction(event) {
this.updateContentModel("index");
}
goAction(event) {
XUI.Util.consumeEvent(event);
const href = event.target.getAttribute("href");
if (href) {
this.updateContentModel("go " + href);
}
}
updateContentModel(params) {
this._docView.sendExecuteCommand(EXECUTE_HELPER,
"showContentModel", params)
.then((result) => {
if (result !== null &&
result.status === COMMAND_RESULT_STOPPED &&
result.value !== null) {
this.showContentModel(result.value);
}
});
}
showContentModel(info) {
let title = null;
let html = null;
let ref = null;
let canGoBack = false;
let canGoForward = false;
if (info && info.length > 1) {
const fields = info.split(info.charAt(0));
if (fields.length === 6) {
title = fields[1];
if (title.length === 0) {
title = null;
}
html = fields[2];
if (html.length === 0) {
html = null;
}
ref = fields[3];
if (ref.length === 0) {
ref = null;
}
canGoBack = (fields[4] === "1");
canGoForward = (fields[5] === "1");
}
}
if (title !== null) {
this.titleText.textContent = title;
}
this._backButton.disabled = !canGoBack;
this._forwardButton.disabled = !canGoForward;
if (html !== null) {
let body;
const match = html.match(/<body[^>]*>(.*)<\/body>/ms);
if (match !== null && match[1]) {
body = match[1];
} else {
body = XUI.Util.escapeHTML(html);
}
this._info.innerHTML = body;
const links = this._info.querySelectorAll("a[href]");
for (let link of links) {
link.addEventListener("click", this._goAction);
}
}
let elem = null;
if (ref !== null) {
elem = this._info.querySelector("#" + ref);
}
if (elem === null) {
elem = this._info.firstElementChild;
}
elem.scrollIntoView();
}
}
ShowContentModelDialog.TEMPLATE = document.createElement("template");
ShowContentModelDialog.TEMPLATE.innerHTML = `
<div class="xxe-scm-content">
  <div class="xxe-scm-buttons">
    <button type="button"
            class="xui-control xui-small-icon xxe-scm-back"></button>
    <button type="button"
            class="xui-control xui-small-icon xxe-scm-forward"></button>
    <button type="button"
            class="xui-control xui-small-icon xxe-scm-index"></button>
  </div>
  <div class="xxe-scm-scroller"><div class="xxe-scm-info"></div></div>
</div>
`;
class ShowContentModelCmd extends InteractiveRemoteCommand {
constructor() {
super("showContentModel");
}
resumeExecution(mode, docView, params,
stoppedCommandInfo, resolve, reject) {
let dialog = new ShowContentModelDialog(stoppedCommandInfo, docView);
dialog.open("center", docView.xmlEditor);
resolve(CommandResult.DONE);
}
}
ALL_LOCAL_COMMANDS.showContentModel = new ShowContentModelCmd();
class ExecuteCmd extends Command {
constructor() {
super();
this._cmdStringHistory = [ "insert after[implicitElement] ???" ];
}
execute(mode, docView, params, event=null) {
if (mode === EXECUTE_TEST) {
return Promise.resolve(true);
}
Command.consumeEvent(event);
return XUI.Prompt.showPrompt(
"Execute Command",
"Command name optionally followed by parameters:",
60, this._cmdStringHistory[0], this._cmdStringHistory,
null, docView)
.then((cmdString) => {
if (cmdString === null ||
(cmdString = cmdString.trim()).length === 0) {
return CommandResult.CANCELED;
} else {
return this.doIt(docView, cmdString);
}
});
}
doIt(docView, cmdString) {
let i = this._cmdStringHistory.indexOf(cmdString);
if (i !== 0) {
if (i > 0) {
this._cmdStringHistory.splice(i, 1);
}
this._cmdStringHistory.unshift(cmdString);
while (this._cmdStringHistory.length > 20) {
this._cmdStringHistory.pop();
}
}
let cmdName = null;
let cmdParams = null;
let sep = cmdString.indexOf(' ');
if (sep < 0) {
cmdName = cmdString;
} else {
cmdName = cmdString.substring(0, sep);
cmdParams = cmdString.substring(sep+1).trim();
if (cmdParams.length === 0) {
cmdParams = null;
}
}
return docView.executeCommand(EXECUTE_NORMAL, cmdName, cmdParams)
.then((result) => {
let msg;
if (result === null) {
msg = `Could not execute command "${cmdString}".`;
} else {
msg = `Command "${cmdString}" returned: ${result}`;
}
docView.showStatus(msg,  true);
return CommandResult.DONE;
});
}
}
ALL_LOCAL_COMMANDS.execute = new ExecuteCmd();
class ToolBar extends HTMLElement {
constructor() {
super();
this._xmlEditor = null;
this._grid = null;
this._gridRows = null;
this._toolSets = [];
this._commandButtons = [];
this._resizeObserver = new ResizeObserver(this.onResize.bind(this));
this._resizeTimeout = this.resizeTimeout.bind(this);
this._resizeTimeoutId = null;
this._resizeWidth = -1;
this._widthAfterResize = -1;
}
onResize(entries, observer) {
if (entries.length > 0) {
const entry = entries[0];
if (entry.target === this) {
let toolBarW = -1;
if (Array.isArray(entry.contentBoxSize) &&
entry.contentBoxSize.length > 0) {
toolBarW = entry.contentBoxSize[0].inlineSize;
} else if (entry.contentRect) {
toolBarW = entry.contentRect.width;
}
if (toolBarW <= 0) {
this.cancelResizeTimeout();
} else {
this.newResizeTimeout(toolBarW);
}
}
}
}
cancelResizeTimeout() {
this._resizeWidth = -1;
const resizeTimoutId = this._resizeTimeoutId;
if  (resizeTimoutId !== null) {
this._resizeTimeoutId = null;
clearTimeout(resizeTimoutId);
}
}
newResizeTimeout(toolBarW) {
this._resizeWidth = toolBarW;
if (this._resizeTimeoutId === null) {
this._resizeTimeoutId = setTimeout(this._resizeTimeout, 100 );
}
}
resizeTimeout() {
this._resizeTimeoutId = null;
const toolBarW = this._resizeWidth;
this._resizeWidth = -1;
if (toolBarW > 0) {
this.resizeToolSets(toolBarW);
}
}
resizeToolSets(toolBarW=-1) {
XUI.Dialogs.closeAllPopups();
if (toolBarW <= 0) {
this._widthAfterResize = -1;
toolBarW = this.clientWidth;
const [pl, pr] = XUI.Util.getPxProperties(this,
"padding-left", NaN,
"padding-right", NaN);
toolBarW -= pl + pr;
if (isNaN(toolBarW) || toolBarW <= 0) {
console.error(`ToolBar.resizeToolSets: \
cannot determine toolbar width (this.clientWidth=${this.clientWidth})`);
return;
}
}
if (toolBarW === this._widthAfterResize) {
return;
}
this._widthAfterResize = toolBarW;
let toolSetW = 0;
for (let toolSet of this._toolSets) {
toolSetW += toolSet.width;
ToolBar.expandToolSet(toolSet, this._gridRows);
}
let i = this._toolSets.length-1;
while (toolSetW > toolBarW && i >= 0) {
let toolSet = this._toolSets[i];
ToolBar.collapseToolSet(toolSet, this._gridRows);
toolSetW -= toolSet.width;
--i;
}
}
static expandToolSet(toolSet, rows) {
if (toolSet.collapsedCell.parentNode === null) {
return;
}
rows[0].removeChild(toolSet.collapsedCell);
const cellLists = toolSet.cellLists;
for (let i = 0; i < 3; ++i) {
const row = rows[i];
const cellList = cellLists[i];
const last = cellList.length-1;
const separCell = cellList[last];
for (let i = 0; i < last; ++i) {
row.insertBefore(cellList[i], separCell);
}
}
}
static collapseToolSet(toolSet, rows) {
if (toolSet.collapsedCell.parentNode !== null) {
return;
}
ToolBar.detachToolSet(toolSet, rows,  false);
const firstCellList = toolSet.cellLists[0];
rows[0].insertBefore(toolSet.collapsedCell,
firstCellList[firstCellList.length-1]);
}
static detachToolSet(toolSet, rows, includingSepar) {
const cellLists = toolSet.cellLists;
for (let i = 0; i < 3; ++i) {
const row = rows[i];
const cellList = cellLists[i];
let last = cellList.length-1;
if (!includingSepar) {
--last;
}
for (let i = last; i >= 0; --i) {
row.removeChild(cellList[i]);
}
}
}
static attachToolSet(toolSet, rows, includingSepar) {
const cellLists = toolSet.cellLists;
for (let i = 0; i < 3; ++i) {
const row = rows[i];
const cellList = cellLists[i];
let last = cellList.length-1;
if (!includingSepar) {
--last;
}
for (let i = 0; i <= last; ++i) {
row.appendChild(cellList[i]);
}
}
}
connectedCallback() {
if (this.firstChild === null) {
this._gridRows = new Array(3);
this._grid = ToolBar.createGrid(this._gridRows);
this.appendChild(this._grid);
this.addToolSet(ToolBar.EDIT_TOOL_SET);
}
this._resizeTimeoutId = null;
this._resizeWidth = -1;
this._widthAfterResize = -1;
this._resizeObserver.observe(this);
}
static createGrid(rows) {
let table = document.createElement("table");
table.setAttribute("class", "xui-control xxe-tlbr-grid");
let tbody = document.createElement("tbody");
table.appendChild(tbody);
rows[0] = document.createElement("tr");
tbody.appendChild(rows[0]);
rows[1] = document.createElement("tr");
tbody.appendChild(rows[1]);
rows[2] = document.createElement("tr");
tbody.appendChild(rows[2]);
return table;
}
disconnectedCallback() {
this.cancelResizeTimeout();
this._resizeObserver.unobserve(this);
this._widthAfterResize = -1;
}
addToolSet(toolSetSpec) {
let toolSet = this.createToolSet(toolSetSpec, this._toolSets.length);
this._toolSets.push(toolSet);
let oldW = this._grid.getBoundingClientRect().width;
ToolBar.attachToolSet(toolSet, this._gridRows,  true);
let newW = this._grid.getBoundingClientRect().width;
toolSet.width = newW - oldW;
}
createToolSet(toolSetSpec, toolSetIndex) {
const cellLists = [[], [], []];
const toolSet = { index: toolSetIndex, cellLists: cellLists };
const rowLengths = [0, 0, 1];
let whichRow = 0;
const addSepar = () => {
let paddingCellCount = 0;
if (rowLengths[0] < rowLengths[1]) {
whichRow = 0;
paddingCellCount = rowLengths[1] - rowLengths[0];
} else if (rowLengths[0] > rowLengths[1]) {
whichRow = 1;
paddingCellCount = rowLengths[0] - rowLengths[1];
}
while (paddingCellCount > 0) {
cellLists[whichRow].push(ToolBar.createCell(null, 1));
++rowLengths[whichRow];
--paddingCellCount;
}
let td = ToolBar.createCell("xxe-tlbr-cell-separ", 1);
cellLists[0].push(td);
++rowLengths[0];
td = ToolBar.createCell("xxe-tlbr-cell-separ", 1);
cellLists[1].push(td);
++rowLengths[1];
whichRow = 0;
};
if (Array.isArray(toolSetSpec.tools) && toolSetSpec.tools.length > 0) {
for (let tool of toolSetSpec.tools) {
switch (ToolBar.toolType(tool)) {
case "separator":
addSepar();
break;
case "button":
case "checkbox":
case "menu":
case "checkboxmenu":
{
let iconSize = [0];
let button = this.createButton(tool, iconSize);
if (button === null) {
continue;
}
ToolBar.setButtonToolSet(button, toolSetIndex);
let td = ToolBar.createCell("xxe-tlbr-cell", 1);
td.appendChild(button);
if (iconSize[0] > 16) {
let colSpan1 = 0;
let colSpan2 = 0;
if (rowLengths[0] < rowLengths[1]) {
colSpan1 = rowLengths[1] - rowLengths[0];
} else if (rowLengths[0] > rowLengths[1]) {
colSpan2 = rowLengths[0] + rowLengths[1];
}
if (colSpan1 > 0) {
let td = ToolBar.createCell(null, colSpan1);
cellLists[0].push(td);
rowLengths[0] += colSpan1;
}
if (colSpan2 > 0) {
let td = ToolBar.createCell(null, colSpan2);
cellLists[1].push(td);
rowLengths[1] += colSpan2;
}
whichRow = 0;
td.setAttribute("rowspan", "2");
cellLists[0].push(td);
++rowLengths[0];
++rowLengths[1];
} else {
cellLists[whichRow].push(td);
++rowLengths[whichRow];
if (whichRow === 0) {
whichRow = 1;
} else {
whichRow = 0;
}
}
}
break;
case "spacer":
case "span":
{
let toolSpan = null;
if (Array.isArray(tool.items) &&
tool.items.length > 0) {
toolSpan =
this.createToolSpan(tool.items, toolSetIndex);
}
let td = ToolBar.createCell("xxe-tlbr-cell", 1);
if (toolSpan !== null) {
td.appendChild(toolSpan);
}
cellLists[whichRow].push(td);
++rowLengths[whichRow];
if (whichRow === 0) {
whichRow = 1;
} else {
whichRow = 0;
}
}
break;
}
}
}
let menuOnly = false;
if (rowLengths[0] === 0) {
if (Array.isArray(toolSetSpec.menu)) {
menuOnly = true;
let td = ToolBar.createCell("xxe-tlbr-menu-only-cell", 1);
td.setAttribute("rowspan", "2");
cellLists[0].push(td);
++rowLengths[0];
++rowLengths[1];
const button = document.createElement("span");
button.setAttribute("class", "xxe-tool-button");
if (toolSetSpec.label) {
button.setAttribute("title", toolSetSpec.label);
}
ToolBar.setButtonToolSet(button, toolSetIndex);
button.textContent = XUI.StockIcon["menu"];
td.appendChild(button);
this.attachMenu(toolSetSpec.menu,  "menu", button);
} else {
cellLists[0].push(ToolBar.createCell(null, 1));
rowLengths[0] = 1;
}
}
addSepar();
let td = ToolBar.createCell("xxe-tlbr-footer-cell", rowLengths[0]-1);
cellLists[2].push(td);
const div = document.createElement("div");
td.appendChild(div);
let span = document.createElement("span");
span.setAttribute("class", "xxe-tlbr-footer-label");
let toolSetLabel = !toolSetSpec.label? "\u00A0" : toolSetSpec.label;
span.appendChild(document.createTextNode(menuOnly?
"\u00A0" : toolSetLabel));
div.appendChild(span);
if (!menuOnly && Array.isArray(toolSetSpec.menu)) {
const button = document.createElement("span");
button.setAttribute("class",
"xxe-tool-button xxe-tlbr-footer-button");
button.setAttribute("title", "More commands...");
ToolBar.setButtonToolSet(button, toolSetIndex);
button.textContent = XUI.StockIcon["triangle-1-se"];
div.appendChild(button);
this.attachMenu(toolSetSpec.menu,  "comboboxmenu", button);
}
cellLists[2].push(ToolBar.createCell("xxe-tlbr-cell-separ", 1));
td = ToolBar.createCell("xxe-tlbr-collapsed-cell", 1);
td.setAttribute("rowspan", "3");
span = document.createElement("span");
span.setAttribute("class", "xxe-tlbr-collapsed-label");
if (toolSetLabel.length > 10) {
toolSetLabel = XUI.Util.shortenText(toolSetLabel, 10);
}
span.appendChild(document.createTextNode("\u25C2 " + toolSetLabel));
td.appendChild(span);
toolSet.collapsedCell = td;
const onclick = (event) => {
XUI.Util.consumeEvent(event);
this.showToolSetPopup(toolSet);
};
td.onclick = onclick;
td.oncontextmenu = onclick;
return toolSet;
}
static toolType(tool) {
let type = tool.type;
if (!type) {
if (Object.keys(tool).length === 0) {
type = "separator";
} else if ("items" in tool) {
type = "menu";
} else {
type = "button";
}
}
return type;
}
static createCell(classes, colSpan) {
let td = document.createElement("td");
if (classes !== null) {
td.setAttribute("class", classes);
}
if (colSpan > 1) {
td.setAttribute("colspan", String(colSpan));
}
return td;
}
static setButtonToolSet(button, toolSetIndex) {
button.setAttribute("data-toolset", String(toolSetIndex));
}
getButtonToolSet(button) {
let toolSet = null;
let toolSetIndex = parseInt(button.getAttribute("data-toolset"));
if (toolSetIndex >= 0 && toolSetIndex < this._toolSets.length) {
toolSet = this._toolSets[toolSetIndex];
}
return toolSet;
}
createToolSpan(toolSpecs, toolSetIndex) {
let toolSpan = document.createElement("div");
toolSpan.setAttribute("class", "xxe-tlbr-span");
for (let toolSpec of toolSpecs) {
const toolType = ToolBar.toolType(toolSpec);
switch (toolType) {
case "separator":
case "spacer":
{
let separ = document.createElement("div");
separ.setAttribute("class", "xxe-tlbr-span-" + toolType);
if (toolType === "separator" &&
("line" in toolSpec) && !toolSpec.line) {
separ.setAttribute("style", "border-style:none;");
}
toolSpan.appendChild(separ);
}
break;
case "button":
case "checkbox":
case "menu":
case "checkboxmenu":
{
let button = this.createButton(toolSpec,  [16]);
if (button !== null) {
ToolBar.setButtonToolSet(button, toolSetIndex);
toolSpan.appendChild(button);
}
}
break;
}
}
return toolSpan;
}
createButton(tool, iconSize) {
let button = document.createElement("div");
button.setAttribute("class", "xxe-tlbr-button");
if (tool.tooltip) {
button.setAttribute("title", tool.tooltip);
}
if (!tool.iconData && !tool.iconName) {
iconSize[0] = 0;
} else {
if (iconSize[0] <= 0) {
iconSize[0] = !tool.iconSize? 16 : tool.iconSize;
}
if (iconSize[0] < 16) {
iconSize[0] = 16;
} else if (iconSize[0] > 16) {
iconSize[0] = 24;
}
}
let label = !tool.label? null : tool.label;
let childElem = (iconSize[0] > 16 && label !== null)? "div" : "span";
const items = (Array.isArray(tool.items) && tool.items.length > 0)?
tool.items : null;
if (iconSize[0] > 0) {
let buttonIcon = document.createElement(childElem);
buttonIcon.setAttribute("class", "xxe-tlbr-button-icon");
button.appendChild(buttonIcon);
let icon;
let iconName = null;
if (tool.iconData) {
icon = XUI.MenuItem.createIcon("url(" + tool.iconData + ")",
iconSize[0]);
} else {
iconName = ToolBar.iconNameWithFallback(tool);
icon = XUI.MenuItem.createEditIcon(iconName, iconSize[0]);
}
buttonIcon.appendChild(icon);
if (items !== null &&
(iconName === null || iconName !== "menu")) {
let combo  = document.createElement("span");
combo.setAttribute("class",
"xui-small-icon xxe-tlbr-button-combo" +
"-" + iconSize[0]);
combo.textContent = XUI.StockIcon["down-dir"];
buttonIcon.appendChild(combo);
}
}
if (label !== null) {
let buttonLabel = document.createElement(childElem);
buttonLabel.setAttribute("class", "xxe-tlbr-button-label");
buttonLabel.appendChild(document.createTextNode(label));
button.appendChild(buttonLabel);
}
if (items !== null) {
let attachedMenu = this.attachMenu(items,  "menu", button);
if (attachedMenu !== null && tool.type === "checkboxmenu") {
ToolBar.setCheckboxMenuCmdString(button, items);
}
} else {
const cmdString = ToolBar.itemSpecCmdString(tool);
if (cmdString === null) {
button = null;
} else {
button.setAttribute("data-command", cmdString);
const onclick = (event) => {
XUI.Util.consumeEvent(event);
if (!button.classList.contains("xui-control-disabled")) {
this.closeToolSetPopup(button);
this.performAction(cmdString, event);
}
};
button.onclick = onclick;
button.oncontextmenu = onclick;
}
}
return button;
}
static iconNameWithFallback(itemSpec) {
let iconName = itemSpec.iconName;
if (!iconName || !XUI.EditIcon[iconName]) {
iconName = "fallback";
}
return iconName;
}
static itemSpecCmdString(itemSpec) {
return Command.joinCmdString(
!itemSpec.cmdName? null : itemSpec.cmdName,
!itemSpec.cmdParam? null : itemSpec.cmdParam,
XMLEditor.CMD_STRING_SEPAR);
}
static setCheckboxMenuCmdString(button, itemSpecs) {
let cmdStrings = [];
for (let itemSpec of itemSpecs) {
switch (ToolBar.toolType(itemSpec)) {
case "separator":
break;
case "checkbox":
{
let cmdString = ToolBar.itemSpecCmdString(itemSpec);
if (cmdString === null) {
continue;
}
cmdStrings.push(cmdString);
}
break;
default:
return;
}
}
if (cmdStrings.length > 0) {
button.setAttribute("data-command",
cmdStrings.join(ToolBar.CMD_STRINGS_SEPAR));
}
}
performAction(cmdString, event) {
const docView = this.activeDocumentView;
let [cmdName, cmdParam] =
Command.splitCmdString(cmdString, ToolBar.CMD_STRING_SEPAR);
if (cmdName.endsWith("()")) {
const functionName = cmdName.substring(0, cmdName.length-2);
this[functionName]( false, docView, cmdParam);
} else {
if (docView !== null) {
docView.executeCommand(EXECUTE_NORMAL,cmdName, cmdParam, event);
}
}
}
get activeDocumentView() {
if (this._xmlEditor === null || !this._xmlEditor.documentIsOpened) {
return null;
} else {
return this._xmlEditor.documentView;
}
}
attachMenu(itemSpecs, position, button) {
const menuItems = this.createMenuItems(itemSpecs);
if (menuItems.length === 0) {
return null;
}
const menu = XUI.Menu.create(menuItems);
menu.addEventListener("menuitemselected", (event) => {
this.closeToolSetPopup(button);
this.performAction(event.detail.menuItem.name, event);
});
const onclick = (event) => {
XUI.Util.consumeEvent(event);
this.showingToolSetMenu(menu, button);
this.showMenu(menu, position, button);
};
button.onclick = onclick;
button.oncontextmenu = onclick;
return menu;
}
closeToolSetPopup(button) {
let toolSet = this.getButtonToolSet(button);
if (toolSet !== null && toolSet.popup) {
XUI.Dialogs.close(toolSet.popup);
}
}
showingToolSetMenu(menu, button) {
let toolSet = this.getButtonToolSet(button);
if (toolSet !== null && toolSet.popup) {
XUI.Dialogs.closeSubsequentPopups(toolSet.popup);
}
}
createMenuItems(itemSpecs) {
let menuItems = [];
let addSepar = false;
for (let itemSpec of itemSpecs) {
const type = ToolBar.toolType(itemSpec);
switch (type) {
case "separator":
addSepar = true;
break;
case "button":
case "checkbox":
case "menu":
case "checkboxmenu":
{
let menuItem = {};
let keyboardShortcut = null;
if (type === "menu" || type === "checkboxmenu") {
if (!Array.isArray(itemSpec.items)) {
continue;
}
let submenuItems = this.createMenuItems(itemSpec.items);
if (submenuItems.length === 0) {
continue;
}
menuItem.type = "submenu";
menuItem.items = submenuItems;
} else {
menuItem.type = type;
menuItem.name = ToolBar.itemSpecCmdString(itemSpec);
if (menuItem.name === null) {
continue;
}
}
if (itemSpec.iconData) {
menuItem.icon = "url(" + itemSpec.iconData + ")";
} else if (itemSpec.iconName) {
menuItem.icon = ToolBar.iconNameWithFallback(itemSpec);
}
if (itemSpec.label) {
menuItem.text = itemSpec.label;
}
if (itemSpec.tooltip) {
menuItem.tooltip = itemSpec.tooltip;
}
if (addSepar) {
addSepar = false;
menuItem.separator = true;
}
menuItems.push(menuItem);
}
break;
}
}
return menuItems;
}
showMenu(menu, position, reference) {
this.updateMenuItems(menu);
menu.open(position, reference);
}
updateMenuItems(menu) {
const docView = this.activeDocumentView;
for (let menuItem of menu.getAllItems()) {
const name = menuItem.name;
if (name === null) {
let submenu = menuItem.submenu;
if (submenu !== null) {
this.updateMenuItems(submenu);
}
} else {
let enabled = false;
let tooltip = null;
let inSelectedState = null;
let [cmdName, cmdParam] =
Command.splitCmdString(name, ToolBar.CMD_STRING_SEPAR);
let state =
this.getCommandState(docView, cmdName, cmdParam);
if (state !== null) {
enabled = state[0];
tooltip = state[1];
inSelectedState = state[2];
}
menuItem.enabled = enabled;
if (inSelectedState !== null &&
inSelectedState !== menuItem.selected) {
menuItem.selected = inSelectedState;
}
ToolBar.updateMenuItemTooltip(menuItem, tooltip);
ToolBar.setMenuItemShortcut(
menuItem,
ToolBar.getKeyboardShortcut(docView, cmdName, cmdParam));
}
}
}
getCommandState(docView, cmdName, cmdParam) {
if (cmdName.endsWith("()")) {
const functionName = cmdName.substring(0, cmdName.length-2);
return this[functionName]( true, docView, cmdParam);
} else {
if (docView === null) {
return [ false,  null,  null];
} else {
return docView.getCommandState(cmdName, cmdParam);
}
}
}
static updateMenuItemTooltip(menuItem, info) {
switch (menuItem.name) {
case "undo":
case "redo":
case "repeat":
{
let tooltip = menuItem.getOption("tooltip");
tooltip = ToolBar.replaceTooltipInfo(tooltip,
ToolBar.TOOLTIP_INFO_BEGIN,
ToolBar.TOOLTIP_INFO_END,
info);
menuItem.setOption("tooltip", tooltip);
}
break;
}
}
static replaceTooltipInfo(tooltip, begin, end, info) {
let tooltipLines = null;
if (tooltip !== null) {
let pos1 = tooltip.indexOf('\n');
if (pos1 > 0 && pos1+1 < tooltip.length) {
tooltipLines = tooltip.substring(pos1);
tooltip = tooltip.substring(0, pos1);
}
pos1 = tooltip.indexOf(begin);
if (pos1 >= 0) {
let pos2 = tooltip.indexOf(end, pos1 + begin.length);
if (pos2 >= 0) {
tooltip = tooltip.substring(0, pos1) +
tooltip.substring(pos2 + end.length);
}
}
}
if (info !== null) {
if (tooltip === null) {
tooltip = "";
}
let insertPos = tooltip.indexOf(ToolBar.KEYBOARD_SHORTCUT_BEGIN);
if (insertPos >= 0) {
tooltip = tooltip.substring(0, insertPos) +
begin + info + end + tooltip.substring(insertPos);
} else {
tooltip += begin + info + end;
}
}
if (tooltip !== null) {
if (tooltipLines !== null) {
tooltip += tooltipLines;
}
if (tooltip.length === 0) {
tooltip = null;
}
}
return tooltip;
}
static getKeyboardShortcut(docView, cmdName, cmdParam) {
let keyboardShortcut = null;
if (docView !== null) {
let binding = docView.getBindingForCommand(cmdName, cmdParam);
if (binding !== null) {
keyboardShortcut = binding.getUserInputLabel();
}
}
return keyboardShortcut;
}
static setMenuItemShortcut(menuItem, keyboardShortcut) {
if (menuItem.getOption("text") !== null) {
menuItem.setOption("detail", keyboardShortcut);
} else {
let tooltip = menuItem.getOption("tooltip");
tooltip = ToolBar.replaceTooltipInfo(tooltip,
ToolBar.KEYBOARD_SHORTCUT_BEGIN,
ToolBar.KEYBOARD_SHORTCUT_END,
keyboardShortcut);
menuItem.setOption("tooltip", tooltip);
}
}
showToolSetPopup(toolSet) {
XUI.Dialogs.closeAllPopups();
let div = document.createElement("div");
div.setAttribute("class", "xxe-tlbr-tlst-popup");
let gridRows = new Array(3);
let grid = ToolBar.createGrid(gridRows);
div.appendChild(grid);
ToolBar.attachToolSet(toolSet, gridRows,  false);
toolSet.popup = XUI.Dialogs.open({ form: div, type: "popup",
position: "menu",
reference: toolSet.collapsedCell });
toolSet.popup.addEventListener("dialogclosed", (event) => {
ToolBar.detachToolSet(toolSet, gridRows,  false);
delete toolSet.popup;
});
}
toggleSearchReplace(getState, docView, param) {
let checked = this._xmlEditor.searchReplaceIsVisible;
if (getState) {
return [ true,  null, checked];
} else {
this._xmlEditor.showSearchReplace(!checked);
let button = this.getCommandButton("toggleSearchReplace()", null);
if (button !== null) {
this.commandStateChanged(docView, button,
false);
}
return null;
}
}
getCommandButton(cmdName, cmdParam) {
const cmdString = Command.joinCmdString(cmdName, cmdParam);
for (let button of this._commandButtons) {
if (button.getAttribute("data-command") === cmdString) {
return button;
}
}
return null;
}
set xmlEditor(editor) {
this._xmlEditor = editor;
}
toolSetsChanged(toolSetSpecs) {
this.removeConfigSpecificToolSets();
if (toolSetSpecs !== null) {
for (let toolSetSpec of toolSetSpecs) {
this.addToolSet(toolSetSpec);
}
}
this._commandButtons =
[...this._grid.querySelectorAll("[data-command]")];
this.resizeToolSets();
this.commandStatesChanged( true);
}
removeConfigSpecificToolSets() {
XUI.Dialogs.closeAllPopups();
const toolSets = this._toolSets;
for (let i = toolSets.length-1; i >= 0; --i) {
const toolSet = toolSets[i];
ToolBar.expandToolSet(toolSet, this._gridRows);
ToolBar.detachToolSet(toolSet, this._gridRows,
true);
}
this._toolSets = [];
if (toolSets.length > 0) {
const toolSet = toolSets[0];
this._toolSets.push(toolSet);
ToolBar.attachToolSet(toolSet, this._gridRows,
true);
}
}
undoStateChanged() {
const docView = this.activeDocumentView;
for (let button of this._commandButtons) {
const cmdString = button.getAttribute("data-command");
switch (cmdString) {
case "undo":
case "redo":
this.commandStateChanged(docView, button);
break;
}
}
}
commandStatesChanged(toolSetsChanged=false) {
const docView = this.activeDocumentView;
for (let button of this._commandButtons) {
this.commandStateChanged(docView, button, toolSetsChanged);
}
}
commandStateChanged(docView, button, toolSetsChanged=false) {
const cmdStrings = button.getAttribute("data-command");
if (cmdStrings.indexOf(ToolBar.CMD_STRINGS_SEPAR) >= 0) {
button.classList.remove("xxe-tlbr-button-checked");
for (let cmdString of
cmdStrings.split(ToolBar.CMD_STRINGS_SEPAR)) {
let [cmdName, cmdParam] =
Command.splitCmdString(cmdString,
ToolBar.CMD_STRING_SEPAR);
let state =
this.getCommandState(docView, cmdName, cmdParam);
if (state !== null) {
let inSelectedState = state[2];
if (inSelectedState !== null && inSelectedState) {
button.classList.add("xxe-tlbr-button-checked");
break;
}
}
}
return;
}
let enabled = false;
let tooltip = null;
let inSelectedState = null;
let [cmdName, cmdParam] =
Command.splitCmdString(cmdStrings, ToolBar.CMD_STRING_SEPAR);
let state = this.getCommandState(docView, cmdName, cmdParam);
if (state !== null) {
enabled = state[0];
tooltip = state[1];
inSelectedState = state[2];
}
if (enabled) {
button.classList.remove("xui-control-disabled");
} else {
button.classList.add("xui-control-disabled");
}
if (inSelectedState !== null) {
if (inSelectedState) {
button.classList.add("xxe-tlbr-button-checked");
} else {
button.classList.remove("xxe-tlbr-button-checked");
}
}
ToolBar.updateButtonTooltip(button, tooltip);
if (toolSetsChanged) {
ToolBar.setButtonShortcut(
button,
ToolBar.getKeyboardShortcut(docView, cmdName, cmdParam));
}
}
static updateButtonTooltip(button, info) {
const cmdString = button.getAttribute("data-command");
switch (cmdString) {
case "undo":
case "redo":
case "repeat":
{
let tooltip = button.getAttribute("title");
tooltip = ToolBar.replaceTooltipInfo(tooltip,
ToolBar.TOOLTIP_INFO_BEGIN,
ToolBar.TOOLTIP_INFO_END,
info);
if (tooltip === null) {
button.removeAttribute("title");
} else {
button.setAttribute("title", tooltip);
}
}
break;
}
}
static setButtonShortcut(button, keyboardShortcut) {
let tooltip = button.getAttribute("title");
tooltip = ToolBar.replaceTooltipInfo(tooltip,
ToolBar.KEYBOARD_SHORTCUT_BEGIN,
ToolBar.KEYBOARD_SHORTCUT_END,
keyboardShortcut);
if (tooltip === null) {
button.removeAttribute("title");
} else {
button.setAttribute("title", tooltip);
}
}
}
ToolBar.CMD_STRING_SEPAR = '\uF876';
ToolBar.CMD_STRINGS_SEPAR = '\uF765';
ToolBar.TOOLTIP_INFO_BEGIN = "\u00A0\u201C";
ToolBar.TOOLTIP_INFO_END = "\u201D";
ToolBar.KEYBOARD_SHORTCUT_BEGIN = "\u00A0\u00A0\u00A0\u00A0[";
ToolBar.KEYBOARD_SHORTCUT_END = "]";
ToolBar.EDIT_TOOL_SET = {
label: "Edit",
menu: [
{ iconName: "repeat", tooltip: "Repeat", cmdName: "repeat" },
{},
{ iconName: "commandHistory", tooltip: "Command History...",
cmdName: "listRepeatable" },
{},
{ label: "Copy as Text",
cmdName: "copyChars", cmdParam: "[separateParagraphs]" },
{},
{ iconName: "split", label: "Split", cmdName: "split" },
{ iconName: "join", label: "Join", cmdName: "join" },
{},
{ label: "Comment", items: [
{ label: "Insert Comment Before", cmdName: "insertNode",
cmdParam: "commentBefore[implicitElement]" },
{ label: "Insert Comment",
cmdName: "insertNode", cmdParam: "commentInto" },
{ label: "Insert Comment After", cmdName: "insertNode",
cmdParam: "commentAfter[implicitElement]" },
{},
{ label: "Comment Out", cmdName: "commentOut" },
{ label: "Uncomment",
cmdName: "commentOut", cmdParam: "uncomment[implicitNode]" }
] },
{ label: "Processing Instruction", items: [
{ label: "Insert Processing Instruction Before",
cmdName: "insertNode", cmdParam: "piBefore[implicitElement]" },
{ label: "Insert Processing Instruction",
cmdName: "insertNode", cmdParam: "piInto" },
{ label: "Insert Processing Instruction After",
cmdName: "insertNode", cmdParam: "piAfter[implicitElement]" },
{},
{ label: "Change Processing Instruction Target...",
cmdName: "editPITarget", cmdParam: "[implicitNode]" }
] },
{},
{ label: "Remark", items: [
{ iconName: "insertOrEditRemark", label: "Insert or Edit Remark",
cmdName: "remark" },
{},
{ iconName: "deleteRemark", label: "Delete Remark",
cmdName: "remark", cmdParam: "delete" },
{ iconName: "deleteAllRemarks", label: "Delete All Remarks",
cmdName: "remark", cmdParam: "deleteAll" },
{},
{ iconName: "previousRemark", label: "Select Preceding Remark",
cmdName: "remark", cmdParam: "previous" },
{ iconName: "nextRemark", label: "Select Following Remark",
cmdName: "remark", cmdParam: "next" }
] },
{},
{ iconName: "declareNamespace", label: "Declare Namespace...",
cmdName: "declareNamespace" },
{},
{ iconName: "styles", label: "Change Stylesheet...",
cmdName: "setStyleSheet" }
],
tools: [
{ iconName: "undo", tooltip: "Undo", cmdName: "undo" },
{ iconName: "redo", tooltip: "Redo", cmdName: "redo" },
{},
{ iconName: "copy",
tooltip: "Copy\n\n\u2022 Shift-click to copy as text.",
cmdName: "copy", cmdParam: "[implicitElement]" },
{ iconName: "pasteBefore", tooltip: "Paste Before",
cmdName: "paste", cmdParam: "before[implicitElement]" },
{ iconName: "cut", tooltip: "Cut",
cmdName: "cut", cmdParam: "[implicitElement]" },
{ iconName: "paste", tooltip: "Paste",
cmdName: "paste", cmdParam: "toOrInto" },
{ iconName: "delete", tooltip: "Delete",
cmdName: "delete", cmdParam: "[implicitElement]" },
{ iconName: "pasteAfter", tooltip: "Paste After",
cmdName: "paste", cmdParam: "after[implicitElement]" },
{},
{ iconName: "replace", tooltip: "Replace...",
cmdName: "replace", cmdParam: "[implicitElement]" },
{ iconName: "insertBefore", tooltip: "Insert Before...",
cmdName: "insert", cmdParam: "before[implicitElement]" },
{ iconName: "convert", tooltip: "Convert...",
cmdName: "convert", cmdParam: "[implicitElement]" },
{ iconName: "insert", tooltip: "Insert...",
cmdName: "insert", cmdParam: "into" },
{ iconName: "wrap", tooltip: "Wrap...",
cmdName: "wrap", cmdParam: "[implicitElement]" },
{ iconName: "insertAfter", tooltip: "Insert After...",
cmdName: "insert", cmdParam: "after[implicitElement]" },
{},
{ iconName: "editAttributes", tooltip: "Edit Attributes...",
cmdName: "editAttributes", cmdParam: "[implicitElement]" },
{ iconName: "textSearchReplace", tooltip: "Search/Replace Text",
cmdName: "toggleSearchReplace()" }
]
}
window.customElements.define("xxe-tool-bar", ToolBar);
class SearchReplace extends HTMLElement {
constructor() {
super();
this._options = "i";
this._xmlEditor = null;
}
connectedCallback() {
if (this.firstChild === null) {
this.appendChild(SearchReplace.TEMPLATE.content.cloneNode(true));
this._searchedText = this.firstElementChild;
this._searchedText.onkeydown = this.onKeydownSearched.bind(this);
this._previousButton = this._searchedText.nextElementSibling;
this._previousButton.textContent = XUI.StockIcon["up-open"];
this._previousButton.onclick = this.findPreviousAction.bind(this);
this._nextButton = this._previousButton.nextElementSibling;
this._nextButton.textContent = XUI.StockIcon["down-open"];
this._nextButton.onclick = this.findNextAction.bind(this);
this._replacementText = this._nextButton.nextElementSibling;
this._replacementText.onkeydown =
this.onKeydownReplacement.bind(this);
this._replaceButton = this._replacementText.nextElementSibling;
this._replaceButton.textContent = XUI.StockIcon["replace"];
this._replaceButton.onclick = this.replaceNextAction.bind(this);
this._replaceAllButton = this._replaceButton.nextElementSibling;
this._replaceAllButton.textContent = XUI.StockIcon["replace-all"];
this._replaceAllButton.onclick = this.replaceAllAction.bind(this);
this._settingsButton = this._replaceAllButton.nextElementSibling;
this._settingsButton.textContent = XUI.StockIcon["settings"];
this._settingsButton.onclick = this._settingsButton.oncontextmenu =
this.setOptionsAction.bind(this);
this._settingsMenu = null;
let id = XUI.Util.uid();
this._searchHistory = XUI.Util.appendDatalist(id, [], this);
this._searchedText.setAttribute("list", id);
id = XUI.Util.uid();
this._replaceHistory = XUI.Util.appendDatalist(id, [], this);
this._replacementText.setAttribute("list", id);
}
}
onKeydownSearched(event) {
if (event.key === "Enter") {
XUI.Util.consumeEvent(event);
this.findAction( false);
}
}
findPreviousAction(event) {
XUI.Util.consumeEvent(event);
if (!event.target.classList.contains("xui-control-disabled")) {
this.findAction(true);
}
}
findNextAction(event) {
XUI.Util.consumeEvent(event);
if (!event.target.classList.contains("xui-control-disabled")) {
this.findAction(false);
}
}
findAction(previous) {
const searched = this._searchedText.value;
if (searched.length === 0) {
return;
}
if (searched.trim().length > 0) {
XUI.Util.prependDatalistItem(this._searchHistory, searched);
}
const op = previous? "p" : "n";
this._xmlEditor.documentView.executeCommand(
EXECUTE_NORMAL, "textSearchReplace",
`${op}[${this._options}]${searched}`);
}
onKeydownReplacement(event) {
if (event.key === "Enter") {
XUI.Util.consumeEvent(event);
this.replaceAction( false);
}
}
replaceNextAction(event) {
XUI.Util.consumeEvent(event);
if (!event.target.classList.contains("xui-control-disabled")) {
this.replaceAction(false);
}
}
replaceAllAction(event) {
XUI.Util.consumeEvent(event);
if (!event.target.classList.contains("xui-control-disabled")) {
this.replaceAction(true);
}
}
replaceAction(all) {
const searched = this._searchedText.value;
if (searched.length === 0) {
return;
}
if (searched.trim().length > 0) {
XUI.Util.prependDatalistItem(this._searchHistory, searched);
}
const replacement = this._replacementText.value;
if (replacement.trim().length > 0) {
XUI.Util.prependDatalistItem(this._replaceHistory, replacement);
}
const op = all? "a" : "r";
this._xmlEditor.documentView.executeCommand(
EXECUTE_NORMAL, "textSearchReplace",
`${op}[${this._options}]${searched}\n${replacement}`);
}
setOptionsAction(event) {
XUI.Util.consumeEvent(event);
if (this._settingsMenu === null) {
this._settingsMenu = XUI.Menu.create([
{ type: "checkbox",
text: "Ignore Case", name: "ignoreCase" },
{ type: "checkbox",
text: "Whole Word", name: "wholeWord" },
{ type: "checkbox",
text: "Regular Expression", name: "regularExpression" },
]);
this._settingsMenu.addEventListener("menuitemselected", (event) => {
this.toggleOption(event.detail.menuItem.name);
});
}
let item = this._settingsMenu.getItem("ignoreCase");
item.setSelected(this._options.indexOf('i') >= 0);
item = this._settingsMenu.getItem("wholeWord");
item.setSelected(this._options.indexOf('w') >= 0);
item = this._settingsMenu.getItem("regularExpression");
item.setSelected(this._options.indexOf('r') >= 0);
this._settingsMenu.open("menu", this._settingsButton);
}
toggleOption(optionName) {
let option;
switch (optionName) {
case "ignoreCase":
case "wholeWord":
case "regularExpression":
option = optionName.charAt(0);
break;
default:
return;
}
let optionList = this._options.split('');
let index = optionList.indexOf(option);
if (index >= 0) {
optionList.splice(index, 1);
} else {
optionList.push(option);
}
this._options = optionList.join('');
}
set xmlEditor(editor) {
this._xmlEditor = editor;
}
visiblityChanged(visible) {
this._searchedText.value = this._replacementText.value = "";
}
documentEdited(docUID, readOnlyDoc) {
let disable = (docUID === null);
for (let element of [ this._searchedText,
this._previousButton, this._nextButton ]) {
SearchReplace.setDisabled(element, disable);
}
disable = (disable || readOnlyDoc);
for (let element of [ this._replacementText,
this._replaceButton, this._replaceAllButton ]) {
SearchReplace.setDisabled(element, disable);
}
}
static setDisabled(element, disable) {
if (element.localName === "input") {
if (disable) {
element.setAttribute("disabled", "disabled");
} else {
element.removeAttribute("disabled");
}
} else {
if (disable) {
element.classList.add("xui-control-disabled");
} else {
element.classList.remove("xui-control-disabled");
}
}
}
}
SearchReplace.TEMPLATE = document.createElement("template");
SearchReplace.TEMPLATE.innerHTML = `
<input type="text" size="20" class="xui-control xxe-srt-searched"
       spellcheck="false" autocomplete="off"
       placeholder="Find what" />
<span class="xxe-tool-button xxe-srt-previous"
      title="Find previous occurrence"></span>
<span class="xxe-tool-button xxe-srt-next"
      title="Find next occurrence"></span>
<input type="text" size="20" class="xui-control xxe-srt-replacement"
       spellcheck="false" autocomplete="off"
       placeholder="Replace with" />
<span class="xxe-tool-button xxe-srt-replace"
      title="Replace &amp; find"></span>
<span class="xxe-tool-button xxe-srt-replace-all"
      title="Replace all"></span>
<span class="xxe-tool-button xxe-srt-settings"
      title="Search options"></span>
`;
window.customElements.define("xxe-search-replace", SearchReplace);
class NodePath extends HTMLElement {
constructor() {
super();
this._xmlEditor = null;
this._onNodeClicked = this.onNodeClicked.bind(this);
}
onNodeClicked(event) {
XUI.Util.consumeEvent(event);
if (this._xmlEditor === null ||
(event.type === "click" && event.detail !== 1)) {
return;
}
let docView = this._xmlEditor.documentView;
let uid = event.target.getAttribute("data-uid");
if (uid) {
let view = docView.getNodeView(uid,  false);
if (view !== null) {
let selectItem;
if (!Object.is(docView.selected, view) ||
docView.selected2 !== null) {
selectItem = docView.selectNode(view,  true);
} else {
selectItem = Promise.resolve(true);
}
if (event.type === "contextmenu") {
const where = event.target.getBoundingClientRect();
selectItem.then((selected) => {
if (selected) {
let cmd = docView.getCommand("contextualMenu");
if (cmd) {
cmd.showContextualMenu(docView,  null,
where.left, where.bottom);
}
}
});
} else {
if (event.shiftKey) {
selectItem.then((selected) => {
if (selected) {
docView.executeCommand(EXECUTE_NORMAL,
"selectNode", "children");
}
});
}
}
}
}
}
set xmlEditor(editor) {
this._xmlEditor = editor;
}
nodePathChanged(items) {
XUI.Util.removeAllChildren(this);
if (items === null) {
return;
}
this.appendChild(this.createDocumentSegment(this._xmlEditor));
let seg = null;
const count = items.length;
for (let i = 0; i < count; i += 3) {
let uid = items[i];
let tag = items[i+1];
let info = items[i+2];
this.appendChild(this.createSegmentSeparator());
seg = this.createNodeSegment(uid, tag, info);
this.appendChild(seg);
}
if (seg !== null) {
seg.scrollIntoView({ block: "center", inline: "end" });
}
}
createDocumentSegment(editor) {
let seg = document.createElement("span");
seg.textContent = XUI.StockIcon["doc"];
this.updateDocumentSegment(seg, editor);
return seg;
}
updateDocumentSegment(seg, editor) {
let classNames = "xui-small-icon xxe-ndpth-doc";
let tooltip = "Location:\n" + editor.documentURI;
if (editor.readOnlyDocument) {
classNames += " xxe-ndpth-doc-ro";
tooltip += "\n(read-only)";
} else if (editor.saveNeeded) {
classNames += " xxe-ndpth-doc-mod";
tooltip += "\n(modified)";
}
if (editor.configurationName !== null) {
tooltip += "\nConfiguration: " + editor.configurationName;
}
if (editor.diffSupport >= 1) {
tooltip += "\nComparison of revisions: enabled.";
if (editor.diffSupport >= 2) {
tooltip += " All revisions stored in the document.";
}
}
seg.className = classNames;
seg.setAttribute("title", tooltip);
}
createSegmentSeparator() {
let seg = document.createElement("span");
seg.className = "xui-small-icon xxe-ndpth-separ";
seg.textContent = XUI.StockIcon["right-open"];
return seg;
}
createNodeSegment(uid, tag, info) {
let detail = null;
const pos = tag.indexOf(' ');
if (pos > 0 && pos+1 < tag.length) {
detail = tag.substring(pos+1);
tag = tag.substring(0, pos);
}
let classNames = "xxe-ndpth-node";
let tooltip = tag;
if (info.length > 0) {
const lines = info.split('\n');
const lineCount = lines.length;
if (lineCount > 0) {
let flags = lines[0];
if (flags.length > 0) {
if (flags.indexOf("sel") >= 0) {
classNames += " xxe-ndpth-node-sel";
}
if (flags.indexOf("ro") >= 0) {
classNames += " xxe-ndpth-node-ro";
}
}
if (lineCount > 1) {
for (let i = 1; i < lineCount; i += 2) {
let name = lines[i];
let value = lines[i+1];
tooltip += "\n\u2022 ";
tooltip += name;
tooltip += "=";
let quote = (value.indexOf("\"") >= 0)? "'" : "\"";
tooltip += quote;
tooltip += value;
tooltip += quote;
}
}
}
}
let seg = document.createElement("span");
seg.setAttribute("data-uid", uid);
seg.className = classNames;
tooltip += `\n\n\
-\u00A0Click to select.`;
if (!tag.startsWith("#")) {
tooltip += `\n\
-\u00A0Shift-click to select child nodes.`;
}
tooltip += `\n\
-\u00A0Right-click to display contextual menu.`;
seg.setAttribute("title", tooltip);
if (detail === null) {
if (tag.length > 40) {
tag = tag.substring(0, 39) + "\u2026";
}
} else {
if (tag.length + detail.length > 40) {
if (tag.length > 30) {
tag = tag.substring(0, 29) + "\u2026";
}
let remain = 40 - tag.length;
if (remain < 5) {
remain = 5;
}
if (detail.length > remain) {
detail = detail.substring(0, remain-1) + "\u2026";
}
}
}
seg.textContent = tag;
if (detail !== null) {
let segDetail = document.createElement("span");
segDetail.setAttribute("data-uid", uid);
segDetail.className = "xxe-ndpth-node-detail";
segDetail.textContent = detail;
seg.appendChild(segDetail);
}
seg.addEventListener("click", this._onNodeClicked);
seg.addEventListener("contextmenu", this._onNodeClicked);
return seg;
}
documentStateChanged() {
let seg = this.firstElementChild;
if (seg !== null) {
this.updateDocumentSegment(seg, this._xmlEditor);
}
}
}
window.customElements.define("xxe-node-path", NodePath);
class ValidateTool extends HTMLElement {
constructor() {
super();
const infos = [
{ severity: SEVERITY_NONE, icon: "ok-circled",
color: "green", description: "Document is Valid" },
{ severity: SEVERITY_INVALID_REFERENCE, icon: "attention",
color: "#FFEF00", description: "Invalid Reference" },
{ severity: SEVERITY_SEMANTIC_WARNING, icon: "minus-circled",
color: "orange", description: "Semantic Warning" },
{ severity: SEVERITY_SEMANTIC_ERROR, icon: "minus-circled",
color: "red", description: "Semantic Error" },
{ severity: SEVERITY_INVALID_DATA, icon: "cancel-squared",
color: "orange", description: "Invalid Data" },
{ severity: SEVERITY_INVALID_STRUCTURE, icon: "cancel-squared",
color: "red", description: "Invalid Structure" }
];
this._severityInfo = {};
for (let info of infos) {
this._severityInfo[info.severity] = info;
}
this._xmlEditor = null;
this._button = null;
this._diagPopup = null;
this._onDiagPopupClosed = this.onDiagPopupClosed.bind(this);
this._onLinkClicked = this.onLinkClicked.bind(this);
}
connectedCallback() {
if (this.firstChild === null) {
this._button = document.createElement("span");
this._button.className = "xxe-tool-button xxe-valid-tool-button";
this.showSeverity(-1);
this.appendChild(this._button);
let handler = this.onClick.bind(this);
this._button.addEventListener("click", handler);
this._button.addEventListener("contextmenu", handler);
}
}
onClick(event) {
XUI.Util.consumeEvent(event);
if (this._xmlEditor !== null &&
!this._button.classList.contains("xui-control-disabled")) {
this._xmlEditor.validateDocument()
.then((result) => {
this.showDiagnostics((result !== null)?
result.diagnostics : null);
})
.catch((error) => {
console.error(`Request "validateDocument" has failed: \
${error}`);
});
}
}
showDiagnostics(diagnostics) {
if (this._diagPopup === null) {
if (diagnostics !== null && diagnostics.length > 0) {
this._diagPopup = XUI.Dialogs.open({
form: this.createDiagPopup(diagnostics), type: "popup",
classes:
"xui-control xui-dialog xxe-tool-popup xxe-valid-tool-diag",
position: "startmenu", reference: this
});
this._diagPopup.addEventListener("dialogclosed",
this._onDiagPopupClosed);
}
} else {
XUI.Dialogs.close(this._diagPopup);
}
}
createDiagPopup(diagnostics) {
let list = document.createElement("div");
list.className =
"xui-control xxe-tool-popup-list xxe-valid-tool-diag-list";
list.style.width =
String(this.parentElement.getBoundingClientRect().width / 2) + "px";
let count = 0;
for (let diag of diagnostics) {
let item = document.createElement("div");
item.className =
"xxe-valid-tool-diag-item xxe-valid-tool-diag-" +
String(diag.severity) + "-" + String(count % 2);
let link1 = this.createDiagLink(diag.severity,
String(1+count), diag.elementUID);
item.appendChild(link1);
let msg = document.createElement("div");
msg.className = "xxe-valid-tool-diag-msg";
msg.appendChild(document.createTextNode(diag.message));
const detail = diag.detail;
if (detail !== null &&
Array.isArray(detail) && detail.length === 3 &&
(detail[0] === "DUPLICATE_ID" ||
detail[0] === "DUPLICATE_ANCHOR")) {
msg.appendChild(document.createElement("br"));
msg.appendChild(document.createTextNode(
`First occurrence of "${detail[1]}" is found `));
let link2 = this.createDiagLink(-1, "here", detail[2]);
msg.appendChild(link2);
}
item.appendChild(msg);
list.appendChild(item);
++count;
}
return list;
}
createDiagLink(severity, text, elementUID) {
let link = document.createElement("span");
link.className = "xxe-valid-tool-diag-link";
link.setAttribute("data-uid", elementUID);
if (severity >= SEVERITY_INVALID_REFERENCE &&
severity <= SEVERITY_INVALID_STRUCTURE) {
const desc = this._severityInfo[severity].description;
link.setAttribute("title",
`${desc} #${text}\nClick to select the element having this error.`);
const icon = this.createDiagIcon(severity);
link.appendChild(icon);
link.appendChild(document.createTextNode("\u00A0"));
}
let label = document.createElement("span");
label.className = "xxe-valid-tool-diag-label";
label.appendChild(document.createTextNode(text));
link.appendChild(label);
link.addEventListener("click", this._onLinkClicked);
return link;
}
createDiagIcon(severity) {
let iconChar = '\u25CF';
let iconColor = "silver";
switch (severity) {
case SEVERITY_INVALID_STRUCTURE:
iconChar = '\u25A0';
//FALLTHROUGH
case SEVERITY_SEMANTIC_ERROR:
iconColor = "red";
break;
case SEVERITY_INVALID_DATA:
iconChar = '\u25A0';
//FALLTHROUGH
case SEVERITY_SEMANTIC_WARNING:
iconColor = "orange";
break;
case SEVERITY_INVALID_REFERENCE:
iconChar = '\u25B4';
iconColor = "#E4D00A";
break;
}
const icon = document.createElement("span");
icon.setAttribute("style", `color:${iconColor}`);
icon.appendChild(document.createTextNode(iconChar));
return icon;
}
onLinkClicked(event) {
XUI.Util.consumeEvent(event);
if (this._xmlEditor !== null) {
let docView = this._xmlEditor.documentView;
let uid = event.currentTarget.getAttribute("data-uid");
if (uid) {
let view = docView.getNodeView(uid,  false);
if (view !== null) {
docView.selectNode(view,  true);
}
}
}
}
onDiagPopupClosed(event) {
this._diagPopup = null;
if (this._xmlEditor !== null) {
this._xmlEditor.documentView.requestFocus();
}
}
set xmlEditor(editor) {
this._xmlEditor = editor;
}
validityStateChanged(event) {
let severity = -1;
if (event !== null) {
severity = event.severity;
}
this.showSeverity(severity);
}
showSeverity(severity) {
let title = "Check Document Validity";
let icon = "ok-circled";
let color = null;
if (severity >= SEVERITY_NONE &&
severity <= SEVERITY_INVALID_STRUCTURE) {
this._button.classList.remove("xui-control-disabled");
const info = this._severityInfo[severity];
title += "\n\n" + info.description;
icon = info.icon;
color = info.color;
} else {
this._button.classList.add("xui-control-disabled");
}
this._button.setAttribute("title", title);
this._button.textContent = XUI.StockIcon[icon];
this._button.style.color = color;
}
}
window.customElements.define("xxe-validate-tool", ValidateTool);
class StatusTool extends HTMLElement {
constructor() {
super();
this._xmlEditor = null;
this._maxLogSize = 100;
this._log = [];
this._timeout = null;
this._logPopup = null;
this._onLogPopupClosed = this.onLogPopupClosed.bind(this);
}
get maxLogSize() {
return this._maxLogSize;
}
set maxLogSize(size) {
if (size <= 0) {
size = 100;
}
this._maxLogSize = size;
while (this._log.length > size) {
this._log.pop();
}
}
connectedCallback() {
if (this.firstChild === null) {
this._messageText = document.createElement("span");
this._messageText.className = "xxe-status-tool-text";
this.appendChild(this._messageText);
this._logButton = document.createElement("span");
this._logButton.className ="xxe-tool-button xxe-status-tool-button";
this._logButton.setAttribute("title", `Show message log.
(${(PLATFORM_IS_MAC_OS? "Command" : "Ctrl")}-click to clear log.)`);
this._logButton.textContent = XUI.StockIcon["up-dir"];
this.appendChild(this._logButton);
let handler = this.onClick.bind(this);
this._logButton.addEventListener("click", handler);
this._logButton.addEventListener("contextmenu", handler);
this._messageText.addEventListener("contextmenu", handler);
}
}
disconnectedCallback() {
this.cancelTimeout();
}
cancelTimeout() {
if (this._timeout !== null) {
clearTimeout(this._timeout);
this._timeout = null;
}
}
onClick(event) {
XUI.Util.consumeEvent(event);
if (this._logPopup === null) {
const mod = PLATFORM_IS_MAC_OS? event.metaKey : event.ctrlKey;
if (mod) {
this._log = [];
}
this._logButton.textContent = XUI.StockIcon["down-dir"];
this._logPopup = XUI.Dialogs.open({
form: this.createLogPopup(), type: "popup",
classes:
"xui-control xui-dialog xxe-tool-popup xxe-status-tool-log",
position: "startmenu", reference: this
});
this._logPopup.addEventListener("dialogclosed",
this._onLogPopupClosed);
} else {
XUI.Dialogs.close(this._logPopup);
}
}
createLogPopup() {
let list = document.createElement("xui-list");
list.classList.add("xxe-tool-popup-list", "xxe-status-tool-log-list");
list.style.width =
String(this._messageText.getBoundingClientRect().width) + "px";
list.setAll(this._log);
return list;
}
onLogPopupClosed(event) {
this._logPopup = null;
this._logButton.textContent = XUI.StockIcon["up-dir"];
if (this._xmlEditor !== null) {
this._xmlEditor.documentView.requestFocus();
}
}
set xmlEditor(editor) {
this._xmlEditor = editor;
}
showStatus(text, autoErase=true) {
this.cancelTimeout();
if (text === null || (text = text.trim()).length === 0) {
text = "";
autoErase = false;
} else {
while (this._log.length >= this._maxLogSize) {
this._log.pop();
}
this._log.unshift(text);
}
this._messageText.textContent = XUI.Util.shortenText(text, 256);
if (autoErase) {
this._timeout = setTimeout(() => {
this._messageText.textContent = "";
}, 10000 );
}
}
}
window.customElements.define("xxe-status-tool", StatusTool);
class ClipboardTool extends HTMLElement {
constructor() {
super();
this._xmlEditor = null;
this._source = null;
this._sourcePopup = null;
this._onSourcePopupClosed = this.onSourcePopupClosed.bind(this);
this._plainTextTimeout = null;
}
connectedCallback() {
if (this.firstChild === null) {
this._button = document.createElement("span");
this._button.className = "xxe-tool-button xxe-clpbrd-tool-button";
this._button.setAttribute("title", "Show Clipboard Content");
this._button.textContent = XUI.StockIcon["clipboard"];
this.appendChild(this._button);
this._label = document.createElement("span");
this._label.className = "xxe-clpbrd-tool-label";
this.appendChild(this._label);
let handler = this.onClick.bind(this);
this._button.addEventListener("click", handler);
this._button.addEventListener("contextmenu", handler);
this._label.addEventListener("contextmenu", handler);
}
}
onClick(event) {
XUI.Util.consumeEvent(event);
if (this._sourcePopup === null) {
this._sourcePopup = XUI.Dialogs.open({
form: this.createSourcePopup(this._source), type: "popup",
classes:
"xui-control xui-dialog xxe-tool-popup xxe-clpbrd-tool-src",
position: "stopmenu", reference: this
});
this._sourcePopup.addEventListener("dialogclosed",
this._onSourcePopupClosed);
} else {
XUI.Dialogs.close(this._sourcePopup);
}
}
createSourcePopup(source) {
let pane = document.createElement("pre");
pane.className =
"xui-control xxe-tool-popup-list xxe-clpbrd-tool-src-pane";
pane.style.width =
String(this.parentElement.getBoundingClientRect().width / 2) + "px";
pane.textContent = (source === null)? "" : source;
return pane;
}
onSourcePopupClosed(event) {
this._sourcePopup = null;
if (this._xmlEditor !== null) {
this._xmlEditor.documentView.requestFocus();
}
}
onPaste(event) {
XUI.Util.consumeEvent(event);
if (this._xmlEditor !== null && this._xmlEditor.documentIsOpened) {
const pasted = event.clipboardData.getData('text/plain');
if (pasted !== null && pasted.length > 0) {
this._xmlEditor.documentView.sendSetClipboard(pasted);
}
}
}
set xmlEditor(editor) {
assertOrError(editor !== null);
this._xmlEditor = editor;
editor.clipboardIntegration.getLevel()
.then((level) => {
if (level < ClipboardIntegration.CAN_READ_WRITE) {
this._button.style.color =
(level < ClipboardIntegration.CAN_WRITE)?
"orange" : "#F4C430";
let toolTip = this._button.getAttribute("title");
toolTip += `\n\n\
SYSTEM CLIPBOARD INTEGRATION NOT AVAILABLE\n\
in this browser, therefore this is just a clipboard\n\
which is private to this XML editor.\n\n\
You'll have to paste text (Ctrl-V; \u2318-V on the Mac)\n\
copied from outside this XML editor into the text field\n\
at right to update the content of this private clipboard.`;
this._button.setAttribute("title", toolTip);
this._label.setAttribute("tabindex", "-1");
this._label.addEventListener("paste",
this.onPaste.bind(this));
}
});
}
clipboardUpdated(update) {
if (update !== null) {
if (!update.label) {
this.updateLabel("", false, null);
} else {
this.updateLabel(update.label, update.inclusion, update.source);
}
} else {
if (!this._xmlEditor.documentIsOpened) {
this.updateLabel("", false, null);
}
}
}
updateLabel(label, readOnly, source) {
if (this._plainTextTimeout !== null) {
clearTimeout(this._plainTextTimeout);
this._plainTextTimeout = null;
}
let plainText = null;
if (label === "#text" && (plainText = source.trim()).length > 0) {
plainText = plainText.replaceAll(/\s+/g, ' ');
plainText = XUI.Util.shortenText(plainText, 40);
this._label.textContent = plainText;
this._plainTextTimeout = setTimeout(() => {
this._label.textContent = "#text";
}, 1000 );
} else {
this._label.textContent = label;
}
if (readOnly) {
this._label.classList.add("xxe-clpbrd-tool-label-ro");
} else {
this._label.classList.remove("xxe-clpbrd-tool-label-ro");
}
this._source = source;
}
}
window.customElements.define("xxe-clipboard-tool", ClipboardTool);
export class XMLEditorEvent {
constructor(xmlEditor, data, type) {
if (data) {
for (const key in data) {
this["_" + key] = data[key];
}
}
this._xmlEditor = xmlEditor;
this._type = type;
}
get xmlEditor() {
return this._xmlEditor;
}
get type() {
return this._type;
}
toString() {
return this._type.charAt(0).toUpperCase() + this._type.substring(1) +
"Event" +
JSON.stringify(this,
(key, value) => (key === "_xmlEditor" ||
key === "_type")? undefined : value,
4);
}
}
export class ConnectedEvent extends XMLEditorEvent {
constructor(xmlEditor, data) {
super(xmlEditor, data, "connected");
}
}
export class DisconnectedEvent extends XMLEditorEvent {
constructor(xmlEditor, data) {
super(xmlEditor, data, "disconnected");
}
get code() {
return this._code;
}
get reason() {
return this._reason;
}
}
export class DocumentOpenedEvent extends XMLEditorEvent {
constructor(xmlEditor, data, type="documentOpened") {
super(xmlEditor, data, type);
if (!this._diffSupport) {
this._diffSupport = 0;
}
if (!Array.isArray(this._contextualCmds)) {
this._contextualCmds = null;
}
if (!this._configName) {
this._configName = null;
}
if (!Array.isArray(this._bindings)) {
this._bindings = null;
} else {
let bindings = [];
for (let data of this._bindings) {
let binding = Binding.fromJSON(data);
if (binding !== null) {
bindings.push(binding);
}
}
this._bindings = (bindings.length === 0)? null : bindings;
}
if (!Array.isArray(this._toolSets)) {
this._toolSets = null;
}
if (!Array.isArray(this._extensionModules)) {
this._extensionModules = null;
}
}
get uid() {
return this._uid;
}
get url() {
return this._url;
}
get persistent() {
return this._persistent;
}
get nsPrefixes() {
return this._nsPrefixes;
}
get readOnly() {
return this._readOnly;
}
get saveNeeded() {
return this._saveNeeded;
}
get saveAsNeeded() {
return this._saveAsNeeded;
}
get diffSupport() {
return this._diffSupport;
}
get view() {
return this._view;
}
get styles() {
return this._styles;
}
get marks() {
return this._marks;
}
get contextualCommands() {
return this._contextualCmds;
}
get configurationName() {
return this._configName;
}
get bindings() {
return this._bindings;
}
get toolSets() {
return this._toolSets;
}
get extensionModules() {
return this._extensionModules;
}
}
export class DocumentCreatedEvent extends DocumentOpenedEvent {
constructor(xmlEditor, data) {
super(xmlEditor, data, "documentCreated");
}
}
export class DocumentRecoveredEvent extends DocumentOpenedEvent {
constructor(xmlEditor, data) {
super(xmlEditor, data, "documentRecovered");
}
}
export const SEVERITY_NONE = 0;
export const SEVERITY_INVALID_REFERENCE = 1;
export const SEVERITY_SEMANTIC_WARNING = 2;
export const SEVERITY_SEMANTIC_ERROR = 3;
export const SEVERITY_INVALID_DATA = 4;
export const SEVERITY_INVALID_STRUCTURE = 5;
export class DocumentValidatedEvent extends XMLEditorEvent {
constructor(xmlEditor, data) {
super(xmlEditor, data, "documentValidated");
this._severity = this._diagnostics.severity;
this._diagnostics = this._diagnostics.diagnostics;
}
get severity() {
return this._severity;
}
get diagnostics() {
return this._diagnostics;
}
}
export class DocumentSavedEvent extends XMLEditorEvent {
constructor(xmlEditor, data) {
super(xmlEditor, data, "documentSaved");
}
}
export class DocumentSavedAsEvent extends XMLEditorEvent {
constructor(xmlEditor, data) {
super(xmlEditor, data, "documentSavedAs");
}
get url() {
return this._url;
}
get readOnly() {
return this._readOnly;
}
}
export class DocumentClosedEvent extends XMLEditorEvent {
constructor(xmlEditor, data) {
super(xmlEditor, data, "documentClosed");
}
}
export class SaveStateChangedEvent extends XMLEditorEvent {
constructor(xmlEditor, data) {
super(xmlEditor, data, "saveStateChanged");
}
get saveNeeded() {
return this._saveNeeded;
}
}
export class UndoStateChangedEvent extends XMLEditorEvent {
constructor(xmlEditor, data) {
super(xmlEditor, data, "undoStateChanged");
}
get commandStates() {
return !this._commandStates? null : this._commandStates;
}
}
export class EditingContextChangedEvent extends XMLEditorEvent {
constructor(xmlEditor, data) {
super(xmlEditor, data, "editingContextChanged");
}
get commandStates() {
return !this._commandStates? null : this._commandStates;
}
get nodePathItems() {
return !this._nodePathItems? null : this._nodePathItems;
}
get clipboardUpdate() {
return !this._clipboardUpdate? null : this._clipboardUpdate;
}
}
export const CHANGE_INSERT_TEXT = 0;
export const CHANGE_DELETE_TEXT = 1;
export const CHANGE_REPLACE_TEXT = 2;
export const CHANGE_REPLACE_VIEW = 3;
export const CHANGE_INSERT_VIEW = 4;
export const CHANGE_REMOVE_VIEW = 5;
export const CHANGE_PATCH_TEXT = 6;
export const CHANGE_UPDATE_CUSTOM_PART = 7;
export class DocumentViewChangedEvent extends XMLEditorEvent {
constructor(xmlEditor, data) {
super(xmlEditor, data, "documentViewChanged");
}
get changes() {
return this._changes;
}
}
export const CHANGE_NODE_MARK_ADDED       = 0;
export const CHANGE_NODE_MARK_MOVED       = 1;
export const CHANGE_NODE_MARK_REMOVED     = 2;
export const CHANGE_TEXT_LOCATION_ADDED   = 3;
export const CHANGE_TEXT_LOCATION_MOVED   = 4;
export const CHANGE_TEXT_LOCATION_REMOVED = 5;
export class DocumentMarksChangedEvent extends XMLEditorEvent {
constructor(xmlEditor, data) {
super(xmlEditor, data, "documentMarksChanged");
}
get changes() {
return this._changes;
}
get reason() {
return !this._reason? null : this._reason;
}
get dragging() {
return (this._reason === "dragging");
}
get showing() {
return (this._reason === "showing");
}
}
export class NamespacePrefixesChangedEvent extends XMLEditorEvent {
constructor(xmlEditor, data) {
super(xmlEditor, data, "namespacePrefixesChanged");
}
get nsPrefixes() {
return this._nsPrefixes;
}
}
export class ReadOnlyStateChangedEvent extends XMLEditorEvent {
constructor(xmlEditor, data) {
super(xmlEditor, data, "readOnlyStateChanged");
}
get readOnly() {
return this._readOnly;
}
}
export class ShowAlertEvent extends XMLEditorEvent {
constructor(xmlEditor, data) {
super(xmlEditor, data, "showAlert");
}
get messageType() {
return this._messageType;
}
get message() {
return this._message;
}
}
export class ShowStatusEvent extends XMLEditorEvent {
constructor(xmlEditor, data) {
super(xmlEditor, data, "showStatus");
}
get message() {
return this._message;
}
get autoErase() {
return this._autoErase;
}
}
const BINARY_PLACEHOLDER_START = "\uF192\uF193\uF194";
const BINARY_PLACEHOLDER_END = "\uF195\uF196\uF197";
class Request {
constructor(number, args, done, failed) {
this.number = number;
this.name = (typeof args[0] === "string")? args[0] : null;
this.done = done;
this.failed = failed;
this.result = undefined;
this.error = undefined;
let binaryIndex = -1;
const argCount = args.length;
for (let i = 0; i < argCount; ++i) {
let arg = args[i];
if ((arg instanceof Blob) || (arg instanceof ArrayBuffer)) {
binaryIndex = i;
break;
}
}
if (binaryIndex < 0) {
this.body = JSON.stringify([number, ...args]);
} else {
if (this.name !== null) {
args[0] = this.name + "B";
}
let binary = args[binaryIndex];
let binaryType = binary.type;
if (!binaryType) {
binaryType = "";
}
args = args.slice(0, argCount);
args[binaryIndex] =
BINARY_PLACEHOLDER_START + binaryType + BINARY_PLACEHOLDER_END;
let header = new Blob([ JSON.stringify([number, ...args]) ]);
let headerSize = header.size.toString();
while (headerSize.length < 10) {
headerSize += " ";
}
this.body = new Blob([ headerSize, header, binary ]);
}
}
}
class Client {
constructor(xmlEditor, serverURL) {
this._xmlEditor = xmlEditor;
this._serverURL = serverURL;
this._connectingWebSocket = null;
this._onOpenSuccess = null;
this._onOpenError = null;
this._onOpenFailed = null;
this._webSocket = null;
this._requestCounter = 0;
this._pendingRequests = [];
this._onDisconnectSuccess = null;
}
get xmlEditor() {
return this._xmlEditor;
}
get serverURL() {
return this._serverURL;
}
get connected() {
return (this._webSocket !== null);
}
connect() {
if (this.connected) {
return Promise.resolve(true);
} else {
if (this._connectingWebSocket !== null) {
return Promise.reject(
new Error(`already connecting to ${this._serverURL}`));
} else {
return new Promise((resolve, reject) => {
this._connectingWebSocket = new WebSocket(this._serverURL);
this._connectingWebSocket.binaryType = "blob";
this._onOpenSuccess = (event) => {
this.onOpenSuccess(event, resolve);
};
this._onOpenError = (event) => {
this.onOpenError(event);
};
this._onOpenFailed = (event) => {
this.onOpenFailed(event, reject);
};
this._connectingWebSocket.addEventListener(
"open", this._onOpenSuccess);
this._connectingWebSocket.addEventListener(
"error", this._onOpenError);
this._connectingWebSocket.addEventListener(
"close", this._onOpenFailed);
});
}
}
}
onOpenSuccess(event, resolve) {
this._webSocket = this.forgetConnectingWebSocket();
this._webSocket.onmessage = (event) => {
this.onMessage(event);
};
this._webSocket.onerror = (event) => {
this.onError(event);
};
this._webSocket.onclose = (event) => {
this.onClose(event);
};
this.initClientProperties()
.then((done) => {
resolve(true);
this._xmlEditor.notifyEventListeners("connected", null);
});
}
initClientProperties() {
let props = this._xmlEditor.allClientProperties();
if (props === null) {
return Promise.resolve(true);
} else {
return this.sendRequest( false,
"initClientProperties", props)
.then((done) => {
return true;
},
(failed) => {
console.error(
`Client[${this._serverURL}].initClientProperties: \
${failed}`);
return false;
});
}
}
forgetConnectingWebSocket() {
let ws = this._connectingWebSocket;
this._connectingWebSocket = null;
ws.removeEventListener("open", this._onOpenSuccess);
ws.removeEventListener("error", this._onOpenError);
ws.removeEventListener("close", this._onOpenFailed);
this._onOpenSuccess = null;
this._onOpenError = null;
this._onOpenFailed = null;
return ws;
}
onOpenError(event) {
console.error(`Client[${this._serverURL}].onOpenError: ${event}`);
}
onOpenFailed(event, reject) {
this.forgetConnectingWebSocket();
reject(new Error(`cannot connect to ${this._serverURL}: \
code=${event.code}, reason="${event.reason}"`));
}
onMessage(event) {
if (event.data instanceof Blob) {
this.processBinaryMessage(event.data);
} else {
let args;
try {
args = JSON.parse(event.data);
} catch (error) {
console.error(`Client[${this._serverURL}].onMessage: \
INTERNAL ERROR: cannot parse '${event.data}' as JSON: ${error}`);
return;
}
this.processMessage(args);
}
}
async processBinaryMessage(data) {
let args;
try {
let chunk = data.slice(0, 10);
let text = await chunk.text();
text = text.trim();
let headerSize = parseInt(text);
if (isNaN(headerSize) || headerSize <= 0) {
throw new Error(`'${text}', invalid header size`);
}
chunk = data.slice(10, 10 + headerSize);
text = await chunk.text();
args = JSON.parse(text);
let binaryIndex = -1;
let binaryType = "";
for (let i = 0; i < args.length; ++i) {
let arg = args[i];
if ((typeof arg === "string") &&
arg.startsWith(BINARY_PLACEHOLDER_START) &&
arg.endsWith(BINARY_PLACEHOLDER_END)) {
binaryIndex = i;
binaryType =
arg.substring(BINARY_PLACEHOLDER_START.length,
arg.length - BINARY_PLACEHOLDER_END.length);
break;
}
}
if (binaryIndex < 0) {
throw new Error(`did not find the binary placeholder`);
}
chunk = data.slice(10 + headerSize, data.size, binaryType);
args[binaryIndex] = chunk;
} catch (error) {
console.error(`Client[${this._serverURL}].processBinaryMessage: \
INTERNAL ERROR: cannot parse binary message: ${error}`);
return;
}
this.processMessage(args);
}
processMessage(args) {
const argCount = args.length;
if (argCount === 3 && args[0] === "E") {
this._xmlEditor.notifyEventListeners( args[1],
args[2]);
} else if (argCount === 4 && args[0] === "R") {
const requestNumber = args[1];
const result = args[2];
const error = args[3];
let requestName = null;
let foundRequest = false;
const pendingRequests = this._pendingRequests;
for (let request of pendingRequests) {
if (request.number === requestNumber) {
request.result = result;
request.error = error;
foundRequest = true;
break;
} else {
if (requestName === null) {
let r = Client.findRequest(pendingRequests,
requestNumber);
requestName = (r === null)? "NOT FOUND" :
((r.name === null)? "NO NAME" : r.name);
}
const msg =
`received result #${requestNumber} (${requestName}) \
before result #${request.number} (${request.name})`;
console.warn(`Client[${this._serverURL}].processMessage: \
${msg}`);
}
}
if (!foundRequest) {
const msg = `INTERNAL ERROR: \
did not find pending request corresponding to result #${requestNumber}`;
console.error(`Client[${this._serverURL}].processMessage: \
${msg}`);
}
let request = pendingRequests.shift();
while (request) {
if (request.result === undefined &&
request.error === undefined) {
pendingRequests.unshift(request);
break;
} else {
if (request.error !== null) {
request.failed(new Error(request.error));
} else {
request.done(request.result);
}
}
request = pendingRequests.shift();
}
} else {
console.error(`Client[${this._serverURL}].processMessage: \
INTERNAL ERROR: received unsupported message '${args}'`);
}
}
static findRequest(list, num) {
for (let r of list) {
if (r.number === num) {
return r;
}
}
return null;
}
onError(event) {
console.error(`Client[${this._serverURL}].onError: ${event}`);
}
onClose(event) {
this._webSocket = null;
this._requestCounter = 0;
let pendingRequests = this._pendingRequests;
this._pendingRequests = [];
let reason = new Error(`connection to ${this._serverURL} is closed`);
for (let req of pendingRequests) {
req.failed(reason);
}
let resolve = this._onDisconnectSuccess;
if (resolve !== null) {
this._onDisconnectSuccess = null;
resolve(true);
}
this._xmlEditor.notifyEventListeners("disconnected",
{ code: event.code,
reason: event.reason });
}
sendRequest(autoConnect, ...args) {
if (!this.connected && !autoConnect) {
return Promise.reject(
new Error(`not connected to ${this._serverURL}`));
}
return new Promise((resolve, reject) => {
this.connect()
.then((connected) => {
this.doSendRequest(args, resolve, reject);
},
(reason) => {
reject(reason);
});
});
}
doSendRequest(args, resolve, reject) {
let req = new Request(this._requestCounter++, args, resolve, reject);
this._webSocket.send(req.body);
req.body = null;
this._pendingRequests.push(req);
}
disconnect() {
if (!this.connected) {
return Promise.resolve(true);
} else {
if (this._onDisconnectSuccess !== null) {
return Promise.reject(
new Error(`already disconnecting from ${this._serverURL}`));
} else {
return new Promise((resolve, reject) => {
this._onDisconnectSuccess = resolve;
setTimeout(() => {
this._webSocket.close(1000 );
}, 500 );
});
}
}
}
}
class ClipboardIntegration {
constructor(editor) {
this._xmlEditor = editor;
this._level = ClipboardIntegration.UNKNOWN;
this._systemClipboardText = null;
}
getLevel() {
if (this._level === ClipboardIntegration.UNKNOWN) {
if (window.isSecureContext) {
return ClipboardIntegration.doGetLevel(
[ ClipboardIntegration.NONE ])
.then((level) => {
this._level = level;
return Promise.resolve(level);
});
} else {
this._level = ClipboardIntegration.NONE;
console.warn(`Current context not secure; \
SYSTEM CLIPBOARD INTEGRATION NOT AVAILABLE`);
return Promise.resolve(this._level);
}
} else {
return Promise.resolve(this._level);
}
}
static doGetLevel(level) {
return ClipboardIntegration.queryPermission("clipboard-write")
.then((canWrite) => {
if (canWrite.state === "denied") {
throw new Error('"clipboard-write" permission is denied');
} else {
level[0] = ClipboardIntegration.CAN_WRITE;
return ClipboardIntegration.queryPermission("clipboard-read");
}
})
.then((canRead) => {
if (canRead.state === "denied") {
throw new Error('"clipboard-read" permission is denied');
} else {
level[0] = ClipboardIntegration.CAN_READ_WRITE;
return Promise.resolve(level[0]);
}
})
.catch((error) => {
console.warn(`${error}; \
SYSTEM CLIPBOARD INTEGRATION NOT AVAILABLE`);
return Promise.resolve(level[0]);
});
}
static queryPermission(name) {
if (BROWSER_ENGINE_IS_GECKO) {
let state;
if (name === "clipboard-write") {
state = "granted";
} else {
state = "denied";
}
return Promise.resolve({ name: name, state: state });
} else {
return navigator.permissions.query({ name: name });
}
}
autoUpdatePrivateClipboard(docView) {
if (this._level === ClipboardIntegration.CAN_READ_WRITE) {
docView.addEventListener("focusin", (event) => {
if (this._xmlEditor.documentIsOpened) {
this.updatePrivateClipboard(docView);
}
});
docView.addEventListener("click", (event) => {
if (this._xmlEditor.documentIsOpened &&
event.button === 0 &&
event.detail === 1) {
this.updatePrivateClipboard(docView);
}
}, true);
}
}
updatePrivateClipboard(docView) {
const systemClipboardText = this._systemClipboardText;
let hasHTML = [ false ];
ClipboardIntegration.readText(ClipboardIntegration.MAX_READ_SIZE,
hasHTML)
.then((text) => {
if (text && text !== systemClipboardText) {
this._systemClipboardText = text;
if (hasHTML[0]) {
text = ClipboardIntegration.HTML_MAGIC_STRING + text;
}
docView.sendSetClipboard(text);
}
});
}
static readText(maxReadSize, hasHTML) {
hasHTML[0] = false;
return navigator.clipboard.read()
.then((items) => {
let item = null;
for (let i of items) {
const types = i.types;
if (types.includes("text/plain")) {
item = i;
if (types.includes("text/html")) {
hasHTML[0] = true;
}
break;
}
}
return item;
})
.then((item) => {
if (item === null) {
return null;
} else {
return item.getType("text/plain");
}
})
.then((blob) => {
if (blob === null) {
return null;
} else {
if (maxReadSize > 0 && blob.size > maxReadSize) {
throw new Error(`system clipboard content is \
too large (size=${blob.size}b > max=${maxReadSize}b)`);
}
return blob.text();
}
})
.catch((error) => {
const msg = `Could not read system clipboard: ${error}`;
if (error.name === "SecurityError") {
console.warn(msg);
} else {
console.error(msg);
}
return null;
});
}
clipboardUpdated(clipboardUpdate) {
if (this._level >= ClipboardIntegration.CAN_WRITE) {
if (clipboardUpdate !== null) {
this.updateSystemClipboard(clipboardUpdate.source);
} else {
if (!this._xmlEditor.documentIsOpened) {
this._systemClipboardText = null;
}
}
}
}
updateSystemClipboard(newText) {
if (newText && newText !== this._systemClipboardText) {
navigator.clipboard.writeText(newText)
.then(() => {
this._systemClipboardText = newText;
})
.catch((error) => {
console.warn(`Could not write text to \
the system clipboard: ${error}`);
});
}
}
}
ClipboardIntegration.UNKNOWN = -1;
ClipboardIntegration.NONE = 0;
ClipboardIntegration.CAN_WRITE = 1;
ClipboardIntegration.CAN_READ_WRITE = 2;
ClipboardIntegration.HTML_MAGIC_STRING = "\uEFED\uEFEC\uEFEB\uEFEA";
ClipboardIntegration.MAX_READ_SIZE = 500 * 1024;
export class XMLEditor extends HTMLElement {
constructor() {
super();
this._attrBeingChanged = null;
this._client = null;
this._clipboardIntegration = new ClipboardIntegration(this);
this._toolBar = null;
this._searchReplace = null;
this._nodePath = null;
this._validateTool = null;
this._statusTool = null;
this._clipboardTool = null;
this._docView = null;
this._extensionModules = {}
this.clearDocumentState();
this._resourceStorage = new ResourceStorage(this);
this._remoteFileStorage = new RemoteFiles(this);
this._resourceCache = {};
let listeners = {};
for (let type of XMLEditor.EVENT_TYPES) {
listeners[type] = [];
}
this._eventListeners = listeners;
this._requestListeners = [];
}
clearDocumentState() {
this._documentUID = null;
this._documentURL = null;
this._namespacePrefixes = null;
this._readOnlyDocument = false;
this._saveNeeded = false;
this._saveAsNeeded = false;
this._diffSupport = 0;
this._configurationName = null;
this._contextualCmdStrings = null;
this._commandStates = null;
this._documentFileHandle = null;
}
connectedCallback() {
if (this.firstChild === null) {
if (!this.hasAttribute("button2pastestext")) {
this.button2PastesText = false;
}
if (!this.hasAttribute("autoconnect")) {
this.autoConnect = true;
}
if (!this.hasAttribute("autorecover")) {
this.autoRecover = true;
}
this.serverURL = this.getAttribute("serverurl");
this.appendChild(XMLEditor.TEMPLATE.content.cloneNode(true));
this._toolBar = this.querySelector("xxe-tool-bar");
this._toolBar.xmlEditor = this;
this._searchReplace = null;
this._nodePath = this.querySelector("xxe-node-path");
this._nodePath.xmlEditor = this;
this._validateTool = this.querySelector("xxe-validate-tool");
this._validateTool.xmlEditor = this;
this._statusTool = this.querySelector("xxe-status-tool");
this._statusTool.xmlEditor = this;
this._clipboardTool = this.querySelector("xxe-clipboard-tool");
this._clipboardTool.xmlEditor = this;
this._docView = this.querySelector("xxe-document-view");
this._docView.xmlEditor = this;
this.reset();
let openFileArgs = null;
if (window.name && window.name.startsWith("[")) {
try {
openFileArgs = JSON.parse(window.name);
if (Array.isArray(openFileArgs) &&
openFileArgs.length === 3 &&
openFileArgs[0] === "openRemoteFile") {
window.name = "";
} else {
openFileArgs = null;
}
} catch {}
}
setTimeout(() => {
if (this.autoRecover) {
this.initAutoRecoverInfo();
}
if (openFileArgs !== null) {
this.openRemoteFile(openFileArgs[1], openFileArgs[2]);
} else {
if (this.autoRecover) {
this.autoRecoverDocument();
}
}
}, 250 );
}
}
reset() {
this.unconfigure();
}
unconfigure() {
this._resourceCache = {};
this.clearDocumentState();
this._docView.setView(null);
this._toolBar.toolSetsChanged( null);
if (this._searchReplace !== null) {
this._searchReplace.documentEdited( null,  false);
}
this._nodePath.nodePathChanged( null);
this._validateTool.validityStateChanged( null);
this._clipboardTool.clipboardUpdated( null);
this._clipboardIntegration.clipboardUpdated( null);
}
get clientProperties() {
let props = this.getAttribute("clientproperties");
if (props !== null && (props = props.trim()).length === 0) {
props = null;
}
return props;
}
allClientProperties() {
let props = this.clientProperties;
let addedProps = [];
const info = Intl.DateTimeFormat().resolvedOptions();
let value = info.locale;
if (value) {
addedProps.push("XXE_CLIENT_LOCALE", value);
}
value = info.timeZone;
if (value) {
addedProps.push("XXE_CLIENT_TIME_ZONE", value);
}
const addedPropCount = addedProps.length;
if (addedPropCount === 0) {
return props;
}
if (props === null) {
props = "";
}
for (let i = 0; i < addedPropCount; i += 2) {
const key = addedProps[i] + "=";
const value = addedProps[i+1];
if (props.indexOf(key) < 0) {
if (props.length > 0) {
props += ";";
}
props += key + value;
}
}
return props;
}
set clientProperties(props) {
if (props === null || (props = props.trim()).length === 0) {
this.removeAttribute("clientproperties");
} else {
this.setAttribute("clientproperties", props);
}
}
get button2PastesText() {
return this.getAttribute("button2pastestext") === "true";
}
set button2PastesText(pastes) {
this.setAttribute("button2pastestext", pastes? "true" : "false");
if (this._docView !== null) {
this._docView.button2PastesText = pastes;
}
}
get autoConnect() {
return this.getAttribute("autoconnect") === "true";
}
set autoConnect(connect) {
this.setAttribute("autoconnect", connect? "true" : "false");
}
get autoRecover() {
return this.getAttribute("autorecover") === "true";
}
set autoRecover(recover) {
this.setAttribute("autorecover", recover? "true" : "false");
}
get serverURL() {
return this.getAttribute("serverurl");
}
set serverURL(url) {
this._attrBeingChanged = "serverurl";
if (!url || !/^((wss?)|(\$\{protocol\})):\/\//.test(url)) {
url = "${protocol}://${hostname}:${port}/xxe/ws";
}
if (url.indexOf("${") >= 0) {
const secure = (window.location.protocol === "https:");
let wsProtocol = secure? "wss" : "ws";
let wsHostname = window.location.hostname;
let wsPort = window.location.port;
if (!wsPort) {
wsPort = secure? "443" : "80";
}
let wsDefaultPort = secure? "18079" : "18078";
const pairs = [
[ "${protocol}", wsProtocol ],
[ "${hostname}", wsHostname ],
[ "${port}", wsPort ],
[ "${defaultPort}", wsDefaultPort ]
];
for (let [ref, value] of pairs) {
if (url.indexOf(ref) >= 0) {
url = url.replaceAll(ref, value);
}
}
}
if (url !== this.getAttribute("serverurl")) {
this.setAttribute("serverurl", url);
}
if (!this._client || url !== this._client.serverURL) {
if (this._client && this._client.connected) {
this._client.disconnect();
}
this._client = new Client(this, url);
}
this._attrBeingChanged = null;
}
initAutoRecoverInfo() {
if (!this.id) {
const instances = document.querySelectorAll("xxe-client");
for (let i = 0; i < instances.length; ++i) {
if (Object.is(instances[i], this)) {
this.id = `__XXE_${String(1+i)}`;
break;
}
}
if (!this.id) {
this.id = "__XXE";
}
}
this._documentUIDKey =
window.location.pathname + "#" + this.id + ";documentUID";
}
autoRecoverDocument() {
if (!this.autoConnect || !this.autoRecover) {
window.localStorage.removeItem(this._documentUIDKey);
return;
}
let info = this.getAutoRecoverInfo();
this.pruneAutoRecoverInfo(info);
if (info.length > 0) {
this.doAutoRecoverDoc(info, 0);
}
}
getAutoRecoverInfo() {
let info = [];
let value = window.localStorage.getItem(this._documentUIDKey);
if (value !== null) {
let parsed = null;
try {
parsed = JSON.parse(value);
if (!Array.isArray(parsed) ||
parsed.length === 0 || !Array.isArray(parsed[0])) {
parsed = null;
}
} catch {}
if (parsed === null) {
window.localStorage.removeItem(this._documentUIDKey);
} else {
info = parsed;
}
}
return info;
}
pruneAutoRecoverInfo(info) {
const prevLength = info.length;
const now = Date.now();
const maxDelta = 24*3600*1000;
while (info.length > 0) {
let entry = info[0];
if (now - entry[1] < maxDelta) {
break;
}
info.splice(0, 1);
}
if (info.length !== prevLength) {
this.storeAutoRecoverInfo(info);
}
}
storeAutoRecoverInfo(info) {
if (info.length === 0) {
window.localStorage.removeItem(this._documentUIDKey);
} else {
window.localStorage.setItem(this._documentUIDKey,
JSON.stringify(info));
}
}
logAutoRecoverInfo(message, info) {
console.log(message + "; '" + this._documentUIDKey + "'=\n" +
XMLEditor.formatAutoRecoverInfo(info));
}
static formatAutoRecoverInfo(info) {
let formatted = "---";
for (let i = 0; i < info.length; ++i) {
formatted += "\n#" + i.toString() + " " +
XMLEditor.formatAutoRecoverEntry(info[i]);
}
formatted += "\n---"
return formatted;
}
static formatAutoRecoverEntry(entry) {
return `${entry[0]} ${(new Date(entry[1])).toLocaleString()}`;
}
doAutoRecoverDoc(info, from) {
if (from >= info.length) {
return;
}
const entry = info[from];
this.recoverDocument(entry[0])
.then((recovered) => {
if (!recovered) {
this.doAutoRecoverDoc(info, from+1);
}
}, (error) => {
const xmlEditorLocation =
window.location.href + "#" + this.id;
console.error(`XMLEditor[${xmlEditorLocation}]: \
cannot recover document "${entry[0]}": ${error}`);
});
}
static get observedAttributes() {
return [ "serverurl" ];
}
attributeChangedCallback(attrName, oldVal, newVal) {
if (this._attrBeingChanged !== null) {
return;
}
if (attrName === "serverurl") {
this.serverURL = newVal;
}
}
connect() {
if (this._client === null) {
return Promise.resolve(false);
} else {
return this._client.connect();
}
}
disconnect() {
if (this._client === null) {
return Promise.resolve(false);
} else {
return this._client.disconnect();
}
}
get connected() {
return (this._client !== null && this._client.connected);
}
newDocumentFromTemplate(templateContent, docURI=null) {
if (this.documentIsOpened && this.saveNeeded) {
return Promise.resolve(false);
}
return this.syncDocumentView()
.then((synced) => {
if (synced) {
return this.doSendRequest(this.autoConnect,
"newDocumentFromTemplate",
templateContent, docURI);
} else {
return false;
}
});
}
syncDocumentView() {
if (this._docView) {
return this._docView.sync();
} else {
return Promise.resolve(false);
}
}
doSendRequest(autoConnect, requestName, ...args) {
const hasListeners = (this._requestListeners.length > 0);
if (hasListeners) {
this.notifyRequestListeners(autoConnect, requestName, args,
undefined);
}
let result =
this._client.sendRequest(autoConnect, requestName, ...args);
if (hasListeners) {
return result.then((response) => {
this.notifyRequestListeners(
autoConnect, requestName, args,
response);
return response;
},
(error) => {
this.notifyRequestListeners(
autoConnect, requestName, args,
error);
throw error;
});
} else {
return result;
}
}
notifyRequestListeners(autoConnect, requestName, requestArgs, response) {
const listeners = this._requestListeners;
for (let listener of listeners) {
listener(autoConnect, requestName, requestArgs, response);
}
}
listDocumentTemplates() {
return this.syncDocumentView()
.then((synced) => {
if (synced) {
return this.doSendRequest(this.autoConnect,
"listDocumentTemplates");
} else {
return [];
}
});
}
newDocument(categoryName, templateName, docURI=null) {
if (this.documentIsOpened && this.saveNeeded) {
return Promise.resolve(false);
}
return this.syncDocumentView()
.then((synced) => {
if (synced) {
return this.doSendRequest(this.autoConnect,
"newDocument", categoryName,
templateName, docURI);
} else {
return false;
}
});
}
newRemoteFileFromTemplate(templateContent, docURL=null) {
if (this.documentIsOpened && this.saveNeeded) {
return Promise.resolve(false);
}
return this.syncDocumentView()
.then((synced) => {
if (synced) {
return this.doSendRequest(this.autoConnect,
"newFileFromTemplate",
templateContent, docURL);
} else {
return false;
}
});
}
newRemoteFile(categoryName, templateName, docURL=null) {
if (this.documentIsOpened && this.saveNeeded) {
return Promise.resolve(false);
}
return this.syncDocumentView()
.then((synced) => {
if (synced) {
return this.doSendRequest(this.autoConnect,
"newFile", categoryName,
templateName, docURL);
} else {
return false;
}
});
}
openDocument(docContent, docURI, readOnly=false) {
if (docURI === null) {
throw new Error("null docURI");
}
if (this.documentIsOpened && this.saveNeeded) {
return Promise.resolve(false);
}
return this.syncDocumentView()
.then((synced) => {
if (synced) {
return this.doSendRequest(this.autoConnect,
"openDocument", docContent,
docURI, readOnly);
} else {
return false;
}
});
}
listRootDirectories() {
return this.syncDocumentView()
.then((synced) => {
if (synced) {
return this.doSendRequest(this.autoConnect,
"listRootDirectories");
} else {
return [];
}
});
}
listFiles(url) {
return this.syncDocumentView()
.then((synced) => {
if (synced) {
return this.doSendRequest(this.autoConnect,
"listFiles", url);
} else {
return [];
}
});
}
createDirectory(url) {
return this.syncDocumentView()
.then((synced) => {
if (synced) {
return this.doSendRequest(this.autoConnect,
"createDirectory", url);
} else {
return [];
}
});
}
openRemoteFile(docURL, readOnly=false) {
if (docURL === null) {
throw new Error("null docURL");
}
if (this.documentIsOpened && this.saveNeeded) {
return Promise.resolve(false);
}
return this.syncDocumentView()
.then((synced) => {
if (synced) {
return this.doSendRequest(this.autoConnect,
"openFile", docURL, readOnly);
} else {
return false;
}
});
}
recoverDocument(documentUID) {
if (this.documentIsOpened && this.saveNeeded) {
return Promise.resolve(false);
}
return this.syncDocumentView()
.then((synced) => {
if (synced) {
return this.doSendRequest(this.autoConnect,
"recoverDocument", documentUID);
} else {
return false;
}
});
}
validateDocument() {
if (!this.documentIsOpened) {
return Promise.resolve(null);
}
return this.syncDocumentView()
.then((synced) => {
if (synced) {
return this.sendRequest("validateDocument");
} else {
return null;
}
});
}
sendRequest(requestName, ...args) {
return this.doSendRequest( false,
requestName, ...args);
}
getDocument(formattingOptions=null) {
if (!this.documentIsOpened) {
return Promise.resolve(null);
}
return this.syncDocumentView()
.then((synced) => {
if (synced) {
return this.sendRequest("getDocument", formattingOptions);
} else {
return null;
}
});
}
saveDocument() {
if (!this.documentIsOpened || !this.saveNeeded || this.saveAsNeeded) {
return Promise.resolve(false);
}
return this.syncDocumentView()
.then((synced) => {
if (synced) {
return this.sendRequest("saveDocument");
} else {
return false;
}
});
}
saveDocumentAs(location) {
if (location === null) {
throw new Error("null location");
}
if (!this.documentIsOpened) {
return Promise.resolve(false);
}
return this.syncDocumentView()
.then((synced) => {
if (synced) {
return this.sendRequest("saveDocumentAs", location);
} else {
return false;
}
});
}
closeDocument(discardChanges=false) {
if (!this.documentIsOpened) {
return Promise.resolve(true);
}
if (!discardChanges && this.saveNeeded) {
return Promise.resolve(false);
}
return this.syncDocumentView()
.then((synced) => {
if (synced) {
return this.sendRequest("closeDocument", discardChanges);
} else {
return false;
}
});
}
get documentIsOpened() {
return (this._documentUID !== null);
}
get documentUID() {
return this._documentUID;
}
get documentURI() {
return URIUtil.csriURLToURI(this._documentURL);
}
get documentURL() {
return this._documentURL;
}
get isRemoteFile() {
return (this._documentURL === null)? false :
!this._documentURL.startsWith("csri:");
}
get namespacePrefixes() {
return this._namespacePrefixes;
}
get configurationName() {
return this._configurationName;
}
get readOnlyDocument() {
return this._readOnlyDocument;
}
get saveNeeded() {
return this._saveNeeded;
}
get saveAsNeeded() {
return this._saveAsNeeded;
}
get diffSupport() {
return this._diffSupport;
}
get documentFileHandle() {
return this._documentFileHandle;
}
set documentFileHandle(handle) {
this._documentFileHandle = handle;
}
addEventListener(type, listener) {
this.removeEventListener(type, listener);
let listeners = this._eventListeners[type];
if (listeners) {
listeners.push(listener);
}
}
removeEventListener(type, listener) {
let listeners = this._eventListeners[type];
if (listeners) {
let i = listeners.indexOf(listener);
if (i >= 0) {
listeners.splice(i, 1);
}
}
}
addRequestListener(listener) {
this.removeRequestListener(listener);
this._requestListeners.push(listener);
}
removeRequestListener(listener) {
let listeners = this._requestListeners;
let i = listeners.indexOf(listener);
if (i >= 0) {
listeners.splice(i, 1);
}
}
get clipboardIntegration() {
return this._clipboardIntegration;
}
get documentView() {
return this._docView;
}
showStatus(text, autoErase=true) {
this._statusTool.showStatus(text, autoErase);
}
get searchReplaceIsVisible() {
return (this._searchReplace !== null &&
this._searchReplace.style.display !== "none");
}
showSearchReplace(show) {
let visiblityChanged = false;
if (this._searchReplace === null) {
this._searchReplace = document.createElement("xxe-search-replace");
this._nodePath.parentNode.insertBefore(this._searchReplace,
this._nodePath);
this._searchReplace.xmlEditor = this;
this._searchReplace.documentEdited(this._documentUID,
this._readOnlyDocument);
visiblityChanged = true;
}
if (show) {
if (this._searchReplace.style.display === "none") {
this._searchReplace.style.display = "block";
visiblityChanged = true;
}
} else {
if (this._searchReplace.style.display !== "none") {
this._searchReplace.style.display = "none";
visiblityChanged = true;
}
}
if (visiblityChanged) {
this._searchReplace.visiblityChanged(show);
}
return this._searchReplace;
}
getCommandState(cmdName, cmdParam) {
if (this._commandStates === null) {
return null;
}
let cmd = Command.joinCmdString(cmdName, cmdParam,
XMLEditor.CMD_STRING_SEPAR);
if (cmd === null) {
return null;
}
let state = this._commandStates[cmd];
return !state? null : state;
}
get resourceStorage() {
return this._resourceStorage;
}
set resourceStorage(storage) {
this._resourceStorage = storage;
}
getResource(uri) {
let res = this.fetchResource(uri);
if (res) {
return Promise.resolve(res);
}
return this.loadResource(uri)
.then((res) => {
if (res) {
this.cacheResource(res);
}
return res;
});
}
fetchResource(uri) {
uri = URIUtil.normalizeFileURI(uri);
const res = this._resourceCache[uri];
return !res? null : res;
}
cacheResource(resource) {
const uri = URIUtil.normalizeFileURI(resource.uri);
this._resourceCache[uri] = resource;
}
loadResource(uri) {
if (!this.documentIsOpened) {
return Promise.reject(new Error("no opened document"));
}
let loaded;
if (this.isRemoteFile) {
loaded = this._remoteFileStorage.loadResource(uri);
} else {
loaded = this._resourceStorage.loadResource(uri);
}
return loaded.then((resource) => {
if (resource === null) {
return this.downloadResource(uri);
} else {
return resource;
}
});
}
downloadResource(uri) {
return fetch(uri)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error ${response.status} \
${response.statusText}`);
}
return response.blob();
})
.then((data) => {
return new Resource(uri, data);
})
.catch((error) => {
throw new Error(`could not download resource "${uri}": ${error}`);
});
}
putResource(data, uri) {
return this.storeResource(data, uri)
.then((res) => {
if (res) {
this.cacheResource(res);
}
return res;
});
}
storeResource(data, uri) {
if (!this.documentIsOpened) {
return Promise.reject(new Error("no opened document"));
}
let stored;
if (this.isRemoteFile) {
stored = this._remoteFileStorage.storeResource(data, uri);
} else {
stored = this._resourceStorage.storeResource(data, uri);
}
return stored.then((resource) => {
if (resource === null) {
throw new Error(`do not know how to store data into "${uri}"`);
} else {
return resource;
}
});
}
openResource(options) {
if (!this.documentIsOpened) {
return Promise.reject(new Error("no opened document"));
}
if (this.isRemoteFile) {
return this._remoteFileStorage.openResource(options);
} else {
return this._resourceStorage.openResource(options);
}
}
notifyEventListeners(type, data) {
let cls = XMLEditor._EVENT_CLASSES[type];
if (!cls) {
console.error(`XMLEditor.notifyEventListeners: INTERNAL ERROR: \
${type}, unknown event type`);
return;
}
const event = new cls(this, data);
switch (type) {
case "connected":
this.onConnected(event);
break;
case "disconnected":
this.onDisconnected(event);
break;
case "documentCreated":
case "documentOpened":
case "documentRecovered":
this.onDocumentOpened(event);
break;
case "documentValidated":
this.onDocumentValidated(event);
break;
case "documentSaved":
this.onDocumentSaved(event);
break;
case "documentSavedAs":
this.onDocumentSavedAs(event);
break;
case "documentClosed":
this.onDocumentClosed(event);
break;
case "saveStateChanged":
this.onSaveStateChanged(event);
break;
case "undoStateChanged":
this.onUndoStateChanged(event);
break;
case "editingContextChanged":
this.onEditingContextChanged(event);
break;
case "documentViewChanged":
this._docView.onDocumentViewChanged(event);
break;
case "documentMarksChanged":
this._docView.onDocumentMarksChanged(event);
break;
case "namespacePrefixesChanged":
this.onNamespacePrefixesChanged(event);
break;
case "readOnlyStateChanged":
this.onReadOnlyStateChanged(event);
break;
case "showAlert":
this.onShowAlert(event);
break;
case "showStatus":
this.onShowStatus(event);
break;
}
const listeners = this._eventListeners[type];
if (listeners) {
for (const listener of listeners) {
listener(event);
}
}
}
onConnected(event) {}
onDisconnected(event) {
this.reset();
}
onDocumentOpened(event) {
this.configure(event);
if (this.autoConnect && this.autoRecover) {
this.addAutoRecoverInfo(event.uid);
}
if (event.extensionModules) {
this.loadExtensionModules(event.extensionModules);
}
}
addAutoRecoverInfo(docUID) {
let info = this.getAutoRecoverInfo();
XMLEditor.removeAutoRecoverEntry(info, docUID);
while (info.length >= 20) {
info.splice(0, 1);
}
info.push([docUID, Date.now()]);
this.storeAutoRecoverInfo(info);
}
static removeAutoRecoverEntry(info, docUID) {
let index = -1;
const count = info.length;
for (let i = 0; i < count; ++i) {
const entry = info[i];
if (entry[0] === docUID) {
index = i;
break;
}
}
if (index >= 0) {
info.splice(index, 1);
}
}
configure(event) {
this._resourceCache = {};
this._documentUID = event.uid;
this._documentURL = event.url;
this._namespacePrefixes = event.nsPrefixes;
this._readOnlyDocument = event.readOnly;
this._saveNeeded = event.saveNeeded;
this._saveAsNeeded = event.saveAsNeeded;
this._diffSupport = event.diffSupport;
let contextualCmdStrings = [];
let cmdStates = {};
const contextualCmdPairs = event.contextualCommands;
if (contextualCmdPairs !== null) {
const pairCount = contextualCmdPairs.length;
for (let i = 0; i < pairCount; i +=2) {
let cmd = Command.joinCmdString(contextualCmdPairs[i],
contextualCmdPairs[i+1],
XMLEditor.CMD_STRING_SEPAR);
contextualCmdStrings.push(cmd);
cmdStates[cmd] = [false, null, null];
}
}
this._contextualCmdStrings = contextualCmdStrings;
this._commandStates = cmdStates;
this._configurationName = event.configurationName;
this._documentFileHandle = null;
this._docView.setView({
view: event.view,
styles: event.styles,
marks: event.marks,
bindings: event.bindings });
this._toolBar.toolSetsChanged(event.toolSets);
if (this._searchReplace !== null) {
this._searchReplace.documentEdited(this._documentUID,
this._readOnlyDocument);
}
this._nodePath.nodePathChanged( []);
this._validateTool.validityStateChanged( null);
this._clipboardTool.clipboardUpdated( null);
this._clipboardIntegration.clipboardUpdated( null);
}
loadExtensionModules(moduleURIs) {
if (moduleURIs.length === 0) {
return Promise.resolve(true);
}
const moduleURI = moduleURIs.shift();
let loadingExtensionModules;
if (!this._extensionModules[moduleURI]) {
const headElem = document.documentElement.firstElementChild;
if (headElem !== null && moduleURI.endsWith(".js")) {
let cssURL = new URL(
moduleURI.substring(0, moduleURI.length-3) + ".css",
import.meta.url);
XUI.Util.addStylesheetLink(headElem, cssURL.toString());
}
loadingExtensionModules = import(moduleURI);
} else {
loadingExtensionModules = Promise.resolve(null);
}
return loadingExtensionModules
.then((module) => {
if (module !== null) {
this._extensionModules[moduleURI] = module;
}
return this.loadExtensionModules(moduleURIs);
})
.catch((error) => {
console.error(`Cannot import module "${moduleURI}": ${error}`);
return this.loadExtensionModules(moduleURIs);
});
}
onDocumentValidated(event) {
this._validateTool.validityStateChanged(event);
}
onDocumentSaved(event) {
this._saveNeeded = false;
this._saveAsNeeded = false;
this._nodePath.documentStateChanged();
}
onDocumentSavedAs(event) {
this._documentURL = event.url;
this._readOnlyDocument = event.readOnly;
this._saveNeeded = false;
this._saveAsNeeded = false;
this._nodePath.documentStateChanged();
}
onDocumentClosed(event) {
const docUID = this._documentUID;
this.unconfigure();
if (this.autoConnect && this.autoRecover) {
this.removeAutoRecoverInfo(docUID);
}
}
removeAutoRecoverInfo(docUID) {
let info = this.getAutoRecoverInfo();
XMLEditor.removeAutoRecoverEntry(info, docUID);
this.storeAutoRecoverInfo(info);
}
onSaveStateChanged(event) {
this._saveNeeded = event.saveNeeded;
this._nodePath.documentStateChanged();
this._docView.saveStateChanged();
}
onUndoStateChanged(event) {
this.updateCommandStates(event.commandStates);
this._toolBar.undoStateChanged();
}
updateCommandStates(cmdStates) {
if (cmdStates === null ||
this._contextualCmdStrings === null) {
console.error(`XMLEditor.updateCommandStates: INTERNAL ERROR: \
cannot update: cmdStates=${cmdStates}, \
this._contextualCmdStrings=${this._contextualCmdStrings}`);
this.clearCommandStates();
return;
}
const count = cmdStates.length;
for (let i = 0; i < count; ++i) {
let cmdState = cmdStates[i];
let enabled = (cmdState.charAt(0) === '1');
let inSelectedState;
switch (cmdState.charAt(1)) {
case '0':
inSelectedState = false;
break;
case '1':
inSelectedState = true;
break;
default:
inSelectedState = null;
}
let detail = (cmdState.length > 2)? cmdState.substring(2) : null;
let cmd = this._contextualCmdStrings[i];
if (!cmd) {
console.error(`XMLEditor.updateCommandStates: INTERNAL ERROR: \
command index ${i} out of range 0-${this._contextualCmdStrings.length}`);
this.clearCommandStates();
break;
}
let state = this._commandStates[cmd];
if (!state) {
console.error(`XMLEditor.updateCommandStates: INTERNAL ERROR: \
command "${cmd}" not registered`);
this.clearCommandStates();
break;
}
state[0] = enabled;
state[1] = detail;
state[2] = inSelectedState;
}
}
clearCommandStates() {
for (let cmd of this._contextualCmdStrings) {
let state = this._commandStates[cmd];
state[0] = false;
state[1] = null;
state[2] = null;
}
}
onEditingContextChanged(event) {
this.updateCommandStates(event.commandStates);
this._toolBar.commandStatesChanged();
this._nodePath.nodePathChanged(event.nodePathItems);
this._clipboardTool.clipboardUpdated(event.clipboardUpdate);
this._clipboardIntegration.clipboardUpdated(event.clipboardUpdate);
}
onNamespacePrefixesChanged(event) {
this._namespacePrefixes = event.nsPrefixes;
}
onReadOnlyStateChanged(event) {
this._readOnlyDocument = event.readOnly;
if (this._searchReplace !== null) {
this._searchReplace.documentEdited(this._documentUID,
this._readOnlyDocument);
}
}
onShowAlert(event) {
XUI.Alert.showAlert(event.messageType, event.message, this);
}
onShowStatus(event) {
this._statusTool.showStatus(event.message, event.autoErase);
}
}
XMLEditor.CMD_STRING_SEPAR = '\uF876';
XMLEditor.TEMPLATE = document.createElement("template");
XMLEditor.TEMPLATE.innerHTML = `
<div class="xxe-tool-pane">
  <xxe-tool-bar></xxe-tool-bar>
  <xxe-node-path></xxe-node-path>
</div>
<xxe-document-view></xxe-document-view>
<div class="xxe-status-pane">
  <xxe-validate-tool></xxe-validate-tool>
  <xxe-status-tool></xxe-status-tool>
  <xxe-clipboard-tool></xxe-clipboard-tool>
</div>
`;
XMLEditor.EVENT_TYPES = [
"connected",
"disconnected",
"documentCreated",
"documentOpened",
"documentRecovered",
"documentValidated",
"documentSaved",
"documentSavedAs",
"documentClosed",
"saveStateChanged",
"undoStateChanged",
"editingContextChanged",
"documentViewChanged",
"documentMarksChanged",
"namespacePrefixesChanged",
"readOnlyStateChanged",
"showAlert",
"showStatus",
];
XMLEditor._EVENT_CLASSES = {
connected: ConnectedEvent,
disconnected: DisconnectedEvent,
documentCreated: DocumentCreatedEvent,
documentOpened: DocumentOpenedEvent,
documentRecovered: DocumentRecoveredEvent,
documentValidated: DocumentValidatedEvent,
documentSaved: DocumentSavedEvent,
documentSavedAs: DocumentSavedAsEvent,
documentClosed: DocumentClosedEvent,
saveStateChanged: SaveStateChangedEvent,
undoStateChanged: UndoStateChangedEvent,
editingContextChanged: EditingContextChangedEvent,
documentViewChanged: DocumentViewChangedEvent,
documentMarksChanged: DocumentMarksChangedEvent,
namespacePrefixesChanged: NamespacePrefixesChangedEvent,
readOnlyStateChanged: ReadOnlyStateChangedEvent,
showAlert: ShowAlertEvent,
showStatus: ShowStatusEvent,
};
window.customElements.define("xxe-client", XMLEditor);
XMLEditor.DEFAULT_CONTEXTUAL_COMMANDS = [
"undo",
"redo",
"repeat",
"cut [implicitElement]",
"copy [implicitElement]",
"paste before[implicitElement]",
"paste toOrInto",
"paste after[implicitElement]",
"delete [implicitElement]",
"replace [implicitElement]",
"insert before[implicitElement]",
"insert into",
"insert after[implicitElement]",
"convert [implicitElement]",
"wrap [implicitElement]",
"editAttributes [implicitElement]",
"listRepeatable",
"copyChars [separateParagraphs]",
"split",
"join",
"insertNode commentBefore[implicitElement]",
"insertNode commentInto",
"insertNode commentAfter[implicitElement]",
"commentOut",
"commentOut uncomment[implicitNode]",
"insertNode piBefore[implicitElement]",
"insertNode piInto",
"insertNode piAfter[implicitElement]",
"editPITarget [implicitNode]",
"remark",
"remark delete",
"remark deleteAll",
"remark previous",
"remark next",
"declareNamespace",
"setStyleSheet",
];
export class NewDocumentDialog extends XUI.Dialog {
static showDialog(xmlEditor) {
return new Promise((resolve, reject) => {
let dialog = new NewDocumentDialog(xmlEditor, resolve);
dialog.open("center", xmlEditor);
});
}
constructor(xmlEditor, resolve) {
super({ title: "New Document",
movable: true, resizable: true, closeable: true,
template: NewDocumentDialog.TEMPLATE,
buttons: [ { label: "Cancel", action: "cancelAction" },
{ label: "OK", action: "okAction",
default: true } ]});
this._xmlEditor = xmlEditor;
this._resolve = resolve;
let content = this.contentPane.firstElementChild;
this._lists = content.lastElementChild.lastElementChild;
this._templateSelected = this.templateSelected.bind(this);
this._templateAccepted = this.templateAccepted.bind(this);
this.initLists();
}
templateSelected(event) {
XUI.Util.consumeEvent(event);
let item = this._lists.querySelector(".xxe-newdoc-selected");
if (item !== null) {
item.classList.remove("xxe-newdoc-selected");
}
event.target.classList.add("xxe-newdoc-selected");
}
templateAccepted(event) {
XUI.Util.consumeEvent(event);
this.okAction();
}
initLists() {
return this._xmlEditor.listDocumentTemplates()
.then((templateLists) => {
this.setLists(templateLists);
return true;
},
(error) => {
XUI.Alert.showError(
`Cannot list document templates:\n${error}`,
this._xmlEditor);
return false;
});
}
setLists(templateLists) {
XUI.Util.removeAllChildren(this._lists);
for (let item of templateLists) {
this.appendList(item,  null, this._lists);
}
}
appendList(item, categories, parent) {
if (typeof item === "string") {
let div = document.createElement("div");
div.setAttribute("class", "xxe-newdoc-template");
if (categories !== null) {
div.setAttribute("data-category", categories);
}
div.textContent = item;
parent.appendChild(div);
div.onclick = this._templateSelected;
div.ondblclick = this._templateAccepted;
} else {
let category = item.shift();
let expanded = false;
if (category.startsWith('+')) {
expanded = true;
}
category = category.substring(1);
let details = document.createElement("details");
details.setAttribute("class", "xxe-newdoc-category");
if (expanded) {
details.setAttribute("open", "open");
}
parent.appendChild(details);
let summary = document.createElement("summary");
summary.textContent = category;
details.appendChild(summary);
let categories2 = (categories === null)? category :
(categories + "/" + category);
for (let subitem of item) {
this.appendList(subitem, categories2, details);
}
}
}
dialogClosed(result) {
this._resolve(result);
}
cancelAction() {
this.close(null);
}
okAction() {
let item = this._lists.querySelector(".xxe-newdoc-selected");
if (item === null) {
return;
}
let category = item.getAttribute("data-category");
let template = item.textContent;
this.close([category, template]);
}
}
NewDocumentDialog.TEMPLATE = document.createElement("template");
NewDocumentDialog.TEMPLATE.innerHTML = `
<div class="xxe-newdoc-content">
  <div class="xxe-newdoc-icon xui-newDocument-32"></div>
  <div class="xxe-newdoc-pane">
    <div class="xxe-newdoc-label">Document templates:</div>
    <div class="xxe-newdoc-lists" tabindex="0"></div>
  </div>
</div>
`;
export class AutoSave {
constructor(xmlEditor, mode, intervalMillis) {
this._xmlEditor = xmlEditor;
if (intervalMillis < 10000) {
intervalMillis = 10000;
}
this._intervalMillis = intervalMillis;
this._saveTimer = null;
if (!mode ||
!(mode === "local" || mode === "remote" || mode === "both")) {
mode = "remote";
}
if (!FSAccess.isAvailable() &&
(mode === "both" || mode === "local")) {
mode = (mode === "local")? null : "remote";
}
this._mode = mode;
this._docOpenedHandler = this._docClosedHandler = null;
if (this._mode === null) {
return;
}
this._docOpenedHandler = this.onDocumentOpened.bind(this);
xmlEditor.addEventListener("documentCreated", this._docOpenedHandler);
xmlEditor.addEventListener("documentOpened", this._docOpenedHandler);
xmlEditor.addEventListener("documentRecovered", this._docOpenedHandler);
this._docClosedHandler = this.onDocumentClosed.bind(this);
xmlEditor.addEventListener("documentClosed", this._docClosedHandler);
if (this._xmlEditor.documentIsOpened) {
this.onDocumentOpened( null);
}
}
dispose() {
if (this._docOpenedHandler !== null) {
this._xmlEditor.removeEventListener("documentCreated",
this._docOpenedHandler);
this._xmlEditor.removeEventListener("documentOpened",
this._docOpenedHandler);
this._xmlEditor.removeEventListener("documentRecovered",
this._docOpenedHandler);
this._docOpenedHandler = null;
}
if (this._docClosedHandler !== null) {
this._xmlEditor.removeEventListener("documentClosed",
this._docClosedHandler);
this._docClosedHandler = null;
}
this.cancelSaveTimer();
}
cancelSaveTimer() {
if (this._saveTimer !== null) {
clearInterval(this._saveTimer);
this._saveTimer = null;
}
}
get xmlEditor() {
return this._xmlEditor;
}
get mode() {
return this._mode;
}
get intervalMillis() {
return this._intervalMillis;
}
onDocumentOpened(event) {
this.cancelSaveTimer();
if (this.activable) {
this._saveTimer = setInterval(() => {
this.saveDocument();
}, this._intervalMillis);
}
}
get activable() {
return (this._mode !== null &&
this._xmlEditor.documentIsOpened &&
!this._xmlEditor.readOnlyDocument &&
(this._xmlEditor.isRemoteFile &&
(this._mode === "both" || this._mode === "remote")) ||
(!this._xmlEditor.isRemoteFile &&
(this._mode === "both" || this._mode === "local")));
}
saveDocument() {
if (!this._xmlEditor.connected ||
!this._xmlEditor.documentIsOpened ||
!this._xmlEditor.saveNeeded) {
return;
}
if (this._xmlEditor.saveAsNeeded) {
this._xmlEditor.showStatus(
"No autosave: \"Save As\" needed first.");
return;
}
const remote = this._xmlEditor.isRemoteFile;
if (remote) {
this.doSaveDocument(true);
return;
}
if (this._xmlEditor.documentFileHandle === null) {
this._xmlEditor.showStatus(
"No autosave: interactive \"Save\" needed first.");
return;
}
this._xmlEditor.documentFileHandle.queryPermission({mode: "readwrite"})
.then((status) => {
if (status === "granted") {
this.doSaveDocument(false);
} else {
this._xmlEditor.showStatus(
"No autosave: interactive \"Save\" needed first.");
}
})
.catch((error) => {
this._xmlEditor.showStatus(`Autosave error: ${error}`);
});
}
doSaveDocument(remote) {
XMLEditorApp.doSaveDocument(this._xmlEditor, remote)
.then((saved) => {
this._xmlEditor.showStatus(saved? "Autosave." :
"No autosave: unknown reason.");
})
.catch((error) => {
this._xmlEditor.showStatus(`Autosave error: ${error}`);
});
}
onDocumentClosed(event) {
this.cancelSaveTimer();
}
}
export class XMLEditorApp extends HTMLElement {
constructor() {
super();
this._indicator = null;
this._title = null;
this._menuButton = null;
this._onButtonMenuItemSelected =
this.onButtonMenuItemSelected.bind(this);
this._newButton = null;
this._openButton = null;
this._saveButton = null;
this._saveAsButton = null;
this._closeButton = null;
this._xmlEditor = null;
this._autoSave = null;
this._checkLeaveApp = false;
this._onBeforeUnload = this.onBeforeUnload.bind(this);
this._onAnyRequest = this.onAnyRequest.bind(this);
this._onAnyEvent = this.onAnyEvent.bind(this);
}
connectedCallback() {
const firstChild = this.firstElementChild;
let cls = null;
if (firstChild === null ||
(cls = firstChild.getAttribute("class")) === null ||
cls.indexOf("xxe-app-") < 0) {
if (!browserEngineIsSupported()) {
let reason = `<strong>Sorry but your web browser
is not supported.</strong><br /><br />
In order to be supported, your web browser must run on <em>desktop</em>
computer and must use the same browser engine as <em>recent</em> versions
of <em>Chrome</em> or <em>Firefox</em>.`;
const userAgent = window.navigator.userAgent;
if (userAgent) {
reason += `<br />
<small>(Your web browser identifies itself as:
"<code>${userAgent}</code>".)</small>`;
}
if (firstChild === null) {
XUI.Util.removeAllChildren(this);
const para = document.createElement("p");
para.innerHTML = reason;
this.appendChild(para);
} else if (firstChild.classList.contains("xxe-not-supported")) {
firstChild.innerHTML = reason;
}
return;
}
XUI.Util.removeAllChildren(this);
}
if (this.firstChild === null) {
this.appendChild(XMLEditorApp.TEMPLATE.content.cloneNode(true));
let div = this.firstElementChild;
let children = div.children;
this._indicator = children.item(0);
this._indicator.onclick = this.onIndicatorClick.bind(this);
this._title = children.item(1);
this._menuButton = children.item(2);
this._menuButton.textContent = XUI.StockIcon["menu"];
this._menuButton.onclick = this.onMenuButtonClick.bind(this);
children = div.nextElementSibling.children;
this._newButton = children.item(0);
this._newButton.onclick = this.onNewButtonClick.bind(this);
this._openButton = children.item(1);
this._openButton.onclick = this.onOpenButtonClick.bind(this);
this._saveButton = children.item(2);
this._saveButton.onclick = this.onSaveButtonClick.bind(this);
this._saveAsButton = children.item(3);
this._saveAsButton.onclick = this.onSaveAsButtonClick.bind(this);
this._closeButton = children.item(4);
this._closeButton.onclick = this.onCloseButtonClick.bind(this);
this.documentStorage = this.getAttribute("documentstorage");
const xxe = this.lastElementChild;
this._xmlEditor = xxe;
xxe.resourceStorage = new LocalFiles(xxe);
xxe.addEventListener("connected", this.onConnected.bind(this));
xxe.addEventListener("disconnected",
this.onDisconnected.bind(this));
const docOpenedHandler = this.onDocumentOpened.bind(this);
xxe.addEventListener("documentCreated", docOpenedHandler);
xxe.addEventListener("documentOpened", docOpenedHandler);
xxe.addEventListener("documentRecovered", docOpenedHandler);
xxe.addEventListener("documentSavedAs",
this.onDocumentSavedAs.bind(this));
xxe.addEventListener("documentClosed",
this.onDocumentClosed.bind(this));
xxe.addEventListener("saveStateChanged",
this.onSaveStateChanged.bind(this));
xxe.addEventListener("readOnlyStateChanged",
this.onReadOnlyStateChanged.bind(this));
this.onDisconnected( null);
this.onDocumentClosed( null);
window.addEventListener("unhandledrejection", (event) => {
XUI.Alert.showWarning(`SHOULD NOT HAPPEN: \
unhandled promise rejection:\n${event.reason}`, xxe);
});
this.serverURL = this.getAttribute("serverurl");
this.clientProperties = this.getAttribute("clientproperties");
this.autoRecover = this.hasAttribute("autorecover")?
(this.getAttribute("autorecover") === "true") : true;
this.checkLeaveApp = this.hasAttribute("checkleaveapp")?
(this.getAttribute("checkleaveapp") === "true") : true;
this.button2PastesText =
(this.getAttribute("button2pastestext") === "true");
this.initAutoSave();
}
}
get documentStorage() {
return this.getAttribute("documentstorage");
}
set documentStorage(storage) {
if (storage) {
storage = storage.trim();
}
if (!storage ||
!(storage === "local" || storage === "remote" ||
storage === "both")) {
storage = "local";
}
if (storage !== this.getAttribute("documentstorage")) {
this.setAttribute("documentstorage", storage);
}
const buttons = [this._newButton, this._openButton];
for (let button of buttons) {
let detail = button.lastElementChild;
let newDetail = null;
switch (storage) {
case "local":
case "remote":
if (detail.classList.contains("xui-small-icon")) {
newDetail = document.createElement("span");
newDetail.appendChild(document.createTextNode("\u2026"));
}
break;
default:
if (!detail.classList.contains("xui-small-icon")) {
newDetail = document.createElement("span");
newDetail.setAttribute("class",
"xui-small-icon xxe-app-button-menu");
newDetail.appendChild(
document.createTextNode(XUI.StockIcon["down-dir"]));
}
break;
}
if (newDetail !== null) {
button.replaceChild(newDetail, detail);
}
}
}
get serverURL() {
return this.getAttribute("serverurl");
}
set serverURL(url) {
this._xmlEditor.serverURL = url;
url = this._xmlEditor.serverURL;
if (url !== this.getAttribute("serverurl")) {
this.setAttribute("serverurl", url);
}
}
get clientProperties() {
let props = this.getAttribute("clientproperties");
if (props !== null && (props = props.trim()).length === 0) {
props = null;
}
return props;
}
set clientProperties(props) {
this._xmlEditor.clientProperties = props;
props = this._xmlEditor.clientProperties;
if (props === null) {
this.removeAttribute("clientproperties");
} else {
this.setAttribute("clientproperties", props);
}
}
get autoRecover() {
return this.getAttribute("autorecover") === "true";
}
set autoRecover(recover) {
this._xmlEditor.autoRecover = recover;
const recoverValue = recover? "true" : "false";
if (recoverValue !== this.getAttribute("autorecover")) {
this.setAttribute("autorecover", recoverValue);
}
}
get checkLeaveApp() {
return this._checkLeaveApp;
}
set checkLeaveApp(check) {
this._checkLeaveApp = check;
this.setBeforeUnloadListener(check && this._xmlEditor &&
this._xmlEditor.saveNeeded);
const checkValue = check? "true" : "false";
if (checkValue !== this.getAttribute("checkleaveapp")) {
this.setAttribute("checkleaveapp", checkValue);
}
}
setBeforeUnloadListener(add) {
if (add) {
window.addEventListener("beforeunload", this._onBeforeUnload);
} else {
window.removeEventListener("beforeunload", this._onBeforeUnload);
}
}
onBeforeUnload(event) {
if (this._xmlEditor && this._xmlEditor.saveNeeded) {
event.preventDefault();
return (event.returnValue = true);
}
}
setLeaveAppChecker() {
if (this._checkLeaveApp) {
this.setBeforeUnloadListener(this._xmlEditor.saveNeeded);
}
}
get button2PastesText() {
return this.getAttribute("button2pastestext") === "true";
}
set button2PastesText(pastes) {
this._xmlEditor.button2PastesText = pastes;
const pastesValue = pastes? "true" : "false";
if (pastesValue !== this.getAttribute("button2pastestext")) {
this.setAttribute("button2pastestext", pastesValue);
}
}
initAutoSave() {
let spec = this.autoSave;
let [ mode, interval, intervalMs, enabled ] =
XMLEditorApp.parseAutoSave(spec);
if (mode === null) {
spec = null;
} else {
enabled = XMLEditorApp.getPreference("autoSave", enabled);
spec = `${mode} ${interval} ${enabled? "on" : "off"}`;
}
this.autoSave = spec;
}
static parseAutoSave(spec) {
let mode = null;
let interval = null;
let intervalMs = -1;
let enabled = false;
if (spec) {
spec = spec.trim();
for (let m of [ "both", "remote", "local" ]) {
if (spec.startsWith(m)) {
mode = m;
interval = spec.substring(m.length).trim();
if (interval.endsWith("off")) {
enabled = false;
interval =
interval.substring(0, interval.length-3).trim();
} else if (interval.endsWith("on")) {
enabled = true;
interval =
interval.substring(0, interval.length-2).trim();
} else {
enabled = true;
}
break;
}
}
if (!FSAccess.isAvailable() &&
(mode === "both" || mode === "local")) {
mode = (mode === "local")? null : "remote";
}
if (mode !== null) {
if (!interval) {
interval = "30s";
}
let intervalMs = parseFloat(interval);
if (!isNaN(intervalMs) && intervalMs > 0) {
if (interval.endsWith("s")) {
intervalMs *= 1000;
} else if (interval.endsWith("m")) {
intervalMs *= 1000 * 60;
} else if (interval.endsWith("h")) {
intervalMs *= 1000 * 60 * 60;
} else {
intervalMs = -1;
}
} else {
intervalMs = -1;
}
if (intervalMs <= 0) {
mode = null;
} else {
if (intervalMs < 10000) {
intervalMs = 10000;
interval = "10s";
}
}
}
}
return [ mode, interval, intervalMs, enabled ];
}
get autoSave() {
return this.getAttribute("autosave");
}
set autoSave(spec) {
if (this._autoSave !== null) {
this._autoSave.dispose();
this._autoSave = null;
}
let [ mode, interval, intervalMs, enabled ] =
XMLEditorApp.parseAutoSave(spec);
if (mode === null) {
this.removeAttribute("autosave");
XMLEditorApp.setPreference("autoSave", null);
} else {
spec = `${mode} ${interval} ${enabled? "on" : "off"}`;
this.setAttribute("autosave", spec);
XMLEditorApp.setPreference("autoSave", enabled);
if (enabled) {
this._autoSave = new AutoSave(this._xmlEditor, mode, intervalMs);
}
}
}
static getPreference(key, defaultValue) {
let prefs = XMLEditorApp.getPreferences();
if (key in prefs) {
return prefs[key];
} else {
return defaultValue;
}
}
static getPreferences() {
let prefs = null;
let data = window.localStorage.getItem("XXE.XMLEditorApp.preferences");
if (data !== null) {
prefs = JSON.parse(data);
}
if (prefs === null || typeof prefs !== "object") {
prefs = {};
}
return prefs;
}
static setPreference(key, value) {
let prefs = XMLEditorApp.getPreferences();
if (value === null) {
delete prefs[key];
} else {
prefs[key] = value;
}
if (Object.keys(prefs).length === 0) {
window.localStorage.removeItem("XXE.XMLEditorApp.preferences");
} else {
window.localStorage.setItem("XXE.XMLEditorApp.preferences",
JSON.stringify(prefs));
}
}
onIndicatorClick(event) {
const mod = PLATFORM_IS_MAC_OS? event.metaKey : event.ctrlKey;
if (mod) {
if (this._indicator.classList.contains("xxe-app-logging")) {
this._indicator.classList.remove("xxe-app-logging");
this._xmlEditor.removeRequestListener(this._onAnyRequest);
for (let eventType of XMLEditor.EVENT_TYPES) {
this._xmlEditor.removeEventListener(eventType,
this._onAnyEvent);
}
} else {
this._indicator.classList.add("xxe-app-logging");
this._xmlEditor.addRequestListener(this._onAnyRequest);
for (let eventType of XMLEditor.EVENT_TYPES) {
this._xmlEditor.addEventListener(eventType,
this._onAnyEvent);
}
}
}
}
onAnyRequest(autoConnect, requestName, requestArgs, response) {
let reqInfo;
if (response !== undefined) {
reqInfo = "\u25C0 " + requestName;
} else {
if (autoConnect) {
reqInfo = "autoConnect\u25B6 ";
} else {
reqInfo = "\u25B6 ";
}
reqInfo += requestName;
}
reqInfo += JSON.stringify(requestArgs, XMLEditorApp.jsonReplacer, 4);
if (response !== undefined) {
reqInfo += " \u2192 ";
if (response instanceof Error) {
reqInfo += "Error: " + response.toString();
} else {
reqInfo +=
JSON.stringify(response, XMLEditorApp.jsonReplacer, 4);
}
}
this._xmlEditor.showStatus(reqInfo,  true);
}
static jsonReplacer(key, value) {
if (value instanceof Blob) {
let info = "(" + value.size + " bytes";
if (value.type) {
info += ";" + value.type;
}
info += ")";
return info;
} else if (value instanceof ArrayBuffer ||
ArrayBuffer.isView(value)) {
return "[" + value.byteLength + " bytes]";
} else {
return value;
}
}
onAnyEvent(event) {
let text = event.toString();
if (text.length > 10000) {
text = text.substring(0, 5000) + "\u2026" +
text.substring(text.length-4999);
}
this._xmlEditor.showStatus("\u25B2 " + text,  true);
}
onMenuButtonClick(event) {
const menuItems = [
{ type: "submenu", text: "Options", name: "optionsMenu",
items: [
{ type: "checkbox", text: "Autosave",
name: "autoSaveToggle", selected: false, enabled: false }
] },
{ separator: true,
text: "Help", name: "help" },
{ separator: true,
text: "Show Element Reference", name: "elementReference" },
{ text: "Show Content Model", name: "contentModel" },
{ separator: true,
text: "Mouse and Keyboard Bindings", name: "bindings" },
{ separator: true,
text: `About ${XMLEditorApp.NAME}`, name: "about" }
];
let autoSaving = this.autoSave;
if (autoSaving !== null) {
const optionItems = menuItems[0].items;
optionItems[0].enabled = true;
if (autoSaving.endsWith("on")) {
optionItems[0].selected = true;
}
}
if (this._xmlEditor.documentIsOpened) {
if (!this._xmlEditor.configurationName) {
menuItems[2].enabled = false;
}
} else {
menuItems[2].enabled = false;
menuItems[3].enabled = false;
menuItems[4].enabled = false;
}
this.showButtonMenu(menuItems, this._menuButton, "comboboxmenu");
}
showButtonMenu(menuItems, button, position="menu") {
const menu = XUI.Menu.create(menuItems);
menu.addEventListener("menuitemselected",
this._onButtonMenuItemSelected);
menu.open(position, button);
}
onButtonMenuItemSelected(event) {
switch (event.detail.menuItem.name) {
case "newDocument":
XMLEditorApp.newDocument(this._xmlEditor);
break;
case "newRemoteFile":
XMLEditorApp.newRemoteFile(this._xmlEditor);
break;
case "openDocument":
XMLEditorApp.openDocument(this._xmlEditor);
break;
case "openRemoteFile":
XMLEditorApp.openRemoteFile(this._xmlEditor);
break;
case "autoSaveToggle":
this.toggleAutoSave();
break;
case "help":
this.showHelp();
break;
case "elementReference":
this.showElementReference();
break;
case "contentModel":
this.showContentModel();
break;
case "bindings":
XUI.Alert.showInfo(this.getBindingsText(), this);
break;
case "about":
XUI.Alert.showInfo(XMLEditorApp.ABOUT, this);
break;
}
}
toggleAutoSave() {
let autoSaving = this.autoSave;
if (autoSaving !== null) {
let toggled = false;
if (autoSaving.endsWith("on")) {
autoSaving =
autoSaving.substring(0, autoSaving.length-2) + "off";
toggled = true;
} else if (autoSaving.endsWith("off")) {
autoSaving =
autoSaving.substring(0, autoSaving.length-3) + "on";
toggled = true;
}
if (toggled) {
this.autoSave = autoSaving;
if (this._xmlEditor.documentIsOpened) {
this.showAutoSaving(autoSaving.endsWith("on"));
}
}
}
}
showHelp() {
const helpURL =
"https://www.xmlmind.com/xmleditor/_web/doc/manual/wh/basics.html";
window.open(helpURL, "xxewOnlineHelp");
}
showElementReference() {
const helpWin = window.open("", "xxewElementReference");
if (!helpWin) {
docView.showStatus(
"Could not open window containing element reference.",
true);
return;
}
helpWin.focus();
const docView = this._xmlEditor.documentView;
docView.executeCommand(EXECUTE_NORMAL, "showElementReference", null)
.then((result) => {
if (CommandResult.isDone(result) && result.value !== null) {
helpWin.location = result.value;
} else {
docView.showStatus("Element reference not available.",
true);
helpWin.close();
}
});
}
showContentModel() {
const docView = this._xmlEditor.documentView;
docView.executeCommand(EXECUTE_NORMAL, "showContentModel", null)
.then((result) => {
if (result === null ||
result.status === COMMAND_RESULT_FAILED) {
docView.showStatus(
"Cannot show the content model of selected element.",
true);
}
});
}
getBindingsText() {
const bindings = this._xmlEditor.documentView.bindings;
if (bindings === null) {
return "???";
}
let rows = [];
let rows2 = [];
for (let binding of bindings) {
let row = [ binding.getUserInputLabel(),
binding.commandName, binding.commandParams ];
if (binding.userInput instanceof AppEvent) {
rows2.push(row);
} else {
rows.push(row);
}
}
rows.sort((r1, r2) => { return r1[0].localeCompare(r2[0], "en"); });
if (rows2.length > 0) {
rows2.sort((r1, r2) => {return r1[0].localeCompare(r2[0], "en");});
rows.push(...rows2);
}
let html = XMLEditorApp.BINDINGS;
let pos = html.indexOf("</tbody>");
if (pos > 0) {
const tdStartTag = `<td ${XMLEditorApp.BINDINGS_TD_STYLE}>`;
let tr = "";
let trCount = 0;
for (let row of rows) {
tr += "<tr";
if (trCount % 2 === 1) {
tr += " style=\"background-color:#FFFFE0;\"";
}
tr += ">\n";
tr += tdStartTag;
tr += XUI.Util.escapeHTML(row[0]);
tr += "</td>";
tr += tdStartTag;
tr += XUI.Util.escapeHTML(row[1]);
tr += "</td>";
tr += tdStartTag;
if (row[2] === null) {
tr += "\u00A0";
} else {
tr += XUI.Util.escapeHTML(row[2]);
}
tr += "</td>";
tr += "</tr>\n";
++trCount;
}
html = html.substring(0, pos) + tr + html.substring(pos);
}
return html;
}
onNewButtonClick(event) {
switch (this.documentStorage) {
case "local":
XMLEditorApp.newDocument(this._xmlEditor);
break;
case "remote":
XMLEditorApp.newRemoteFile(this._xmlEditor);
break;
default:
{
const menuItems = [
{ text: "New Local Document\u2026", name: "newDocument" },
{ text: "New Remote Document\u2026", name: "newRemoteFile" }
];
this.showButtonMenu(menuItems, this._newButton);
}
break;
}
}
onOpenButtonClick(event) {
switch (this.documentStorage) {
case "local":
XMLEditorApp.openDocument(this._xmlEditor);
break;
case "remote":
XMLEditorApp.openRemoteFile(this._xmlEditor);
break;
default:
{
const menuItems = [
{ text: "Open Local Document\u2026", name: "openDocument" },
{ text: "Open Remote Document\u2026", name: "openRemoteFile" }
];
this.showButtonMenu(menuItems, this._openButton);
}
break;
}
}
onSaveButtonClick(event) {
XMLEditorApp.saveDocument(this._xmlEditor)
.then((saved) => {
if (saved) {
this._xmlEditor.showStatus(
`Document saved to "${this._xmlEditor.documentURI}".`);
}
});
}
onSaveAsButtonClick(event) {
XMLEditorApp.saveDocumentAs(this._xmlEditor)
.then((savedAs) => {
if (savedAs) {
this._xmlEditor.showStatus(
`Document saved to "${this._xmlEditor.documentURI}".`);
}
});
}
onCloseButtonClick(event) {
XMLEditorApp.closeDocument(this._xmlEditor);
}
onConnected(event) {
this._indicator.classList.add("xxe-app-connected");
this._indicator.setAttribute("title", `Connected to
${this._xmlEditor.serverURL}
(${(PLATFORM_IS_MAC_OS? "Command" : "Ctrl")}-click to toggle logging \
requests, responses and events.)`);
}
onDisconnected(event) {
this._indicator.classList.remove("xxe-app-connected");
this._indicator.setAttribute("title", `Disconnected from
${this._xmlEditor.serverURL}
(${(PLATFORM_IS_MAC_OS? "Command" : "Ctrl")}-click to toggle logging \
requests, responses and events.)`);
this.enableButtons();
}
onDocumentOpened(event) {
this.showAutoSaving(true);
this.removeSaveHint();
this.addSaveHint();
this.updateTitle();
this.enableButtons();
this.setLeaveAppChecker();
}
showAutoSaving(show) {
let saveIcon = this._saveButton.firstElementChild;
if (show && this._autoSave !== null && this._autoSave.activable) {
saveIcon.classList.replace("xui-saveDocument-16",
"xui-autoSaveDocument-16");
saveIcon.setAttribute("title", "Autosave enabled.");
} else {
saveIcon.classList.replace("xui-autoSaveDocument-16",
"xui-saveDocument-16");
saveIcon.removeAttribute("title");
}
}
addSaveHint() {
if (!this._xmlEditor.isRemoteFile) {
let tooltip;
let addClass = null;
if (FSAccess.isAvailable()) {
tooltip = `Please note that \
the first time you'll use this "Save" button,\n\
you may be asked to grant your permission to save this file to disk.`
addClass = "xxe-app-save-info";
} else {
tooltip = `Please note that \
DIRECTLY SAVING A FILE TO DISK IS NOT SUPPORTED\n\
by this browser. Therefore "Save" is here equivalent to "Save As".`
addClass = "xxe-app-save-warn";
}
let saveHint = XMLEditorApp.SAVE_HINT_TEMPLATE.content
.cloneNode(true).firstElementChild;
saveHint.textContent = XUI.StockIcon["comment"];
saveHint.classList.add(addClass);
this._saveButton.appendChild(saveHint);
this._saveButton.setAttribute("title", tooltip);
}
}
removeSaveHint() {
let saveHint = this._saveButton.querySelector(".xxe-app-save-hint");
if (saveHint !== null) {
this._saveButton.removeChild(saveHint);
this._saveButton.removeAttribute("title");
}
}
onDocumentSavedAs(event) {
this.updateTitle();
this.enableButtons();
}
onDocumentClosed(event) {
this.updateTitle();
this.enableButtons();
this.setLeaveAppChecker();
this.showAutoSaving(false);
this.removeSaveHint();
}
onSaveStateChanged(event) {
this.updateTitle();
this.enableButtons();
this.setLeaveAppChecker();
}
onReadOnlyStateChanged(event) {
this.updateTitle();
}
updateTitle() {
let title = XMLEditorApp.TITLE;
let style = "bold";
if (this._xmlEditor.documentIsOpened) {
title = this._xmlEditor.documentURI;
style = "normal";
if (this._xmlEditor.saveNeeded) {
title += " (modified)";
}
if (this._xmlEditor.readOnlyDocument) {
title += " (read-only)";
}
}
if (title !== this._title.textContent) {
this._title.textContent = title;
this._title.style.fontWeight = style;
}
}
enableButtons() {
const connected = this._xmlEditor.connected;
const docOpened = this._xmlEditor.documentIsOpened;
XMLEditorApp.enableButton(this._saveButton,
connected && docOpened &&
this._xmlEditor.saveNeeded);
XMLEditorApp.enableButton(this._saveAsButton, connected && docOpened);
XMLEditorApp.enableButton(this._closeButton, connected && docOpened);
}
static enableButton(button, enabled) {
if (enabled) {
if (button.hasAttribute("disabled")) {
button.removeAttribute("disabled");
button.classList.remove("xui-control-disabled");
}
} else {
if (!button.hasAttribute("disabled")) {
button.setAttribute("disabled", "disabled");
button.classList.add("xui-control-disabled");
}
}
}
get xmlEditor() {
return this._xmlEditor;
}
static newDocument(xmlEditor) {
return XMLEditorApp.doNewDocument(xmlEditor,  false);
}
static newRemoteFile(xmlEditor) {
return XMLEditorApp.doNewDocument(xmlEditor,  true);
}
static doNewDocument(xmlEditor, remote) {
let selectedTemplate = [];
return XMLEditorApp.confirmDiscardChanges(xmlEditor)
.then((confirmed) => {
if (confirmed) {
return NewDocumentDialog.showDialog(xmlEditor);
} else {
return null;
}
})
.then((tmpl) => {
if (tmpl === null) {
return false;
} else {
selectedTemplate.push(...tmpl);
return XMLEditorApp.doCloseDocument(xmlEditor);
}
})
.then((closed) => {
if (closed) {
let [category, template] = selectedTemplate;
if (remote) {
return xmlEditor.newRemoteFile(category, template);
} else {
return xmlEditor.newDocument(category, template);
}
} else {
return false;
}
})
.catch((error) => {
XUI.Alert.showError(`Cannot create document:\n${error}`,
xmlEditor);
return false;
});
}
static confirmDiscardChanges(xmlEditor) {
if (!xmlEditor.documentIsOpened || !xmlEditor.saveNeeded) {
return Promise.resolve(true);
}
return XUI.Confirm.showConfirm(
`"${xmlEditor.documentURI}" has been modified\nDiscard changes?`,
xmlEditor);
}
static openDocument(xmlEditor) {
return XMLEditorApp.doOpenDocument(xmlEditor,  false);
}
static openRemoteFile(xmlEditor) {
return XMLEditorApp.doOpenDocument(xmlEditor,  true);
}
static doOpenDocument(xmlEditor, remote) {
const options = {
title: "Open Document",
extensions: XMLEditorApp.ACCEPTED_FILE_EXTENSIONS,
option: [ "readOnly", false,
"Open corresponding document in read-only mode" ]
};
let chosenFile = {};
return XMLEditorApp.confirmDiscardChanges(xmlEditor)
.then((confirmed) => {
if (confirmed) {
if (remote) {
return RemoteFileDialog.showDialog(xmlEditor, options);
} else {
return LocalFileDialog.showDialog(xmlEditor, options);
}
} else {
return null;
}
})
.then((choice) => {
if (choice === null) {
return false;
} else {
Object.assign(chosenFile, choice);
return XMLEditorApp.doCloseDocument(xmlEditor);
}
})
.then((closed) => {
if (closed) {
let readOnly =
!chosenFile.readOnly? false : chosenFile.readOnly;
if (remote) {
return xmlEditor.openRemoteFile(chosenFile.uri,
readOnly);
} else {
return xmlEditor.openDocument(chosenFile.file,
chosenFile.uri, readOnly);
}
} else {
return false;
}
})
.then((opened) => {
if (opened) {
if (!remote && chosenFile.file.handle) {
xmlEditor.documentFileHandle = chosenFile.file.handle;
} else {
xmlEditor.documentFileHandle = null;
}
}
return opened;
})
.catch((error) => {
XUI.Alert.showError(`Cannot open document:\n${error}`,
xmlEditor);
return false;
});
}
static saveDocument(xmlEditor) {
if (!xmlEditor.documentIsOpened || !xmlEditor.saveNeeded) {
return Promise.resolve(false);
}
const remote = xmlEditor.isRemoteFile;
if (xmlEditor.saveAsNeeded ||
(!remote && xmlEditor.documentFileHandle === null)) {
return XMLEditorApp.saveDocumentAs(xmlEditor);
}
return XMLEditorApp.doSaveDocument(xmlEditor, remote)
.catch((error) => {
XUI.Alert.showError(`Could not save document:\n${error}`,
xmlEditor);
return false;
});
}
static doSaveDocument(xmlEditor, remote) {
return XMLEditorApp.getDocumentContent(xmlEditor, remote)
.then((docContent) => {
if (remote) {
return null;
} else {
return FSAccess.fileSave(docContent,  {},
xmlEditor.documentFileHandle,
true);
}
})
.then((savedFileHandle) => {
if (!remote &&
savedFileHandle !== xmlEditor.documentFileHandle) {
throw new Error(`INTERNAL ERROR: expected \
document file handle=${xmlEditor.documentFileHandle}, \
got handle=${savedFileHandle}`);
}
return xmlEditor.saveDocument();
})
}
static getDocumentContent(xmlEditor, remote) {
if (remote) {
return Promise.resolve(null);
}
return xmlEditor.getDocument()
.then((xmlSource) => {
if (xmlSource) {
xmlSource = xmlSource.replace(
/encoding=(('[^']*')|("[^"]*"))\s*\?>/,
"encoding=\"UTF-8\"?>");
return new Blob([ xmlSource ]);
} else {
throw new Error("no document content");
}
});
}
static saveDocumentAs(xmlEditor) {
const remote = xmlEditor.isRemoteFile;
const options = {
title: "Save Document",
extensions: XMLEditorApp.ACCEPTED_FILE_EXTENSIONS,
templateURI: xmlEditor.documentURI
};
let chosenFile = {};
return XMLEditorApp.getDocumentContent(xmlEditor, remote)
.then((docContent) => {
if (remote) {
return RemoteFileDialog.showDialog(xmlEditor, options,
true);
} else {
return LocalFileDialog.showDialog(xmlEditor, options,
docContent);
}
})
.then((choice) => {
if (choice === null) {
return false;
} else {
Object.assign(chosenFile, choice);
return xmlEditor.saveDocumentAs(chosenFile.uri);
}
})
.then((savedAs) => {
if (savedAs) {
if (!remote && chosenFile.fileHandle) {
xmlEditor.documentFileHandle = chosenFile.fileHandle;
} else {
xmlEditor.documentFileHandle = null;
}
}
return savedAs;
})
.catch((error) => {
let where = !chosenFile.uri? "" : ` "${chosenFile.uri}"`;
XUI.Alert.showError(`Cannot save document as${where}:\n${error}`,
xmlEditor);
return false;
});
}
static closeDocument(xmlEditor) {
return XMLEditorApp.confirmDiscardChanges(xmlEditor)
.then((confirmed) => {
if (confirmed) {
return XMLEditorApp.doCloseDocument(xmlEditor);
} else {
return false;
}
})
.catch((error) => {
XUI.Alert.showError(`Cannot close document:\n${error}`,
xmlEditor);
return false;
});
}
static doCloseDocument(xmlEditor) {
if (!xmlEditor.documentIsOpened) {
return Promise.resolve(true);
}
return xmlEditor.closeDocument( true);
}
}
XMLEditorApp.NAME = "XMLmind XML Editor";
XMLEditorApp.CLIENT_VERSION = "1.8.1";
XMLEditorApp.TITLE =
`${XMLEditorApp.NAME} Web Edition ${XMLEditorApp.CLIENT_VERSION}`;
XMLEditorApp.SERVER_VERSION = "11.2.0";
XMLEditorApp.ABOUT = `<html><h3 style="margin-top:0;">${XMLEditorApp.NAME}
<span style="font-size:smaller;">client ${XMLEditorApp.CLIENT_VERSION} / \
server ${XMLEditorApp.SERVER_VERSION}</span></h3>
<p>Copyright (c) 2017-${(new Date()).getFullYear()} XMLmind Software,
all rights reserved.</p>
<p><b style="color:maroon;">&#x2023; Product end-of-life planned for \
April 3, 2027.</b><br />
For more information, please visit<br />
www.xmlmind.com/xmleditor/</p>
`;
XMLEditorApp.BINDINGS_TD_STYLE =
"style=\"border:1px solid #C0C0C0;padding:0.25em 0.5em;\"";
XMLEditorApp.BINDINGS = `<html><h3 style="margin-top:0;">Mouse and keyboard \
bindings</h3>
<table style="border:1px solid #C0C0C0;border-collapse:collapse;">
  <thead style="background-color:#F0F0F0;">
    <tr>
      <th ${XMLEditorApp.BINDINGS_TD_STYLE}>User input</th>
      <th ${XMLEditorApp.BINDINGS_TD_STYLE}>Command</th>
      <th ${XMLEditorApp.BINDINGS_TD_STYLE}>Parameter</th>
    </tr>
  </thead>
  <tbody></tbody>
</table>
`;
XMLEditorApp.TEMPLATE = document.createElement("template");
XMLEditorApp.TEMPLATE.innerHTML = `
<div class="xxe-app-title-bar">
  <span class="xui-control xxe-app-indicator"></span>
  <span class="xxe-app-title">XMLmind XML Editor</span>
  <span class="xxe-tool-button xxe-app-menu"></span>
</div>
<div class="xxe-app-button-bar">
  <button type="button" class="xui-control xxe-app-button"><span
    class="xui-edit-icon-16
        xui-newDocument-16"></span><span>New</span><span>\u2026</span></button>
  <button type="button" class="xui-control xxe-app-button"><span
    class="xui-edit-icon-16
      xui-openDocument-16"></span><span>Open</span><span>\u2026</span></button>
  <button type="button" class="xui-control xxe-app-button"><span
    class="xui-edit-icon-16
           xui-saveDocument-16"></span><span>Save</span></button>
  <button type="button" class="xui-control xxe-app-button"><span
    class="xui-edit-icon-16
         xui-saveDocumentAs-16"></span><span>Save As\u2026</span></button>
  <button type="button" class="xui-control xxe-app-button"><span
    class="xui-edit-icon-16
           xui-closeDocument-16"></span><span>Close</span></button>
</div>
<xxe-client></xxe-client>
`;
XMLEditorApp.SAVE_HINT_TEMPLATE = document.createElement("template");
XMLEditorApp.SAVE_HINT_TEMPLATE.innerHTML = `
<span class="xui-small-icon xxe-app-save-hint"></span>
`;
XMLEditorApp.ACCEPTED_FILE_EXTENSIONS = [
["XML files",
"application/xml",
"xml"],
["DocBook files",
"application/docbook+xml",
"xml", "dbk", "docb"],
["XHTML files",
"application/xhtml+xml",
"xhtml", "xht",
"html", "shtml", "htm"],
["DITA files",
"application/dita+xml",
"dita", "ditamap",
"bookmap", "ditaval"],
["MathML files",
"application/mathml+xml",
"mml"]
];
window.customElements.define("xxe-app", XMLEditorApp);
