/*
 * 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.
 */

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);
}
export const StockIcon = {
"left-bold": "\uE795",
"right-bold": "\uE796",
"cancel": "\uE799",
"folder": "\uE800",
"ok": "\uE801",
"cancel-circled": "\uE802",
"info-circled": "\uE803",
"check": "\uE804",
"down-dir": "\uE805",
"up-dir": "\uE806",
"left-dir": "\uE807",
"right-dir": "\uE808",
"help-circled": "\uE809",
"attention": "\uE80A",
"attention-circled": "\uE80B",
"picture": "\uE80C",
"backspace": "\uE80D",
"minus-circled": "\uE80E",
"ok-circled": "\uE80F",
"cancel-squared": "\uE810",
"plus-circled": "\uE811",
"clipboard": "\uE812",
"folder-open": "\uE813",
"floppy": "\uE814",
"triangle-1-se": "\uE815",
"right-open": "\uE816",
"server": "\uE817",
"eye": "\uE818",
"home": "\uE819",
"drive": "\uE81A",
"comment": "\uE81B",
"plus": "\uE81C",
"up-bold": "\uE81D",
"minus": "\uE81E",
"up-open": "\uE81F",
"down-open": "\uE820",
"replace": "\uE821",
"menu": "\uE94D",
"replace-all": "\uEA9A",
"doc": "\uF04A",
"check-empty": "\uF096",
"plus-squared": "\uF0FE",
"computer": "\uF108",
"circle-empty": "\uF10C",
"reply": "\uF112",
"question": "\uF128",
"minus-squared": "\uF146",
"file": "\uF15B",
"dot-circled": "\uF192",
"settings": "\uF1DE",
"trash": "\uF1F8",
"toggle-off": "\uF204",
"toggle-on": "\uF205",
"hourglass": "\uF254",
"user": "\uF2BE",
};
export const EditIcon = {
"undo": "xui-undo-",
"redo": "xui-redo-",
"repeat": "xui-repeat-",
"commandHistory": "xui-commandHistory-",
"paste": "xui-paste-",
"delete": "xui-delete-",
"replace": "xui-replace-",
"insertBefore": "xui-insertBefore-",
"insert": "xui-insert-",
"insertAfter": "xui-insertAfter-",
"convert": "xui-convert-",
"wrap": "xui-wrap-",
"split": "xui-split-",
"join": "xui-join-",
"textSearchReplace": "xui-textSearchReplace-",
"declareNamespace": "xui-declareNamespace-",
"editAttributes": "xui-editAttributes-",
"insertOrEditRemark": "xui-insertOrEditRemark-",
"deleteRemark": "xui-deleteRemark-",
"deleteAllRemarks": "xui-deleteAllRemarks-",
"previousRemark": "xui-previousRemark-",
"nextRemark": "xui-nextRemark-",
"newDocument": "xui-newDocument-",
"openDocument": "xui-openDocument-",
"saveDocument": "xui-saveDocument-",
"autoSaveDocument": "xui-autoSaveDocument-",
"saveDocumentAs": "xui-saveDocumentAs-",
"closeDocument": "xui-closeDocument-",
"styles": "xui-styles-",
"addListItem": "xui-addListItem-",
"anchor": "xui-anchor-",
"bold": "xui-bold-",
"cancel": "xui-cancel-",
"cell": "xui-cell-",
"charCase": "xui-charCase-",
"column": "xui-column-",
"copy": "xui-copy-",
"cut": "xui-cut-",
"decrementColumnSpan": "xui-decrementColumnSpan-",
"decrementRowSpan": "xui-decrementRowSpan-",
"definitionList": "xui-definitionList-",
"deleteColumn": "xui-deleteColumn-",
"deleteRow": "xui-deleteRow-",
"demoteListItem": "xui-demoteListItem-",
"down": "xui-down-",
"editDocument": "xui-editDocument-",
"editDocuments": "xui-editDocuments-",
"figure": "xui-figure-",
"footnote": "xui-footnote-",
"formatTable": "xui-formatTable-",
"heading": "xui-heading-",
"incrementColumnSpan": "xui-incrementColumnSpan-",
"incrementRowSpan": "xui-incrementRowSpan-",
"insertColumnAfter": "xui-insertColumnAfter-",
"insertColumnBefore": "xui-insertColumnBefore-",
"insertRowAfter": "xui-insertRowAfter-",
"insertRowBefore": "xui-insertRowBefore-",
"italic": "xui-italic-",
"itemizedList": "xui-itemizedList-",
"left": "xui-left-",
"link": "xui-link-",
"list": "xui-list-",
"media": "xui-media-",
"menu": "xui-menu-",
"note": "xui-note-",
"orderedList_aa": "xui-orderedList_aa-",
"orderedList_A": "xui-orderedList_A-",
"orderedList_ii": "xui-orderedList_ii-",
"orderedList_I": "xui-orderedList_I-",
"orderedList": "xui-orderedList-",
"paragraph": "xui-paragraph-",
"pasteAfter": "xui-pasteAfter-",
"pasteBefore": "xui-pasteBefore-",
"plainText": "xui-plainText-",
"pre": "xui-pre-",
"preview": "xui-preview-",
"promoteListItem": "xui-promoteListItem-",
"right": "xui-right-",
"row": "xui-row-",
"section": "xui-section-",
"show_level": "xui-show_level-",
"sortRows": "xui-sortRows-",
"table": "xui-table-",
"typewriter": "xui-typewriter-",
"underline": "xui-underline-",
"up": "xui-up-",
"viewDocument": "xui-viewDocument-",
"viewDocuments": "xui-viewDocuments-",
"markdown": "xui-markdown-",
"paste_from_word": "xui-paste_from_word-",
"fallback": "xui-fallback-",
};
export class Util {
static escapeHTML(text) {
return text.replaceAll('&', "&amp;")
.replaceAll('<', "&lt;")
.replaceAll('>', "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
static shortenText(text, maxLength) {
if (maxLength < 4) {
maxLength = 4;
}
if (text.length > maxLength) {
const half = maxLength / 2;
text = text.substring(0, half - 1) + "\u2026" +
text.substring(text.length - half);
}
return text;
}
static wordWrap(text, lineMaxLength, maxLines=-1) {
let wrapped = "";
let lineCount = 0;
let lines = text.split('\n',  maxLines);
for (let i = 0; i < lines.length; ++i) {
let line = lines[i];
if (i > 0) {
wrapped += '\n';
}
if (line.length <= lineMaxLength || line.indexOf(' ') < 0) {
wrapped += line;
++lineCount;
} else {
let lineLength = 0;
let words = line.split(' ');
for (let word of words) {
let wordLength = word.length;
if (lineLength + wordLength > lineMaxLength) {
if (lineLength > 0) {
wrapped += '\n';
lineLength = 0;
++lineCount;
if (maxLines > 0 && lineCount >= maxLines) {
break;
}
}
} else {
if (lineLength > 0) {
wrapped += ' ';
++lineLength;
}
}
wrapped += word;
lineLength += wordLength;
}
if (lineLength > 0) {
++lineCount;
}
}
if (maxLines > 0 && lineCount >= maxLines) {
break;
}
}
return wrapped;
}
static pageContains(node) {
let foundDoc = false;
while (node) {
if (node === document) {
foundDoc = true;
break;
}
const parent = node.parentNode;
if (parent instanceof DocumentFragment) {
node = parent.host;
} else {
node = parent;
}
}
return foundDoc;
}
static removeAllChildren(parent) {
let child = parent.firstChild;
while (child !== null) {
let next = child.nextSibling;
parent.removeChild(child);
child = next;
}
}
static addStylesheetLink(tree, css=null) {
let link = document.createElement("link");
link.setAttribute("rel", "stylesheet");
link.setAttribute("type", "text/css");
if (!css) {
css = "xui.css";
}
let url = new URL(css, import.meta.url);
link.setAttribute("href", url.toString());
tree.appendChild(link);
return link;
}
static getPxProperty(element, propName) {
let propValue =
window.getComputedStyle(element).getPropertyValue(propName);
if (!propValue || !propValue.endsWith("px")) {
return NaN;
} else {
return parseFloat(propValue.substring(0, propValue.length-2));
}
}
static getPxProperties(element, ...args) {
let values = [];
const props = window.getComputedStyle(element);
for (let i = 0; i < args.length; i += 2) {
const propName = args[i];
const fallback = args[i+1];
let value = NaN;
let propValue = props.getPropertyValue(propName);
if (propValue !== null && propValue.endsWith("px")) {
value = parseFloat(propValue.substring(0, propValue.length-2));
}
if (isNaN(value)) {
value = fallback;
}
values.push(value);
}
return values;
}
static consumeEvent(event) {
event.preventDefault();
event.stopPropagation();
}
static modKey(event) {
return ((Util.PLATFORM_IS_MAC_OS && event.metaKey) ||
(!Util.PLATFORM_IS_MAC_OS && event.ctrlKey));
}
static badTextField(textField) {
textField.select();
textField.focus();
}
static rememberDatalistItem(localStorageKey, value) {
let valueList = [];
let values = window.localStorage.getItem(localStorageKey);
if (values !== null) {
valueList = values.split('\n');
}
if (value) {
while (valueList.length >= 10) {
valueList.pop();
}
let index = valueList.indexOf(value);
if (index < 0) {
valueList.unshift(value);
} else if (index > 0) {
valueList.splice(index, 1);
valueList.unshift(value);
}
}
if (valueList.length > 0) {
window.localStorage.setItem(localStorageKey, valueList.join('\n'));
} else {
window.localStorage.removeItem(localStorageKey);
}
return valueList;
}
static attachDatalist(textField, valueList, form) {
if (textField.hasAttribute("list") ||
!Array.isArray(valueList) || valueList.length === 0) {
return;
}
let datalistId = Util.uid();
Util.appendDatalist(datalistId, valueList, form);
textField.setAttribute("list", datalistId);
}
static uid() {
return Date.now().toString(36) +
Math.random().toString(36).substring(2);
}
static appendDatalist(id, items, parent) {
let list = document.createElement("datalist");
list.id = id;
for (let item of items) {
let option = document.createElement("option");
option.value = item;
list.appendChild(option);
}
parent.appendChild(list);
return list;
}
static prependDatalistItem(list, item, maxItems=20) {
if (item === null || item.length === 0) {
return;
}
let option = list.firstElementChild;
if (option !== null) {
if (option.value === item) {
return;
}
while (option !== null) {
if (option.value === item) {
list.removeChild(option);
break;
}
option = option.nextElementSibling;
}
}
option = document.createElement("option");
option.value = item;
list.insertBefore(option, list.firstElementChild);
if (maxItems < 2) {
maxItems = 2;
}
let itemCount = list.childElementCount;
while (itemCount > maxItems) {
list.removeChild(list.lastElementChild);
--itemCount;
}
}
static intersects(r1, r2) {
return (r1.right > r2.left) && (r1.left < r2.right) &&
(r1.bottom > r2.top) && (r1.top < r2.bottom);
}
}
Util.PLATFORM_IS_MAC_OS = window.navigator.platform.match(/mac/i);
export class Dialogs {
static open(options) {
let opts = Object.assign({
form: null,
type: "modal",
position: "center", reference: null,
classes: null
}, options);
let form = opts.form;
let type = opts.type;
let position = opts.position;
let reference = opts.reference;
let classes = opts.classes;
switch (type) {
case "dialog":
case "popup":
break;
default:
type = "modal";
break;
}
if (!Array.isArray(position)) {
switch (position) {
case "menu":
case "submenu":
case "comboboxmenu":
case "startmenu":
case "stopmenu":
if (reference === null) {
position = "center";
}
break;
case "topleft":
case "topright":
case "bottomleft":
case "bottomright":
case "top":
case "bottom":
case "left":
case "right":
break;
default:
position = "center";
break;
}
}
const html = document.documentElement;
const body = document.body;
let zIndex = 0;
let [max1, topGlass, topGlassType, topGlassZIndex, max2] =
Dialogs.scanExisting(body);
switch (type) {
case "dialog":
zIndex = max1 + 1;
Dialogs.checkNextZIndex(zIndex);
break;
case "popup":
if (topGlassType === "popup") {
zIndex = max2 + 1;
Dialogs.checkNextZIndex(zIndex);
break;
}
//FALLTHROUGH
default:
{
zIndex = (Math.floor(max2 / Dialogs.LAYER_CAPACITY) *
Dialogs.LAYER_CAPACITY) + Dialogs.LAYER_CAPACITY;
let glass = document.createElement("div");
glass.setAttribute("data-xui-glass", `${zIndex} ${type}`);
const [top, right, bottom, left] =
Util.getPxProperties(body, "margin-top", 0, "margin-right", 0,
"margin-bottom", 0, "margin-left", 0);
glass.setAttribute("style", `z-index: ${zIndex}; \
width: ${body.scrollWidth + right + left - 1}px; \
height: ${body.scrollHeight + top + bottom - 1}px;`);
body.appendChild(glass);
for (let eventType of [ "click", "auxclick", "contextmenu" ]) {
glass.addEventListener(eventType, Dialogs.onClickGlass,
true);
}
zIndex += Dialogs.LAYER_CAPACITY;
}
break;
}
let dialog = document.createElement("div");
dialog.setAttribute("data-xui-dialog", `${zIndex} ${type}`);
dialog.setAttribute("tabindex", "-1");
if (classes !== null && (classes = classes.trim()).length > 0) {
dialog.setAttribute("class", classes);
}
dialog.setAttribute("style", `position: absolute; left: 0px; top: 0px; \
z-index: ${zIndex}; visibility: hidden;`);
dialog.addEventListener("keydown", Dialogs.onKeydownDialog);
if (form !== null) {
dialog.appendChild(form);
}
body.appendChild(dialog);
if (type !== "dialog") {
let focused = document.activeElement;
if (focused !== null && !(focused instanceof HTMLBodyElement)) {
dialog.xuiWasFocused = focused;
}
}
const focusDialog = (zIndex > max2);
setTimeout(() => {
Dialogs.doOpen(dialog, focusDialog, position, reference);
}, 100 );
return dialog;
}
static doOpen(dialog, focusDialog, position, reference) {
const html = document.documentElement;
let htmlClientWidth =  html.clientWidth;
if (htmlClientWidth > 1) {
--htmlClientWidth;
}
let htmlClientHeight = html.clientHeight;
if (htmlClientHeight > 1) {
--htmlClientHeight;
}
const htmlRect = new DOMRect(0, 0, htmlClientWidth, htmlClientHeight);
let refRect = null;
if (reference !== null && Util.pageContains(reference)) {
refRect = reference.getBoundingClientRect();
if (!Util.intersects(refRect, htmlRect)) {
refRect = null;
}
}
if (refRect === null) {
refRect = htmlRect;
}
const rect = dialog.getBoundingClientRect();
let left = 0;
let top = 0;
if (Array.isArray(position)) {
left = position[0] + 1;
top = position[1] + 1;
} else {
switch (position) {
case "menu":
left = refRect.left;
top = refRect.bottom;
break;
case "submenu":
left = refRect.right;
top = refRect.top;
break;
case "comboboxmenu":
left = refRect.right - rect.width;
top = refRect.bottom;
break;
case "startmenu":
left = refRect.left;
top = refRect.top - rect.height;
break;
case "stopmenu":
left = refRect.right - rect.width;
top = refRect.top - rect.height;
break;
case "topleft":
left = refRect.left;
top = refRect.top;
break;
case "topright":
left = refRect.right - rect.width;
top = refRect.top;
break;
case "bottomleft":
left = refRect.left;
top = refRect.bottom - rect.height;
break;
case "bottomright":
left = refRect.right - rect.width;
top = refRect.bottom - rect.height;
break;
case "top":
left = refRect.left + ((refRect.width - rect.width)/2);
top = refRect.top;
break;
case "bottom":
left = refRect.left + ((refRect.width - rect.width)/2);
top = refRect.bottom - rect.height;
break;
case "left":
left = refRect.left;
top = refRect.top + ((refRect.height - rect.height)/2);
break;
case "right":
left = refRect.right - rect.width;
top = refRect.top + ((refRect.height - rect.height)/2);
break;
case "center":
left = refRect.left + ((refRect.width - rect.width)/2);
top = refRect.top + ((refRect.height - rect.height)/2);
break;
}
}
if (left < 0) {
left = 0;
} else if (left + rect.width > htmlClientWidth &&
htmlClientWidth - rect.width >= 0) {
left = htmlClientWidth - rect.width;
}
if (top < 0) {
top = 0;
} else if (top + rect.height > htmlClientHeight &&
htmlClientHeight - rect.height >= 0) {
top = htmlClientHeight - rect.height;
}
left += html.scrollLeft;
top += html.scrollTop;
dialog.style.visibility = "visible";
dialog.style.left = `${left}px`;
dialog.style.top = `${top}px`;
if (focusDialog) {
setTimeout(() => { Dialogs.focus(dialog); }, 100 );
}
}
static checkNextZIndex(zIndex) {
if ((zIndex % Dialogs.LAYER_CAPACITY) === 0) {
throw new Error(`Dialogs.open: INTERNAL ERROR: cannot open dialog: \
reached maximum layer capacity=${Dialogs.LAYER_CAPACITY}`);
}
}
static scanExisting(body) {
let max1 = 0;
let topGlass = null;
let topGlassType = null;
let topGlassZIndex = 0;
let max2 = 0;
let node = body.lastChild;
while (node !== null) {
if (node.nodeType === Node.ELEMENT_NODE) {
let [kind, type, zIndex] = Dialogs.getData(node);
if (kind === null) {
break;
}
if (kind === "glass") {
if (zIndex > topGlassZIndex) {
topGlass = node;
topGlassType = type;
topGlassZIndex = zIndex;
}
} else {
if (zIndex > max2) {
max2 = zIndex;
}
if (zIndex < Dialogs.LAYER_CAPACITY && zIndex > max1) {
max1 = zIndex;
}
}
}
node = node.previousSibling;
}
return [max1, topGlass, topGlassType, topGlassZIndex, max2];
}
static getData(elem) {
let kind = null;
let type = null;
let zIndex = 0;
if (elem !== null && elem.nodeType === Node.ELEMENT_NODE) {
let attr = elem.getAttribute("data-xui-dialog");
if (attr) {
kind = "dialog";
} else {
attr = elem.getAttribute("data-xui-glass");
if (attr) {
kind = "glass";
}
}
if (kind !== null) {
let split = attr.split(' ');
if (split.length !== 2) {
kind = null;
} else {
zIndex = parseInt(split[0]);
if (isNaN(zIndex) || zIndex <= 0) {
kind = null;
}
type = split[1];
switch (type) {
case "dialog":
case "popup":
case "modal":
break;
default:
kind = null;
break;
}
}
}
}
let data = [null, null, 0];
if (kind !== null) {
data[0] = kind;
data[1] = type;
data[2] = zIndex;
}
return data;
}
static onClickGlass(event) {
Dialogs.closeAllPopups(event.currentTarget);
event.preventDefault();
event.stopPropagation();
}
static closeAllPopups(glass=null) {
if (glass === null) {
let [max1, topGlass, topGlassType, topGlassZIndex, max2] =
Dialogs.scanExisting(document.body);
if (topGlass === null || topGlassType !== "popup") {
return;
}
glass = topGlass;
}
let [kind, type, zIndex] = Dialogs.getData(glass);
assertOrError(kind === "glass", `${glass}, not a glass pane`);
if (type === "popup") {
const layerZIndex = zIndex + Dialogs.LAYER_CAPACITY;
let popups = Dialogs.getPopups(layerZIndex);
assertOrError(popups.length > 0);
let closed = Dialogs.doClose(popups[popups.length-1].dialog);
assertOrError(glass.parentNode === null || !closed);
}
}
static onKeydownDialog(event) {
let dialog = event.currentTarget;
let [kind, type, zIndex] = Dialogs.getData(dialog);
assertOrError(kind === "dialog", `${dialog}, not a dialog`);
if (event.key === "Escape" &&
!event.altKey && !event.ctrlKey &&
!event.metaKey && !event.shiftKey) {
Dialogs.doClose(dialog);
event.preventDefault();
event.stopPropagation();
}
}
static getPopups(minZIndex) {
let popups = [];
const layerZIndex =
(Math.floor(minZIndex / Dialogs.LAYER_CAPACITY) *
Dialogs.LAYER_CAPACITY);
const nextLayerZIndex = layerZIndex + Dialogs.LAYER_CAPACITY;
let node = document.body.lastChild;
while (node !== null) {
if (node.nodeType === Node.ELEMENT_NODE) {
let [kind, type, zIndex] = Dialogs.getData(node);
if (kind === null) {
break;
}
if (kind === "dialog" && type === "popup" &&
zIndex >= minZIndex && zIndex < nextLayerZIndex) {
popups.push({ dialog: node, zIndex: zIndex });
}
}
node = node.previousSibling;
}
if (popups.length > 1) {
popups.sort((popup1, popup2) => {
return popup2.zIndex - popup1.zIndex;
});
}
return popups;
}
static close(elem, result=null) {
let dialog = Dialogs.lookup(elem);
if (dialog === null) {
return false;
}
return Dialogs.doClose(dialog, result);
}
static lookup(elem) {
let dialog = null;
while (elem !== null) {
if (elem.hasAttribute("data-xui-dialog")) {
dialog = elem;
break;
}
elem = elem.parentElement;
}
return dialog;
}
static doClose(dialog, result=null) {
let [kind, type, zIndex] = Dialogs.getData(dialog);
assertOrError(kind === "dialog", `${dialog}, not a dialog`);
let glassZIndex = 0;
switch (type) {
case "popup":
{
if (!Dialogs.closeSubsequentPopups(dialog)) {
return false;
}
const layerZIndex =
(Math.floor(zIndex / Dialogs.LAYER_CAPACITY) *
Dialogs.LAYER_CAPACITY);
if (zIndex === layerZIndex) {
glassZIndex = layerZIndex - Dialogs.LAYER_CAPACITY;
}
}
break;
case "modal":
glassZIndex = zIndex - Dialogs.LAYER_CAPACITY;
break;
}
if (!Dialogs.notifyListeners(dialog, result)) {
return false;
}
if (glassZIndex > 0) {
let glass = document.body.querySelector(
`[data-xui-glass='${glassZIndex} ${type}']`);
assertOrError(glass !== null,
`no ${type} glass at z-index=${glassZIndex}`);
glass.parentNode.removeChild(glass);
}
Dialogs.discard(dialog);
Dialogs.restoreFocus(dialog);
return true;
}
static closeSubsequentPopups(dialog) {
let [kind, type, zIndex] = Dialogs.getData(dialog);
assertOrError(kind === "dialog", `${dialog}, not a dialog`);
if (type === "popup") {
const layerZIndex = (Math.floor(zIndex / Dialogs.LAYER_CAPACITY) *
Dialogs.LAYER_CAPACITY);
let popups = Dialogs.getPopups(layerZIndex);
for (let popup of popups) {
if (popup.zIndex === zIndex) {
assertOrError(popup.dialog === dialog);
break;
}
if (!Dialogs.notifyListeners(popup.dialog,  null)) {
return false;
}
Dialogs.discard(popup.dialog);
}
}
return true;
}
static notifyListeners(dialog, result) {
const event = new CustomEvent("dialogclosed",
{ bubbles: false, composed: false,
cancelable: true,
detail: { dialogResult: result } });
dialog.dispatchEvent(event);
return !event.defaultPrevented;
}
static discard(dialog) {
dialog.parentNode.removeChild(dialog);
const form = dialog.firstElementChild;
if (form !== null) {
dialog.removeChild(form);
}
}
static restoreFocus(closedDialog) {
if (closedDialog.xuiWasFocused) {
closedDialog.xuiWasFocused.focus();
} else {
let [max1, topGlass, topGlassType, topGlassZIndex, max2] =
Dialogs.scanExisting(document.body);
if (max2 >= 2*Dialogs.LAYER_CAPACITY) {
let dialog =
document.body.querySelector(`[data-xui-dialog^='${max2}']`);
assertOrError(dialog !== null,
`no modal or popup dialog at z-index=${max2}`);
Dialogs.focus(dialog);
}
}
}
static focus(dialog) {
let focusable = dialog.querySelector("[autofocus]");
if (focusable === null) {
focusable = dialog.querySelector(
`input:not([disabled]):not([tabindex='-1']),\
select:not([disabled]):not([tabindex='-1']),\
textarea:not([disabled]):not([tabindex='-1']),\
button:not([disabled]):not([tabindex='-1']),
a[href]:not([tabindex='-1']),\
area[href]:not([tabindex='-1']),\
[tabindex]:not([tabindex='-1']),\
[contenteditable=true]:not([tabindex='-1'])`);
if (focusable === null) {
focusable = dialog;
}
}
if (focusable !== null) {
focusable.focus();
}
}
}
Dialogs.LAYER_CAPACITY = 100;
export class Dialog {
constructor(options) {
this._testMode = false;
let opts = Object.assign({
type: "modal", movable: false, resizable: false,
classes: "xui-control xui-dialog xui-dlg",
title: null, closeable: false,
template: null,
buttons: null
}, options);
this._type = opts.type;
this._classes = opts.classes;
this._form = document.createElement("div");
this._form.classList.add("xui-dlg-container");
if (opts.resizable) {
this._form.classList.add("xui-dlg-resizable");
}
this._form.appendChild(Dialog.TEMPLATE.content.cloneNode(true));
this._titlePane = this._form.firstElementChild;
this._contentPane = this._titlePane.nextElementSibling;
this._buttonPane = this._form.lastElementChild;
this._titleText = this._titlePane.firstElementChild;
this._closeButton = this._titlePane.lastElementChild;
if (opts.title === null && !opts.closeable) {
this._titleText.style.display = "none";
this._closeButton.style.display = "none";
} else {
if (opts.title !== null) {
this._titleText.textContent = opts.title;
}
if (opts.closeable) {
this._closeButton.textContent = StockIcon["cancel"];
this._closeButton.setAttribute("title", "Close");
this._closeButton.addEventListener("click", (e) => {
Util.consumeEvent(e);
this.close(null);
});
}
}
if (opts.template !== null) {
this._contentPane.appendChild(opts.template.content.cloneNode(true));
}
this._buttons = [];
if (opts.buttons !== null) {
let buttonSpecs = opts.buttons;
if (!Array.isArray(buttonSpecs)) {
buttonSpecs = [ buttonSpecs ];
}
let containsSepar = false;
let defaultAction = null;
for (let buttonSpec of buttonSpecs) {
if (Object.keys(buttonSpec).length === 0) {
let separ = document.createElement("span");
separ.setAttribute("class", "xui-dlg-button-separ");
this._buttonPane.appendChild(separ);
containsSepar = true;
} else {
let action = (typeof buttonSpec["action"] === "string")?
buttonSpec["action"] : null;
let label = (typeof buttonSpec["label"] === "string")?
buttonSpec["label"] : "???";
let isDefault = (buttonSpec["default"] === true);
let button = document.createElement("button");
button.setAttribute("type", "button");
button.classList.add("xui-control", "xui-dlg-button");
if (isDefault) {
button.classList.add("xui-dlg-default-button");
if (action !== null) {
defaultAction = this[action].bind(this);
}
}
button.textContent = label;
this._buttonPane.appendChild(button);
this._buttons.push(button);
if (action !== null) {
const buttonAction = this[action].bind(this);
button.addEventListener("click", (e) => {
Util.consumeEvent(e);
buttonAction();
});
}
}
}
if (defaultAction !== null) {
const onPressEnter = (e) => {
if (e.key === "Enter") {
Util.consumeEvent(e);
defaultAction();
}
};
const inputs = this._contentPane.querySelectorAll(
"input[type=text],input:not([type]),select,[tabindex]");
for (let input of inputs) {
input.addEventListener("keydown", onPressEnter);
}
}
if (!containsSepar) {
this._buttonPane.classList.add("xui-dlg-button-default-layout");
}
}
if (opts.movable && opts.title !== null) {
this._titleText.addEventListener("mousedown",
this.onDragStart.bind(this));
this._onDrag = this.onDrag.bind(this);
this._titleText.addEventListener("mouseup",
this.onDragEnd.bind(this));
this._dragOffsetX = this._dragOffsetY = -1;
this._dragging = false;
}
}
get testMode() {
return this._testMode;
}
set testMode(testing) {
this._testMode = testing;
}
get form() {
return this._form;
}
get titlePane() {
return this._titlePane;
}
get contentPane() {
return this._contentPane;
}
get buttonPane() {
return this._buttonPane;
}
get titleText() {
return this._titleText;
}
get closeButton() {
return this._closeButton;
}
get buttons() {
return this._buttons;
}
open(position="center", reference=null) {
this._dialog = Dialogs.open({
type: this._type, classes: this._classes,
form: this._form,
position: position, reference: reference
});
this._dialog.addEventListener("dialogclosed", (e) => {
this.dialogClosed(e.detail.dialogResult);
});
return this._dialog;
}
close(result=null) {
Dialogs.close(this._dialog, result);
this._dialog = null;
}
dialogClosed(result) {
}
onDragStart(event) {
if (event.buttons === 1) {
Util.consumeEvent(event);
this._dragging = true;
let dialogRect = this._dialog.getBoundingClientRect();
this._dragOffsetX = event.clientX - dialogRect.left;
this._dragOffsetY = event.clientY - dialogRect.top;
this._titleText.style.cursor = "move";
document.addEventListener("mousemove", this._onDrag);
}
}
onDrag(event) {
if (this._dragging) {
Util.consumeEvent(event);
this._dialog.style.left =
String(event.pageX - this._dragOffsetX) + "px";
this._dialog.style.top =
String(event.pageY - this._dragOffsetY) + "px";
}
}
onDragEnd(event) {
if (this._dragging) {
Util.consumeEvent(event);
document.removeEventListener("mousemove", this._onDrag);
this._titleText.style.cursor = null;
this._dragOffsetX = this._dragOffsetY = -1;
this._dragging = false;
}
}
}
Dialog.TEMPLATE = document.createElement("template");
Dialog.TEMPLATE.innerHTML = `
<div class="xui-dlg-title-pane">
  <span draggable="false" class="xui-dlg-title"></span>
  <span class="xui-small-icon xui-dlg-close-button"></span>
</div>
<div class="xui-dlg-content-pane"></div>
<div class="xui-dlg-button-pane"></div>
`;
export class Alert extends Dialog {
static showError(message, reference=null) {
Alert.showAlert("error", message, reference);
}
static showWarning(message, reference=null) {
Alert.showAlert("warning", message, reference);
}
static showInfo(message, reference=null) {
Alert.showAlert("info", message, reference);
}
static showAlert(type, message, reference=null) {
let alert = new Alert(type, message);
alert.open("center", reference);
}
constructor(type, message, options={}) {
super(Object.assign({ title: "-", movable: true,
template: Alert.TEMPLATE,
buttons: { label: "OK", action: "okAction" } },
options));
let title = "Error";
let iconClass = "xui-alert-error-icon";
let iconContent = StockIcon["minus-circled"];
switch (type) {
case "warning":
title = "Warning";
iconClass = "xui-alert-warning-icon";
iconContent = StockIcon["attention"];
break;
case "info":
case "information":
title = "Information";
iconClass = "xui-alert-info-icon";
iconContent = StockIcon["info-circled"];
break;
case "confirm":
title = "Confirm";
iconClass = "xui-alert-confirm-icon";
iconContent = StockIcon["question"];
break;
}
this.titleText.textContent = title;
let pane = this.contentPane.firstElementChild;
let icon = pane.firstElementChild;
icon.classList.add(iconClass);
icon.textContent = iconContent;
let msg = pane.lastElementChild;
if (message.startsWith("<html>")) {
message = message.substring(6);
} else {
message = Util.escapeHTML(message);
if (message.indexOf('\n') >= 0) {
message = message.replaceAll('\n', "<br>");
}
}
msg.innerHTML = message;
}
okAction() {
this.close();
}
}
Alert.TEMPLATE = document.createElement("template");
Alert.TEMPLATE.innerHTML = `
<div class="xui-alert-pane">
  <div class="xui-alert-icon"></div>
  <div class="xui-alert-message"></div>
</div>
`;
export class Confirm extends Alert {
static showConfirm(question, reference=null) {
return new Promise((resolve, reject) => {
let dialog = new Confirm(question, resolve);
dialog.open("center", reference);
});
}
constructor(question, resolve) {
super("confirm", question,
{ buttons: [ { label: "Cancel", action: "cancelAction" },
{ label: "OK", action: "okAction" } ] });
this._resolve = resolve;
}
dialogClosed(result) {
if (result === null) {
result = false;
}
this._resolve(result);
}
cancelAction() {
this.close(false);
}
okAction() {
this.close(true);
}
}
export class Prompt extends Dialog {
static showPrompt(title, prompt,
columns=40, initialValue=null,
valueList=null, inputValueChecker=null,
reference=null) {
return Prompt.prompt({ title: title,
prompt: prompt,
columns: columns,
initialValue: initialValue,
valueList: valueList,
inputValueChecker: inputValueChecker },
reference);
}
static prompt(options, reference) {
return new Promise((resolve, reject) => {
let dialog = new Prompt(options, resolve);
dialog.open("center", reference);
});
}
constructor(options, resolve) {
const opts = Object.assign({
title: "???",
prompt: "???",
hint: null,
columns: 40,
initialValue: null,
valueList: null,
inputValueChecker: null
}, options);
super({ movable: true, title: opts.title, closeable: true,
template: Prompt.TEMPLATE,
buttons: [ { label: "Cancel", action: "cancelAction" },
{ label: "OK", action: "okAction",
default: true } ]});
this._resolve = resolve;
let pane = this.contentPane.firstElementChild;
let icon = pane.firstElementChild;
icon.textContent = StockIcon["question"];
let form = pane.lastElementChild;
let question = form.firstElementChild;
question.innerHTML = Prompt.textToHTML(opts.prompt);
this._input = question.nextElementSibling;
if (opts.columns < 10) {
opts.columns = 10;
}
this._input.size = opts.columns;
let hint = form.lastElementChild;
if (opts.hint) {
hint.innerHTML = Prompt.textToHTML(opts.hint);
} else {
hint.style.display = "none";
}
if (opts.initialValue !== null) {
this._input.value = opts.initialValue;
this.selectInputValue();
}
this._input.autocomplete = "off";
if (opts.valueList !== null && Array.isArray(opts.valueList) &&
opts.valueList.length > 0) {
let allValues = [];
for (let value of opts.valueList) {
value = value.toString().trim();
if (value.length > 0) {
allValues.push(value);
}
}
if (allValues.length > 0) {
let datalistId = Util.uid();
Util.appendDatalist(datalistId, allValues, form);
this._input.setAttribute("list", datalistId);
}
}
this._checkInputValue = opts.inputValueChecker;
this._input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
Util.consumeEvent(e);
this.okAction();
}
});
}
static textToHTML(text) {
if (text.startsWith("<html>")) {
text = text.substring(6);
} else {
text = Util.escapeHTML(text);
if (text.indexOf('\n') >= 0) {
text = text.replaceAll('\n', "<br>");
}
}
return text;
}
dialogClosed(result) {
this._resolve(result);
}
cancelAction() {
this.close(null);
}
okAction() {
let inputValue = this.getInputValue();
if (this._checkInputValue !== null) {
if (!this._checkInputValue(inputValue)) {
this.selectInputValue();
return;
}
}
this.close(inputValue);
}
getInputValue() {
let result = this._input.value;
if (result !== null && (result = result.trim()).length === 0) {
result = null;
}
return result;
}
selectInputValue() {
this._input.focus();
this._input.select();
}
}
Prompt.TEMPLATE = document.createElement("template");
Prompt.TEMPLATE.innerHTML = `
<div class="xui-prompt-pane">
  <div class="xui-prompt-icon"></div>
  <div class="xui-prompt-form">
    <div class="xui-prompt-question"></div>
    <input class="xui-prompt-input" type="text" spellcheck="false" />
    <div class="xui-prompt-hint"></div>
  </div>
</div>
`;
export class MenuItem extends HTMLElement {
static create(options) {
let menuItem  = document.createElement("xui-menu-item");
if (options !== null) {
menuItem.options = options;
}
return menuItem;
}
constructor() {
super();
let shadow = this.attachShadow({mode: "open"});
Util.addStylesheetLink(shadow);
this._item = document.createElement("div");
this._item.className = "xui-control xui-menu-item";
shadow.appendChild(this._item);
this._icon = document.createElement("div");
this._icon.className = "xui-menu-item-icon";
this._item.appendChild(this._icon);
this._text = document.createElement("div");
this._text.className = "xui-menu-item-text";
this._item.appendChild(this._text);
this._detail = document.createElement("div");
this._detail.className = "xui-menu-item-detail";
this._item.appendChild(this._detail);
this._parentMenu = null;
this._submenu = null;
this._options = { name: null, type: null, buttonGroup: null,
icon: null, selectedIcon: null, tooltip: null,
text: null, detail: null, items: null,
selected: false, enabled: true, separator: false };
}
get contents() {
return this._item;
}
get options() {
return this._options;
}
set options(options) {
for (const key in options) {
this._options[key] = options[key];
}
this.configure();
}
getOption(key) {
return this._options[key];
}
setOption(key, value) {
switch (key) {
case "name":
this.name = value;
break;
case "tooltip":
this.tooltip = value;
break;
case "selected":
this.selected = value;
break;
case "enabled":
this.enabled = value;
break;
case "separator":
this.separator = value;
break;
default:
this._options[key] = value;
this.configure();
break;
}
}
get itemType() {
return this._options.type;
}
get isIconOnly() {
return (this._options.icon !== null && this._options.text === null);
}
get name() {
return this._options.name;
}
set name(name) {
this._options.name = MenuItem.normalizeString(name);
}
static normalizeString(value) {
if (typeof value === "string") {
const preserveNbsp = (value.indexOf("\u00A0") >= 0);
if (preserveNbsp) {
value = value.replace("\u00A0", "\uEDCB");
}
value = value.trim();
if (preserveNbsp) {
value = value.replace("\uEDCB", "\u00A0");
}
if (value.length === 0) {
value = null;
}
} else {
value = null;
}
return value;
}
get tooltip() {
return this._options.tooltip;
}
set tooltip(tooltip) {
this._options.tooltip = MenuItem.normalizeString(tooltip);
if (this._options.tooltip === null) {
this.removeAttribute("title");
} else {
this.setAttribute("title", this._options.tooltip);
}
}
get selected() {
return this._options.selected;
}
set selected(selected) {
this.setSelected(selected);
if (this._options.selected) {
this.onSelected();
}
}
setSelected(selected) {
this._options.selected = MenuItem.normalizeBoolean(selected);
this.updateIcon(this._options.selected, this._options.enabled);
}
static normalizeBoolean(value) {
return value? true : false;
}
updateIcon(selected, enabled) {
this._icon.textContent = "";
let icon = selected? this._options.selectedIcon : this._options.icon;
if (icon !== null) {
let iconName = null;
let iconFg = null;
let iconBg = null;
if (icon.startsWith("url(") && icon.endsWith(")")) {
let iconElem =
MenuItem.createIcon(icon, MenuItem.SMALL_ICON_SIZE);
this._icon.appendChild(iconElem);
} else if (icon.startsWith("icon(") && icon.endsWith(")")) {
let split =
icon.substring(5, icon.length-1).trim().split(/\s*[,]\s*/);
switch (split.length) {
case 3:
iconBg = split[2];
//FALLTHROUGH
case 2:
iconFg = split[1];
//FALLTHROUGH
case 1:
iconName = split[0];
break;
}
} else {
iconName = icon;
}
if (iconName !== null) {
let iconElem =
MenuItem.createEditIcon(iconName, MenuItem.SMALL_ICON_SIZE);
if (iconElem === null) {
iconElem =
MenuItem.createStockIcon(iconName, iconFg, iconBg,
enabled);
if (iconElem === null) {
iconElem =
MenuItem.createStockIcon("picture", "red", iconBg,
enabled);
}
}
this._icon.appendChild(iconElem);
}
}
}
static createIcon(iconURL, iconSize) {
let span = document.createElement("span");
span.className =`xui-edit-icon-${iconSize}`;
span.setAttribute("style", `background: \
${iconURL} 0px 0px/${iconSize}px ${iconSize}px no-repeat;`);
return span;
}
static createEditIcon(iconName, iconSize) {
let classPrefix = EditIcon[iconName];
if (!classPrefix) {
return null;
}
let span = document.createElement("span");
span.className =`xui-edit-icon-${iconSize} ${classPrefix}${iconSize}`;
return span;
}
static createStockIcon(iconName, iconFg, iconBg, enabled) {
let iconChar = StockIcon[iconName];
if (!iconChar) {
return null;
}
let span = document.createElement("span");
span.className = "xui-small-icon";
if (enabled && (iconFg !== null || iconBg !== null)) {
let style = "";
if (iconFg !== null && iconFg !== "transparent") {
style += `color: ${iconFg};`;
}
if (iconBg !== null && iconBg !== "transparent") {
style += `background-color: ${iconBg};`;
}
span.setAttribute("style", style);
}
span.textContent = iconChar;
return span;
}
onSelected() {
const group = this._options.buttonGroup;
if (group !== null && this._parentMenu !== null) {
let all = this._parentMenu.getAllItems();
for (let item of all) {
if (item !== this && item._options.buttonGroup === group) {
item.setSelected(false);
}
}
}
}
get enabled() {
return this._options.enabled;
}
set enabled(enable) {
this._options.enabled = MenuItem.normalizeBoolean(enable);
this._item.classList.remove("xui-control-disabled");
if (!this._options.enabled) {
this._item.classList.add("xui-control-disabled");
}
}
get separator() {
return this._options.separator;
}
set separator(separ) {
this._options.separator = MenuItem.normalizeBoolean(separ);
}
configure() {
let opts = this._options;
MenuItem.normalizeOptions(opts);
this._text.textContent = "";
if (opts.text !== null) {
this._text.textContent = opts.text;
}
this._detail.textContent = "";
if (opts.type === "submenu") {
let span = document.createElement("span");
span.className = "xui-small-icon";
span.textContent = StockIcon["right-dir"];
this._detail.appendChild(span);
} else if (opts.detail !== null) {
this._detail.textContent = opts.detail;
}
this.tooltip = opts.tooltip;
this.enabled = opts.enabled;
this.separator = opts.separator;
this.selected = opts.selected;
this._item.classList.remove("xui-icon-only");
if (this.isIconOnly) {
this._item.classList.add("xui-icon-only");
}
}
static normalizeOptions(opts) {
if (!opts.items || !Array.isArray(opts.items)) {
opts.items = null;
}
let type = opts.type;
switch (type) {
case "checkbox":
case "radiobutton":
case "submenu":
break;
default:
if (opts.items !== null) {
type = "submenu";
} else {
type = "button";
}
break;
}
opts.type = type;
opts.icon = MenuItem.normalizeString(opts.icon);
opts.selectedIcon = MenuItem.normalizeString(opts.selectedIcon);
if (opts.icon === null) {
switch (type) {
case "checkbox":
opts.icon = "icon(check-empty)";
opts.selectedIcon = "icon(check)"
break;
case "radiobutton":
opts.icon = "icon(circle-empty)";
opts.selectedIcon = "icon(dot-circled)";
break;
}
}
if (opts.icon === null) {
opts.selectedIcon = null;
} else {
if (opts.selectedIcon === null) {
opts.selectedIcon = opts.icon;
}
}
opts.text = MenuItem.normalizeString(opts.text);
opts.detail = MenuItem.normalizeString(opts.detail);
opts.selected = MenuItem.normalizeBoolean(opts.selected);
opts.enabled = MenuItem.normalizeBoolean(opts.enabled);
opts.separator = MenuItem.normalizeBoolean(opts.separator);
opts.name = MenuItem.normalizeString(opts.name);
opts.tooltip = MenuItem.normalizeString(opts.tooltip);
opts.buttonGroup = MenuItem.normalizeString(opts.buttonGroup);
if (type === "submenu") {
opts.icon = null;
opts.selectedIcon = null;
opts.detail = null;
opts.buttonGroup = null;
if (opts.items === null) {
opts.items = [];
}
} else {
if (opts.text === null) {
opts.detail = null;
}
if (type !== "radiobutton") {
opts.buttonGroup = null;
}
opts.items = null;
}
}
get parentMenu() {
return this._parentMenu;
}
set parentMenu(menu) {
assertOrError(this._parentMenu === null);
this._parentMenu = menu;
}
get submenu() {
if (this.itemType === "submenu" && this._submenu === null) {
this._submenu = Menu.create(this._options.items);
this._submenu.ownerItem = this;
}
return this._submenu;
}
}
MenuItem.SMALL_ICON_SIZE = 16;
window.customElements.define("xui-menu-item", MenuItem);
export class Menu extends HTMLElement {
static create(itemOptionsList) {
let menu = document.createElement("xui-menu");
menu.removeAllItems();
if (itemOptionsList !== null) {
for (let opts of itemOptionsList) {
menu.addItem(MenuItem.create(opts));
}
}
return menu;
}
constructor() {
super();
let shadow = this.attachShadow({mode: "open"});
Util.addStylesheetLink(shadow);
this._menu = document.createElement("div");
this._menu.className = "xui-control xui-menu";
shadow.appendChild(this._menu);
this._menuClosed = this.menuClosed.bind(this);
this._handleEvent = this.handleEvent.bind(this);
this._ownerItem = null;
this._allItems = null;
}
get ownerItem() {
return this._ownerItem;
}
set ownerItem(item) {
assertOrError(this._ownerItem === null);
this._ownerItem = item;
}
removeAllItems() {
let all = this.getAllItems();
Menu.removeChildren(this._menu);
for (let item of all) {
this.detachItem(item);
}
this._allItems = null;
}
static removeChildren(parent) {
let node = parent.firstChild;
while (node !== null) {
let next = node.nextSibling;
parent.removeChild(node);
node = next;
}
}
getAllItems() {
if (this._allItems === null) {
this._allItems = [];
Menu.doGetAllItems(this._menu, this._allItems);
}
return this._allItems;
}
static doGetAllItems(tree, list) {
let node = tree.firstChild;
while (node !== null) {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node instanceof MenuItem) {
list.push(node);
} else {
Menu.doGetAllItems(node, list);
}
}
node = node.nextSibling;
}
}
addItem(item, beforeItem=null, deep=true) {
assertOrError(item.parentMenu === null);
let parentMenu = (beforeItem === null)? null : beforeItem.parentMenu;
if (parentMenu !== null) {
if (parentMenu === this) {
this.doAddItem(item, beforeItem);
} else {
if (deep) {
parentMenu.addItem(item, beforeItem, false);
}
}
} else {
this.doAddItem(item, null);
}
}
doAddItem(item, beforeItem) {
let all = this.getAllItems();
let count = all.length;
if (beforeItem === null) {
all.push(item);
} else {
for (let i = 0; i < count; ++i) {
if (all[i] === beforeItem) {
all.splice(i, 0, item);
break;
}
}
}
++count;
Menu.removeChildren(this._menu);
let row = null;
for (let i = 0; i < count; ++i) {
let added = all[i];
if (added.isIconOnly) {
let separ = added.separator;
if (row === null) {
row = document.createElement("div");
row.className = "xui-control xui-menu-row";
this._menu.appendChild(row);
if (added.separator) {
row.style.borderTopStyle = "solid";
separ = false;
}
}
row.appendChild(added);
if (separ) {
added.contents.style.borderLeftStyle = "solid";
} else {
added.contents.style.borderLeftStyle = null;
}
} else {
row = null;
this._menu.appendChild(added);
if (added.separator) {
added.contents.style.borderTopStyle = "solid";
} else {
added.contents.style.borderTopStyle = null;
}
}
}
this.attachItem(item);
this._allItems = null;
}
removeItem(item, deep=true) {
let parentMenu = item.parentMenu;
if (parentMenu === this) {
this.doRemoveItem(item);
} else {
if (deep && parentMenu !== null) {
parentMenu.removeItem(item, false);
}
}
}
doRemoveItem(item) {
let container = item.parentElement;
container.removeChild(item);
if (container.className === "xui-menu-row" &&
container.childElementCount === 0) {
this._menu.removeChild(container);
}
this.detachItem(item);
this._allItems = null;
}
getItem(name, deep=true) {
let all = this.getAllItems();
for (let item of all) {
if (item.name === name) {
return item;
}
}
if (deep) {
for (let item of all) {
if (item.itemType === "submenu") {
let found = item.submenu.getItem(name, true);
if (found !== null) {
return found;
}
}
}
}
return null;
}
open(position, reference=null) {
this.openingMenu(this);
let popup = Dialogs.open({ form: this, type: "popup",
position: position, reference: reference });
popup.addEventListener("dialogclosed", this._menuClosed);
}
openingMenu(menu) {
const event = new CustomEvent("openingmenu",
{ bubbles: false, composed: false,
cancelable: false,
detail: { menu: this } });
this.dispatchEvent(event);
}
menuClosed() {
const event = new CustomEvent("menuclosed",
{ bubbles: false, composed: false,
cancelable: false,
detail: { menu: this } });
this.dispatchEvent(event);
}
attachItem(item) {
item.parentMenu = this;
for (const eventType of Menu.HANDLED_EVENT_TYPES) {
item.addEventListener(eventType, this._handleEvent);
}
}
detachItem(item) {
if (item.separator) {
if (item.isIconOnly) {
item.contents.style.borderLeftStyle = null;
} else {
item.contents.style.borderTopStyle = null;
}
}
item.parentMenu = null;
for (const eventType of Menu.HANDLED_EVENT_TYPES) {
item.removeEventListener(eventType, this._handleEvent);
}
}
handleEvent(event) {
let topMenu = this.getTopMenu();
if (topMenu !== this) {
topMenu.handleEvent(event);
return;
}
const item = event.target;
switch (event.type) {
case "mouseenter":
this.openSubmenu(item);
break;
case "mouseup":
this.selectMenuItem(item, Menu.mouseEventModifiers(event));
//FALLTHROUGH
case "click":
case "auxclick":
case "contextmenu":
event.preventDefault();
event.stopPropagation();
break;
}
}
getTopMenu() {
let topMenu = this;
while (topMenu.ownerItem !== null) {
topMenu = topMenu.ownerItem.parentMenu;
}
return topMenu;
}
openSubmenu(item) {
this.closeAllSubmenus(item.parentMenu);
if (item.itemType === "submenu" && item.enabled) {
this.openingMenu(item.submenu);
item.submenu.open("submenu", item);
}
}
closeAllSubmenus(currentSubmenu=null) {
Menu.doCloseAllSubmenus(this._menu, currentSubmenu);
}
static doCloseAllSubmenus(tree, currentSubmenu) {
let node = tree.firstChild;
while (node !== null) {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node instanceof MenuItem) {
if (node.itemType === "submenu") {
const submenu = node.submenu;
if (submenu.parentNode !== null) {
submenu.closeAllSubmenus(currentSubmenu);
if (currentSubmenu === null ||
!currentSubmenu.isSubmenuOf(submenu)) {
Dialogs.close(submenu);
}
}
}
} else {
Menu.doCloseAllSubmenus(node, currentSubmenu);
}
}
node = node.nextSibling;
}
}
isSubmenuOf(otherMenu) {
let isSubmenu = false;
let menu = this;
for (;;) {
if (menu === otherMenu) {
isSubmenu = true;
break;
}
if (menu.ownerItem !== null) {
menu = menu.ownerItem.parentMenu;
} else {
break;
}
}
return isSubmenu;
}
static mouseEventModifiers(event) {
let modifiers = [];
if (event.altKey) {
modifiers.push("alt");
}
if (event.ctrlKey) {
modifiers.push("ctrl");
}
if (event.metaKey) {
modifiers.push("meta");
}
if (event.shiftKey) {
modifiers.push("shift");
}
return modifiers;
}
selectMenuItem(item, modifiers) {
if (item.enabled) {
switch (item.itemType) {
case "checkbox":
case "radiobutton":
item.selected = !item.selected;
//FALLTHROUGH
case "button":
this.menuItemSelected(item, modifiers);
this.closeAllSubmenus();
Dialogs.close(this);
break;
}
}
}
menuItemSelected(item, modifiers) {
const event =
new CustomEvent("menuitemselected",
{ bubbles: false, composed: false,
cancelable: false,
detail: { menuItem: item,
inputEventModifiers: modifiers } });
this.dispatchEvent(event);
}
}
Menu.HANDLED_EVENT_TYPES = [
"mouseenter", "mouseup", "click", "auxclick", "contextmenu"
];
window.customElements.define("xui-menu", Menu);
export class List extends HTMLElement {
constructor() {
super();
this._singleClickToAccept = false;
this.addEventListener("click", this.onClick.bind(this));
this.addEventListener("keydown", this.onKeydown.bind(this));
this._connected = false;
this._labelMaker = null;
this._htmlRenderer = null;
this._disabledChecker = null;
this._items = [];
this._selection = [];
this._anchor = this._extent = -1;
}
connectedCallback() {
if (!this._connected) {
this._connected = true;
this.classList.add("xui-control");
this.setAttribute("tabindex", "0");
if (!this.hasAttribute("selectionmode")) {
this.selectionMode = "single";
}
}
}
static get observedAttributes() {
return [ "selectionmode" ];
}
attributeChangedCallback(attrName, oldVal, newVal) {
if (newVal === "single" && oldVal === "multiple") {
let index = this.getSelection();
if (index >= 0) {
this.setSelection(index);
}
}
}
get singleClickToAccept() {
return this._singleClickToAccept;
}
set singleClickToAccept(single) {
this._singleClickToAccept = single;
}
onClick(event) {
if (event.button === 0 && event.detail === 1) {
Util.consumeEvent(event);
if (this.length > 0) {
let index = this.clickEventToIndex(event);
if (index >= 0) {
if (event.shiftKey) {
this.extendSelection(index,  1);
} else if (Util.modKey(event)) {
this.toggleSelection(index,  1);
} else {
let changed =
this.changeSelection(index,  1);
if (!changed &&
this.singleClickToAccept &&
this.isSingleSelectionMode()) {
this.selectionChanged( [], 1);
}
}
}
}
this.focus();
}
}
clickEventToIndex(event) {
let itemDiv = null;
let elem = event.target;
while (elem !== null && !Object.is(elem, this)) {
if (elem.classList.contains("xui-lst-item")) {
itemDiv = elem;
break;
}
elem = elem.parentElement;
}
let index = -1;
if (itemDiv !== null) {
let i = 0;
let child = this.firstElementChild;
while (child !== null) {
if (Object.is(child, itemDiv)) {
index = i;
break;
}
++i;
child = child.nextElementSibling;
}
}
return index;
}
extendSelection(index, clickCount=0) {
if (this.isSingleSelectionMode()) {
this.changeSelection(index, clickCount);
return;
}
let curAnchor = this._anchor;
if (curAnchor < 0) {
this.setAnchor(0);
curAnchor = this._anchor;
}
let from, to;
if (index < curAnchor) {
from = index;
to = curAnchor;
} else if (index > curAnchor) {
from = curAnchor;
to = index;
} else {
from = to = index;
}
let sel = [];
for (let i = from; i <= to; ++i) {
sel.push(i);
}
if (!List.sameSelection(sel, this._selection)) {
let prevSelection = this.setSelection(...sel);
this.selectionChanged(prevSelection, clickCount);
}
this.setAnchor(curAnchor);
this._extent = index;
}
static sameSelection(a1, a2) {
let count;
if ((count = a1.length) !== a2.length) {
return false;
}
for (let i = 0; i < count; ++i) {
if (a1[i] !== a2[i]) {
return false;
}
}
return true;
}
toggleSelection(index, clickCount=0) {
let sel = [ ...this._selection ];
let pos = sel.indexOf(index);
if (pos >= 0) {
sel.splice(pos, 1);
} else {
if (this.isSingleSelectionMode()) {
sel = [ index ];
} else {
sel.push(index);
}
}
let prevSelection = this.setSelection(...sel);
this.selectionChanged(prevSelection, clickCount);
this.setAnchor(index);
}
changeSelection(index, clickCount=0) {
let changed = false;
if (this._selection.length !== 1 || this._selection[0] !== index) {
let prevSelection = this.setSelection(index);
changed = this.selectionChanged(prevSelection, clickCount);
}
this.setAnchor(index);
return changed;
}
selectionChanged(prevSelection, clickCount=0) {
if (List.sameSelection(this._selection, prevSelection)) {
return false;
}
let event = new CustomEvent("selectionchanged",
{ bubbles: false, composed: false,
cancelable: false,
detail: { selection: this.getSelection(),
clickCount: clickCount } });
this.dispatchEvent(event);
return true;
}
onKeydown(event) {
let isHotKey = true;
switch (event.key) {
case "PageUp":
this.onUpKey(event.shiftKey, Util.modKey(event), 10);
break;
case "PageDown":
this.onDownKey(event.shiftKey, Util.modKey(event), 10);
break;
case "ArrowUp":
case "Up":
this.onUpKey(event.shiftKey, Util.modKey(event));
break;
case "ArrowDown":
case "Down":
this.onDownKey(event.shiftKey, Util.modKey(event));
break;
case "Home":
this.onHomeKey(event.shiftKey, Util.modKey(event));
break;
case "End":
this.onEndKey(event.shiftKey, Util.modKey(event));
break;
case " ":
this.onSpaceKey(event.shiftKey, Util.modKey(event));
break;
case "a":
this.onAKey(event.shiftKey, Util.modKey(event));
break;
default:
isHotKey = false;
break;
}
if (isHotKey) {
Util.consumeEvent(event);
}
}
onUpKey(isShift, isMod, skip=1) {
if (this.length === 0) {
return;
}
let curExtent = this._extent;
if (this._anchor < 0) {
this.setAnchor(this.length-1);
curExtent = this.length + skip - 1;
}
let nextExtent = curExtent - skip;
if (nextExtent < 0) {
nextExtent = 0;
}
if (isShift) {
this.extendSelection(nextExtent);
} else if (isMod) {
this.setAnchor(nextExtent);
} else {
this.changeSelection(nextExtent);
}
if (this._extent >= 0) {
this.ensureIsVisible(this._extent);
}
}
onDownKey(isShift, isMod, skip=1) {
if (this.length === 0) {
return;
}
let curExtent = this._extent;
if (this._anchor < 0) {
this.setAnchor(0);
curExtent = -skip;
}
let nextExtent = curExtent + skip;
if (nextExtent >= this.length) {
nextExtent = this.length-1;
}
if (isShift) {
this.extendSelection(nextExtent);
} else if (isMod) {
this.setAnchor(nextExtent);
} else {
this.changeSelection(nextExtent);
}
if (this._extent >= 0) {
this.ensureIsVisible(this._extent);
}
}
onHomeKey(isShift, isMod) {
if (this.length === 0) {
return;
}
if (this._anchor < 0) {
this.setAnchor(0);
}
let nextExtent = 0;
if (isShift) {
this.extendSelection(nextExtent);
} else if (isMod) {
this.setAnchor(nextExtent);
} else {
this.changeSelection(nextExtent);
}
if (this._extent >= 0) {
this.ensureIsVisible(this._extent);
}
}
onEndKey(isShift, isMod) {
if (this.length === 0) {
return;
}
if (this._anchor < 0) {
this.setAnchor(this.length-1);
}
let nextExtent = this.length-1;
if (isShift) {
this.extendSelection(nextExtent);
} else if (isMod) {
this.setAnchor(nextExtent);
} else {
this.changeSelection(nextExtent);
}
if (this._extent >= 0) {
this.ensureIsVisible(this._extent);
}
}
onSpaceKey(isShift, isMod) {
if (this.length === 0) {
return;
}
if (this._anchor >= 0) {
if (isMod) {
this.toggleSelection(this._anchor);
} else {
this.changeSelection(this._anchor);
}
}
if (this._extent >= 0) {
this.ensureIsVisible(this._extent);
}
}
onAKey(isShift, isMod) {
if (this.length === 0) {
return;
}
if (isMod) {
let prevSelection = this.selectAll();
this.selectionChanged(prevSelection);
}
if (this._extent >= 0) {
this.ensureIsVisible(this._extent);
}
}
get selectionMode() {
return this.getAttribute("selectionmode");
}
set selectionMode(mode) {
this.setAttribute("selectionmode",
(mode === "multiple")? mode : "single");
}
isSingleSelectionMode() {
return (this.selectionMode === "single");
}
get labelMaker() {
return this._labelMaker;
}
set labelMaker(maker) {
this._labelMaker = maker;
}
get htmlRenderer() {
return this._htmlRenderer;
}
set htmlRenderer(renderer) {
this._htmlRenderer = renderer;
}
get disabledChecker() {
return this._disabledChecker;
}
set disabledChecker(checker) {
this._disabledChecker = checker;
}
get length() {
return this._items.length;
}
get(index) {
let item = null;
if (index >= 0 && index < this.length) {
item = this._items[index];
}
return item;
}
getLabel(index) {
let label = null;
if (index >= 0 && index < this.length) {
label = this.itemLabel(this._items[index]);
}
return label;
}
itemLabel(item) {
let label = null;
if (this._labelMaker !== null) {
try {
label = this._labelMaker(item);
} catch (error) {
console.error(`XUI.List.labelMaker has failed: ${error}`);
}
}
if (label === null) {
label = item.toString();
}
return label;
}
isDisabled(index) {
let disabled = false;
if (index >= 0 && index < this.length) {
disabled = this.itemIsDisabled(this._items[index]);
}
return disabled;
}
itemIsDisabled(item) {
let disabled = false;
if (this._disabledChecker !== null) {
try {
disabled = this._disabledChecker(item);
} catch (error) {
console.error(`XUI.List.disabledChecker has failed: ${error}`);
}
}
return disabled;
}
getAll(copy=true) {
if (copy) {
return [ ...this._items ];
} else {
return this._items;
}
}
setAll(items) {
this.removeAll();
this._items = [ ...items ];
let i = 0;
for (let item of items) {
let child = document.createElement("div");
child.setAttribute("class", "xui-lst-item");
this.populateChild(child, item, i++);
this.appendChild(child);
}
this.update(0);
this.ensureIsVisible(0);
}
removeAll() {
this._items = [];
this._selection = [];
this._anchor = -1;
Util.removeAllChildren(this);
}
populateChild(child, item, index) {
child.innerHTML = this.itemInnerHTML(item, index);
if (this.itemIsDisabled(item)) {
child.classList.add("xui-lst-disabled");
} else {
child.classList.remove("xui-lst-disabled");
}
}
itemInnerHTML(item, index) {
let html = null;
if (this._htmlRenderer !== null) {
try {
html = this._htmlRenderer(item, index);
} catch (error) {
console.error(`XUI.List.htmlRenderer has failed: ${error}`);
}
}
if (html === null) {
html = Util.escapeHTML(this.itemLabel(item));
if (html.indexOf('\n') >= 0) {
html = html.replaceAll('\n', "<br>");
}
}
return html;
}
update(from) {
this._selection = [];
this._anchor = -1;
let i = 0;
let child = this.firstElementChild;
while (child !== null) {
let classList = child.classList;
if (i >= from) {
classList.remove("xui-lst-item0", "xui-lst-item1");
classList.add("xui-lst-item" + String(i % 2));
}
if (classList.contains("xui-lst-selected")) {
this._selection.push(i);
}
if (classList.contains("xui-lst-anchor")) {
this._anchor = i;
}
++i;
child = child.nextElementSibling;
}
}
ensureIsVisible(index) {
const count = this.length;
if (count === 0) {
return;
}
if (index < 0) {
index = 0;
} else if (index >= count) {
index = count-1;
}
this.children[index].scrollIntoView({ block: "nearest",
inline: "nearest" });
}
set(index, item) {
let replaced = null;
if (index >= 0 && index < this.length) {
replaced = this._items[index];
this._items[index] = item;
this.populateChild(this.children[index], item, index);
}
return replaced;
}
remove(index) {
let removed = null;
if (index >= 0 && index < this.length) {
removed = this._items[index];
this._items.splice(index, 1);
this.removeChild(this.children[index]);
this.update(index);
}
return removed;
}
add(...newItems) {
this.insert(this.length, ...newItems);
}
insert(index, ...newItems) {
if (newItems.length === 0) {
return;
}
const count = this.length;
if (index < 0) {
index = 0;
} else if (index > count) {
index = count;
}
let before = (index === count)?  null : this.children[index];
let i = index;
for (let newItem of newItems) {
let child = document.createElement("div");
child.setAttribute("class", "xui-lst-item");
this.populateChild(child, newItem, i++);
this.insertBefore(child, before);
}
this._items.splice(index, 0, ...newItems);
this.update(index);
}
getSelection() {
if (this.isSingleSelectionMode()) {
return (this._selection.length > 0)? this._selection[0] : -1;
} else {
return [ ...this._selection ];
}
}
selectAll() {
let sel = [];
const count = this.length;
for (let i = 0; i < count; ++i) {
sel.push(i);
}
return this.setSelection(...sel);
}
clearSelection() {
return this.setSelection();
}
setSelection(...indices) {
if (this.isSingleSelectionMode() && indices.length > 0) {
indices = indices.slice(0, 1);
}
let prevSelection = this._selection;
this._selection = [];
const count = this.length;
for (let i = 0; i < count; ++i) {
let item = this._items[i];
let childClassList = this.children[i].classList;
if (indices.includes(i) && !this.itemIsDisabled(item)) {
this._selection.push(i);
childClassList.add("xui-lst-selected");
} else {
childClassList.remove("xui-lst-selected");
}
}
this.setAnchor(-1);
return prevSelection;
}
setAnchor(index) {
let oldAnchor = this._anchor;
this._anchor = this._extent = -1;
if (index >= 0 && index < this.length) {
this._anchor = this._extent = index;
}
if (this._anchor !== oldAnchor) {
if (oldAnchor >= 0) {
this.children[oldAnchor].classList.remove("xui-lst-anchor");
}
if (this._anchor >= 0) {
this.children[this._anchor].classList.add("xui-lst-anchor");
}
}
}
get anchor() {
return this._anchor;
}
get extent() {
return this._extent;
}
}
window.customElements.define("xui-list", List);
export class AutocompleteField extends HTMLElement {
constructor() {
super();
this._lenientTextChecker = null;
this._onSelectionChanged = this.onSelectionChanged.bind(this);
this._onDoubleClickList = this.onDoubleClickList.bind(this);
this._onKeydownList = this.onKeydownList.bind(this);
this._field = null;
this._list = null;
this._completionKey = null;
this._completionKeyIsAlt = this._completionKeyIsCtrl =
this._completionKeyIsMeta = this._completionKeyIsShift = false;
this._attrBeingChanged = null;
}
connectedCallback() {
if (this._field === null) {
this.classList.add("xui-control");
if (!this.hasAttribute("for")) {
this.for = null;
}
if (!this.hasAttribute("acceptmode")) {
this.acceptMode = "strict";
}
this.completionKey = this.getAttribute("completionkey");
this._field = document.createElement("input");
this._field.classList.add("xui-control", "xui-acfld-field");
this._field.setAttribute("type", "text");
this._field.setAttribute("autocomplete", "off");
this._field.setAttribute("spellcheck", "false");
this.appendChild(this._field);
this._field.addEventListener("keydown", this.onKeydown.bind(this));
this._field.addEventListener("input", this.onInput.bind(this));
}
}
static get observedAttributes() {
return [ "for", "acceptmode", "completionkey", "singleclicktoaccept" ];
}
attributeChangedCallback(attrName, oldVal, newVal) {
if (this._attrBeingChanged !== null) {
return;
}
switch (attrName) {
case "for":
this.for = newVal;
break;
case "acceptmode":
this.acceptMode = newVal;
break;
case "completionkey":
this.completionKey = newVal;
break;
case "singleclicktoaccept":
this.singleClickToAccept = newVal;
break;
}
}
onSelectionChanged(event) {
let sel = event.detail.selection;
if (sel < 0) {
this._field.value = "";
} else {
this._field.value = this._list.getLabel(sel);
if (event.detail.clickCount === 1 && this.singleClickToAccept) {
this.acceptSelection();
}
}
}
onDoubleClickList(event) {
if (event.button === 0) {
Util.consumeEvent(event);
if (!this.singleClickToAccept) {
this.acceptSelection();
}
}
}
acceptSelection() {
if (this._list === null) {
return false;
}
let acceptedText = this._field.value.trim();
if (acceptedText.length === 0) {
return false;
}
let selectedItem = null;
let selectedIndex = this._list.getSelection();
if (selectedIndex >= 0) {
let selectedLabel = this._list.getLabel(selectedIndex);
if (selectedLabel.startsWith(acceptedText)) {
selectedItem = this._list.get(selectedIndex);
this._field.value = acceptedText = selectedLabel;
}
}
if (selectedItem === null) {
if (this.acceptMode === "lenient") {
if (this._lenientTextChecker !== null) {
let checked = false;
try {
checked = this._lenientTextChecker(acceptedText);
} catch (error) {
console.error(`XUI.AutocompleteField.lenientTextChecker \
has failed: ${error}`);
}
if (!checked) {
acceptedText = null;
}
}
} else {
acceptedText = null;
}
}
if (acceptedText === null) {
this._field.select();
return false;
}
let event = new CustomEvent("selectionaccepted",
{ bubbles: false, composed: false,
cancelable: false,
detail: { acceptedText: acceptedText,
selectedIndex: selectedIndex,
selectedItem: selectedItem } });
this.dispatchEvent(event);
return true;
}
onKeydownList(event) {
if (event.key === "Enter") {
Util.consumeEvent(event);
let sel = this._list.getSelection();
if (sel >= 0) {
let label = this._list.getLabel(sel);
if (label !== this._field.value) {
this._field.value = label;
this._list.setAnchor(sel);
this._list.ensureIsVisible(sel);
}
this.acceptSelection();
}
}
}
onKeydown(event) {
if (this._list === null) {
return;
}
let isHotKey = true;
switch (event.key) {
case "ArrowUp":
case "Up":
case "ArrowDown":
case "Down":
{
let sel = this._list.getSelection();
let label;
if (sel >= 0 &&
(label = this._list.getLabel(sel)) !== this._field.value) {
this._field.value = label;
this._list.setAnchor(sel);
this._list.ensureIsVisible(sel);
} else {
if (event.key === "ArrowUp" || event.key === "Up") {
this._list.onUpKey();
} else {
this._list.onDownKey();
}
}
}
break;
case "Enter":
this.acceptSelection();
break;
default:
if (event.key === this._completionKey &&
event.altKey === this._completionKeyIsAlt &&
event.ctrlKey === this._completionKeyIsCtrl &&
event.metaKey === this._completionKeyIsMeta &&
event.shiftKey === this._completionKeyIsShift) {
this.autoComplete();
} else {
isHotKey = false;
}
break;
}
if (isHotKey) {
Util.consumeEvent(event);
}
}
autoComplete() {
if (this._list === null) {
return;
}
let prefix = this._field.value.trim();
if (prefix.length === 0) {
return;
}
let index = this.findByPrefix(prefix);
if (index < 0) {
return;
}
let curLabel = this._list.getLabel(index);
let nextLabels = [];
const count = this._list.length;
for (let i = index+1; i < count; ++i) {
let label = this._list.getLabel(i);
if (!label.startsWith(prefix)) {
break;
}
nextLabels.push(label);
}
let longestPrefix;
if (nextLabels.length === 0) {
longestPrefix = curLabel;
} else {
let j = prefix.length;
const curLabelLength = curLabel.length;
loop: for (; j < curLabelLength; ++j) {
let c = curLabel.charAt(j);
for (let nextLabel of nextLabels) {
if (j >= nextLabel.length ||
nextLabel.charAt(j) !== c) {
break loop;
}
}
}
longestPrefix = curLabel.substring(0, j);
}
if (longestPrefix !== prefix) {
this.text = longestPrefix;
}
}
findByPrefix(text) {
let index = -1;
const items = this._list.getAll( false);
const count = items.length;
for (let i = 0; i < count; ++i) {
let item = items[i];
if (this._list.itemLabel(item).startsWith(text)) {
index = i;
break;
}
}
return index;
}
onInput(event) {
if (this._list === null) {
return;
}
let prefix = this._field.value.trim();
if (prefix.length === 0) {
this._list.clearSelection();
this._list.ensureIsVisible(0);
return;
}
let index = this.findByPrefix(prefix);
if (index < 0) {
this._list.clearSelection();
} else {
if (this._list.isDisabled(index)) {
this._list.clearSelection();
} else {
this._list.setSelection(index);
}
this._list.setAnchor(index);
this._list.ensureIsVisible(index);
}
}
get for() {
return this.getAttribute("for");
}
set for(id) {
this._attrBeingChanged = "for";
if (this._list !== null) {
this._list.removeEventListener("selectionchanged",
this._onSelectionChanged);
this._list.removeEventListener("dblclick", this._onDoubleClickList);
this._list.removeEventListener("keydown", this._onKeydownList);
this._list = null;
}
if (id !== null && (id = id.trim()).length > 0) {
this._list = document.getElementById(id);
if (this._list !== null && this._list.localName !== "xui-list") {
this._list = null;
}
}
if (this._list === null) {
this.removeAttribute("for");
} else {
this._list.selectionMode = "single";
this._list.addEventListener("selectionchanged",
this._onSelectionChanged);
this._list.addEventListener("dblclick", this._onDoubleClickList);
this._list.addEventListener("keydown", this._onKeydownList);
this._list.singleClickToAccept = this.singleClickToAccept;
this.setAttribute("for", id);
}
this._attrBeingChanged = null;
}
get acceptMode() {
return this.getAttribute("acceptmode");
}
set acceptMode(mode) {
this._attrBeingChanged = "acceptmode";
this.setAttribute("acceptmode", (mode === "lenient")? mode : "strict");
this._attrBeingChanged = null;
}
get completionKey() {
return this.getAttribute("completionkey");
}
set completionKey(spec) {
this._attrBeingChanged = "completionkey";
this._completionKey = null;
this._completionKeyIsAlt = this._completionKeyIsCtrl =
this._completionKeyIsMeta = this._completionKeyIsShift = false;
if (spec !== null && spec.length > 0) {
let segments = spec.toLowerCase().split(/[-+]/);
if (segments.length > 0) {
this._completionKey = segments[segments.length-1];
if (this._completionKey === "space") {
this._completionKey = " ";
}
segments.pop();
if (segments.length > 0) {
for (let segment of segments) {
switch (segment) {
case "alt":
this._completionKeyIsAlt = true;
break;
case "ctrl":
this._completionKeyIsCtrl = true;
break;
case "meta":
this._completionKeyIsMeta = true;
break;
case "shift":
this._completionKeyIsShift = true;
break;
case "mod":
if (Util.PLATFORM_IS_MAC_OS) {
this._completionKeyIsMeta = true;
} else {
this._completionKeyIsCtrl = true;
}
break;
}
}
}
}
}
if (this._completionKey === null) {
this.removeAttribute("completionkey");
} else {
this.setAttribute("completionkey", spec);
}
this._attrBeingChanged = null;
}
get singleClickToAccept() {
return this.hasAttribute("singleclicktoaccept");
}
set singleClickToAccept(single) {
this._attrBeingChanged = "singleclicktoaccept";
if (single) {
this.setAttribute("singleclicktoaccept", "singleclicktoaccept");
} else {
this.removeAttribute("singleclicktoaccept");
}
this._attrBeingChanged = null;
if (this._list !== null) {
this._list.singleClickToAccept = single;
}
}
get lenientTextChecker() {
return this._lenientTextChecker;
}
set lenientTextChecker(checker) {
this._lenientTextChecker = checker;
}
get text() {
return this._field.value;
}
set text(value) {
if (value === null) {
value = "";
}
this._field.value = value;
this.onInput( null);
}
get autofocus() {
return this._field.autofocus;
}
set autofocus(focus) {
this._field.autofocus = focus;
}
}
window.customElements.define("xui-autocomplete-field", AutocompleteField);
export class ItemChooser extends Dialog {
static showChooser(title, items, initialText=null, options=null,
reference=null) {
return new Promise((resolve, reject) => {
let dialog = new ItemChooser(title, items, initialText, options,
resolve);
dialog.open("center", reference);
});
}
constructor(title, items, initialText, options, resolve) {
super({ movable: true, resizable: true,
title: title, closeable: true,
template: ItemChooser.TEMPLATE });
let pane = this.contentPane.firstElementChild;
let fieldPane = pane.firstElementChild;
let cancelButton = fieldPane.firstElementChild;
cancelButton.setAttribute("title", "Cancel");
cancelButton.textContent = StockIcon["cancel"];
cancelButton.onclick = this.cancelAction.bind(this);
let okButton = cancelButton.nextElementSibling;
okButton.setAttribute("title", "OK");
okButton.textContent = StockIcon["ok"];
okButton.onclick = this.okAction.bind(this);
this._field = fieldPane.lastElementChild;
this._list = pane.lastElementChild;
this._list.id = Util.uid();
this._field.setAttribute("for", this._list.id);
this._field.addEventListener("selectionaccepted",
this.onSelectionAccepted.bind(this));
this._items = items;
this._initialText = initialText;
this._options = options;
this._resolve = resolve;
}
open(position="center", reference=null) {
super.open(position, reference);
this._field.autofocus = true;
if (this._options !== null) {
for (let opt in this._options) {
let val = this._options[opt];
switch (opt.toLowerCase()) {
case "completionkey":
this._field.completionKey = val;
break;
case "acceptmode":
this._field.acceptMode =  val;
break;
case "singleclicktoaccept":
this._field.singleClickToAccept = val;
break;
case "lenienttextchecker":
this._field.lenientTextChecker = val;
break;
case "labelmaker":
this._list.labelMaker = val;
break;
case "htmlrenderer":
this._list.htmlRenderer = val;
break;
case "disabledchecker":
this._list.disabledChecker = val;
break;
case "columns":
{
let len = ItemChooser.countToLength(val);
if (len !== null) {
this._field.style.width = len;
}
}
break;
case "rows":
{
let len = ItemChooser.countToLength(val);
if (len !== null) {
this._list.style.height = len;
}
}
break;
default:
console.warn(`ItemChooser: ignoring option "${opt}"`);
break;
}
}
}
this._list.setAll(this._items);
if (this._initialText !== null) {
this._field.text = this._initialText;
}
}
static countToLength(val, mult=1) {
let num = Number.parseInt(val);
if (Number.isNaN(num) || num <= 0) {
return null;
}
return String(mult * num) + "em";
}
onSelectionAccepted(event) {
this.close([ event.detail.acceptedText,
event.detail.selectedIndex, event.detail.selectedItem ]);
}
dialogClosed(result) {
this._resolve(result);
}
cancelAction() {
this.close(null);
}
okAction() {
this._field.acceptSelection();
}
}
ItemChooser.TEMPLATE = document.createElement("template");
ItemChooser.TEMPLATE.innerHTML = `
<div class="xui-item-chooser-pane">
  <div class="xui-itmchsr-field-pane">
    <button type="button"
            class="xui-control xui-small-icon xui-itmchsr-cancel"></button>
    <button type="button"
            class="xui-control xui-small-icon xui-dlg-default-button
                   xui-itmchsr-ok"></button>
    <xui-autocomplete-field class="xui-itmchsr-field"></xui-autocomplete-field>
  </div>
  <xui-list class="xui-itmchsr-list"></xui-list>
</div>
`;
export class Tabs extends HTMLElement {
constructor() {
super();
this._closeableTabs = null;
this._newTabButton = null;
this._selectTab = this.selectTab.bind(this);
this._closeTab = this.closeTab.bind(this);
this._head = null;
this._body = null;
this._tabPairs = null;
}
connectedCallback() {
let head = this.firstElementChild;
if (head !== null && head.classList.contains("xui-tabs-head")) {
return;
}
this.classList.add("xui-control");
this.setAttribute("tabindex", "0");
this._closeableTabs = this.getAttribute("closeabletabs");
this._newTabButton = this.getAttribute("newtabbutton");
let childElements = [];
let child = this.firstChild;
while (child !== null) {
const nextChild = child.nextSibling;
if (child.nodeType === Node.ELEMENT_NODE) {
childElements.push(child);
}
this.removeChild(child);
child = nextChild;
}
this._head = document.createElement("div");
this._head.setAttribute("class", "xui-control xui-tabs-head");
this.appendChild(this._head);
this._body = document.createElement("div");
this._body.setAttribute("class", "xui-tabs-body");
this.appendChild(this._body);
const childCount = 2 * Math.floor(childElements.length / 2);
for (let i = 0; i < childCount; i += 2) {
const label = childElements[i];
const component = childElements[i+1];
const [tab, host] = this.createTab(label, component);
this._head.appendChild(tab);
this._body.appendChild(host);
}
if (this._newTabButton !== null) {
const tab = document.createElement("span");
let tooltip = this._newTabButton;
if (tooltip.trim().length === 0) {
tooltip = "Open a new tab";
}
tab.setAttribute("title", tooltip);
tab.addEventListener("click", this.newTab.bind(this));
tab.setAttribute("class", "xui-tabs-new-tab");
const button = document.createElement("span");
button.setAttribute("class",
"xui-control xui-small-icon xui-tabs-new-button");
button.textContent = StockIcon["plus"];
tab.appendChild(button);
this._head.appendChild(tab);
}
if (childCount > 0) {
this.selected = 0;
}
}
createTab(label, component) {
const tab = document.createElement("span");
tab.setAttribute("class", "xui-tabs-tab");
tab.addEventListener("click", this._selectTab);
tab.appendChild(label);
if (this._closeableTabs !== null) {
const button = document.createElement("span");
button.setAttribute("class",
"xui-control xui-small-icon xui-tabs-close-button");
let tooltip = this._closeableTabs;
if (tooltip.trim().length === 0) {
tooltip = "Close tab";
}
button.setAttribute("title", tooltip);
button.textContent = StockIcon["cancel"];
button.addEventListener("click", this._closeTab);
tab.appendChild(button);
}
const host = document.createElement("div");
host.setAttribute("class", "xui-tabs-host");
host.appendChild(component);
return [tab, host];
}
selectTab(event) {
if (event.button === 0 && event.detail === 1) {
Util.consumeEvent(event);
const tab = event.currentTarget;
const label = tab.firstElementChild;
const index = this.indexOfLabel(label);
if (index >= 0 && this.selected !== index) {
const tabPair = this.get(index);
assertOrError(tabPair !== null && Object.is(tabPair[0], label));
const component = tabPair[1];
const tabsEvent = Tabs.newEvent("selectingtab",
index, label, component);
this.dispatchEvent(tabsEvent);
if (tabsEvent.defaultPrevented) {
return;
}
this.selected = index;
this.dispatchEventLater(Tabs.newEvent("tabselected",
index, label, component));
}
}
}
static newEvent(type, index, label, component) {
return new CustomEvent(type,
{ bubbles: false, composed: false,
cancelable: type.endsWith("tab"),
detail: { index: index, label: label,
component: component }});
}
dispatchEventLater(event) {
window.setTimeout(() => {
this.dispatchEvent(event);
});
}
indexOfLabel(label) {
const tabs = this.all;
const count = tabs.length;
for (let i = 0; i < count; ++i) {
let [l, c] = tabs[i];
if (Object.is(l, label)) {
return i;
}
}
return -1;
}
newTab(event) {
if (event.button === 0 && event.detail === 1) {
Util.consumeEvent(event);
const index = this.count;
const tabsEvent = Tabs.newEvent("addingtab", index, null, null);
this.dispatchEvent(tabsEvent);
let label = null, component = null;
if (tabsEvent.defaultPrevented ||
(label = tabsEvent.detail.label) === null ||
(component = tabsEvent.detail.component) === null) {
return;
}
this.add(label, component);
this.dispatchEventLater(Tabs.newEvent("tabadded",
index, label, component));
}
}
closeTab(event) {
if (event.button === 0 && event.detail === 1) {
Util.consumeEvent(event);
const button = event.currentTarget;
const label = button.parentElement.firstElementChild;
const index = this.indexOfLabel(label);
if (index >= 0) {
const tabPair = this.get(index);
assertOrError(tabPair !== null && Object.is(tabPair[0], label));
const component = tabPair[1];
const tabsEvent = Tabs.newEvent("closingtab",
index, label, component);
this.dispatchEvent(tabsEvent);
if (tabsEvent.defaultPrevented) {
return;
}
this.remove(index);
this.dispatchEventLater(Tabs.newEvent("tabclosed",
index, label, component));
}
}
}
get selected() {
let child = this._body.firstElementChild;
let i = 0;
while (child !== null) {
if (child.classList.contains("xui-tabs-selected")) {
return i;
}
child = child.nextElementSibling;
++i;
}
return -1;
}
set selected(index) {
const tabs = this._head.children;
const hosts = this._body.children;
const count = hosts.length;
for (let i = 0; i < count; ++i) {
const tab = tabs[i];
const host = hosts[i];
if (i === index) {
tab.classList.add("xui-tabs-selected");
host.classList.add("xui-tabs-selected");
} else {
tab.classList.remove("xui-tabs-selected");
host.classList.remove("xui-tabs-selected");
}
}
}
get all() {
if (this._tabPairs === null) {
let tabPairs = [];
const tabs = this._head.children;
const hosts = this._body.children;
const count = hosts.length;
for (let i = 0; i < count; ++i) {
tabPairs.push([ tabs.item(i).firstElementChild,
hosts.item(i).firstElementChild ]);
}
this._tabPairs = tabPairs;
}
return this._tabPairs;
}
get count() {
return this.all.length;
}
get(index) {
const tabPairs = this.all;
if (index < 0 || index >= tabPairs.length) {
return null;
} else {
return tabPairs[index];
}
}
add(label, component) {
this.insert(this.count, label, component);
}
insert(index, label, component) {
const tabs = this._head.children;
const hosts = this._body.children;
const count = hosts.length;
if (index >= 0 && index <= count) {
let beforeTab, beforeHost;
if (index < count) {
beforeTab = tabs[index];
beforeHost = hosts[index];
} else {
beforeTab = (this._newTabButton !== null)? tabs[count] : null;
beforeHost = null;
}
const [tab, host] = this.createTab(label, component);
this._head.insertBefore(tab, beforeTab);
this._body.insertBefore(host, beforeHost);
this._tabPairs = null;
this.selected = index;
}
}
remove(index) {
let removed = null;
const tabs = this._head.children;
const hosts = this._body.children;
const count = hosts.length;
if (index >= 0 && index < count) {
let select = -1;
if (index + 1 < count) {
select = index;
} else {
select = index - 1;
}
removed = [ tabs[index].firstElementChild,
hosts[index].firstElementChild ];
this._head.removeChild(tabs[index]);
this._body.removeChild(hosts[index]);
this._tabPairs = null;
if (select >= 0) {
this.selected = select;
}
}
return removed;
}
}
window.customElements.define("xui-tabs", Tabs);
