/**
* Client-side implementation of CSS proprietary extension
* <code>command-button()</code>. This is specified as part of the generated
* content and it inserts a button which can be used to execute a command.
*/
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, // px
"icon-position": "left",
"select": true,
};
const blockEvent = (event) => {
XUI.Util.consumeEvent(event);
};
for (const eventName of ["mousedown", "mousemove", "mouseup",
"auxclick"]) {
this.addEventListener(eventName, blockEvent);
}
const onButtonClicked = this.onButtonClicked.bind(this);
this.addEventListener("click", onButtonClicked);
this.addEventListener("contextmenu", onButtonClicked);
}
// -----------------------------------------------------------------------
// Event handlers
// -----------------------------------------------------------------------
onButtonClicked(event) {
XUI.Util.consumeEvent(event);
if (this._docView === null || // Should not happen.
(event.type === "click" && event.detail !== 1)) { // Click count.
return;
}
let view = NodeView.lookupView(this);
if (view === null || NodeView.uidIfElementView(view) === null) {
// Not an element view. Should not happen.
return;
}
let selectElem;
if (this._options["select"] &&
(!Object.is(this._docView.selected, view) ||
this._docView.selected2 !== null)) {
selectElem = this._docView.selectNode(view, /*show*/ 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") {
// LIMITATION:
// Only position which works with a
// contextmenu event.
// Otherwise, a mouseup follows it inside the
// opened menu and may automatically select an
// item then automatically close the menu.
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) {
// List of triplets:
// label | null (for a separator; in this case, the triplet is all null)
// command_name | null (for a submenu)
// command_parameter (possibly null) | list of triplets (for a submenu)
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.xuiMenuItem.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) {
// Submenu ---
if (!CommandButton.isMenuSpec(param)) {
continue;
}
let submenuItems = CommandButton.createMenuItems(param);
if (submenuItems.length === 0) {
continue;
}
menuItem.type = "submenu";
menuItem.items = submenuItems;
} else {
// Command ---
menuItem.type = "button";
menuItem.name =
Command.joinCmdString(name, !param? null : param,
CommandButton.CMD_STRING_SEPAR);
if (menuItem.name === null) {
continue; // Unusable.
}
}
if (addSepar) {
addSepar = false;
menuItem.separator = true;
}
menuItems.push(menuItem);
}
}
return menuItems;
}
// -----------------------------------------------------------------------
// Custom element
// -----------------------------------------------------------------------
connectedCallback() {
this._docView = DOMUtil.lookupAncestorByTag(this, "xxe-document-view");
if (this._docView === null) {
// Should not happen.
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: // "left"
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);
}
}
}
}
// A command name may contain space characters, e.g. "{DITA Map}promote".
// (BMP PUA: U+E000..U+F8FF)
CommandButton.CMD_STRING_SEPAR = '\uF876';
window.customElements.define("xxe-command-button", CommandButton);