Files
tubestation/waterfox/browser/components/sidebar/extlib/MenuUI.js
2025-11-06 14:13:52 +00:00

1145 lines
37 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
license: The MIT License, Copyright (c) 2018-2025 YUKI "Piro" Hiroshi
original:
https://github.com/piroor/webextensions-lib-menu-ui
*/
'use strict';
{
class MenuUI {
static $wait(timeout) {
return new Promise((resolve, _reject) => {
setTimeout(resolve, timeout);
});
}
// XPath Utilities
static $hasClass(className) {
return `contains(concat(" ", normalize-space(@class), " "), " ${className} ")`;
};
static $evaluateXPath(expression, context, type) {
if (!type)
type = XPathResult.ORDERED_NODE_SNAPSHOT_TYPE;
try {
return (context.ownerDocument || context).evaluate(
expression,
(context || document),
null,
type,
null
);
}
catch(_e) {
return {
singleNodeValue: null,
snapshotLength: 0,
snapshotItem: function() {
return null
}
};
}
}
static $getArrayFromXPathResult(result) {
const max = result.snapshotLength;
const array = new Array(max);
if (!max)
return array;
for (let i = 0; i < max; i++) {
array[i] = result.snapshotItem(i);
}
return array;
}
RTL_LANGUAGES = new Set([
'ar',
'he',
'fa',
'ur',
'ps',
'sd',
'ckb',
'prs',
'rhg',
]);
get isRTL() {
const lang = (
navigator.language ||
navigator.userLanguage ||
//(new Intl.DateTimeFormat()).resolvedOptions().locale ||
''
).split('-')[0];
return this.RTL_LANGUAGES.has(lang);
}
constructor(params = {}) {
this.$lastHoverItem = null;
this.$lastFocusedItem = null;
this.$mouseDownFired = false;
this.root = params.root;
this.onCommand = params.onCommand || (() => {});
this.onShown = params.onShown || (() => {});
this.onHidden = params.onHidden || (() => {});
this.animationDuration = params.animationDuration || 150;
this.subMenuOpenDelay = params.subMenuOpenDelay || 300;
this.subMenuCloseDelay = params.subMenuCloseDelay || 300;
this.appearance = params.appearance || 'menu';
this.incrementalSearch = params.incrementalSearch || false;
this.incrementalSearchTimeout = params.incrementalSearchTimeout || 1000;
this.$onBlur = this.$onBlur.bind(this);
this.$onMouseOver = this.$onMouseOver.bind(this);
this.$onMouseDown = this.$onMouseDown.bind(this);
this.$onMouseUp = this.$onMouseUp.bind(this);
this.$onClick = this.$onClick.bind(this);
this.$onKeyDown = this.$onKeyDown.bind(this);
this.$onKeyUp = this.$onKeyUp.bind(this);
this.$onTransitionEnd = this.$onTransitionEnd.bind(this);
this.$onContextMenu = this.$onContextMenu.bind(this);
this.root.classList.toggle('rtl', this.isRTL);
if (!this.root.id)
this.root.id = `MenuUI-root-${this.$uniqueKey}-${parseInt(Math.random() * Math.pow(2, 16))}`;
this.root.classList.add(this.$commonClass);
this.root.classList.add('menu-ui');
this.root.classList.add(this.appearance);
this.root.setAttribute('role', 'menu');
this.$screen = document.createElement('div');
this.$screen.classList.add(this.$commonClass);
this.$screen.classList.add('menu-ui-blocking-screen');
this.root.parentNode.insertBefore(this.$screen, this.root.nextSibling);
this.$marker = document.createElement('span');
this.$marker.classList.add(this.$commonClass);
this.$marker.classList.add('menu-ui-marker');
this.$marker.classList.add(this.appearance);
this.root.parentNode.insertBefore(this.$marker, this.root.nextSibling);
this.$lastKeyInputAt = -1;
this.$incrementalSearchString = '';
}
get opened() {
return this.root.classList.contains('open');
}
$updateAccessKey(item) {
const ACCESS_KEY_MATCHER = /(&([^\s]))/i;
const title = item.getAttribute('title');
if (title)
item.setAttribute('title', title.replace(ACCESS_KEY_MATCHER, '$2'));
const label = MenuUI.$evaluateXPath('child::text()', item, XPathResult.STRING_TYPE).stringValue;
item.dataset.lowerCasedText = label.toLowerCase();
const matchedKey = label.match(ACCESS_KEY_MATCHER);
if (matchedKey) {
const textNode = MenuUI.$evaluateXPath(
`child::node()[contains(self::text(), "${matchedKey[1]}")]`,
item,
XPathResult.FIRST_ORDERED_NODE_TYPE
).singleNodeValue;
if (textNode) {
const range = document.createRange();
const startPosition = textNode.nodeValue.indexOf(matchedKey[1]);
range.setStart(textNode, startPosition);
range.setEnd(textNode, startPosition + 2);
range.deleteContents();
const accessKeyNode = document.createElement('span');
accessKeyNode.classList.add('accesskey');
accessKeyNode.textContent = matchedKey[2];
range.insertNode(accessKeyNode);
range.detach();
}
item.dataset.accessKey = matchedKey[2].toLowerCase();
}
else if (/^([^\s])/i.test(item.textContent))
item.dataset.subAccessKey = RegExp.$1.toLowerCase();
else
item.dataset.accessKey = item.dataset.subAccessKey = null;
}
async open(options = {}) {
if (this.closeTimeout) {
clearTimeout(this.closeTimeout);
delete this.closeTimeout;
this.$onClosed();
}
this.canceller = options.canceller;
this.$mouseDownAfterOpen = false;
this.$lastFocusedItem = null;
this.$lastHoverItem = null;
this.anchor = options.anchor;
this.$updateItems(this.root);
this.root.classList.add('open');
this.$screen.classList.add('open');
this.$marker.classList.remove('top');
this.$marker.classList.remove('bottom');
if (this.anchor) {
this.anchor.classList.add('open');
this.$marker.style.transition = `opacity ${this.animationDuration}ms ease-out`;
this.$marker.classList.add('open');
}
this.$updatePositions(this.root, options);
this.onShown();
return new Promise(async (resolve, _reject) => {
await MenuUI.$wait(0);
if (this.$tryCancelOpen()) {
this.close().then(resolve);
return;
}
await MenuUI.$wait(this.animationDuration);
if (this.$tryCancelOpen()) {
this.close().then(resolve);
return;
}
this.root.parentNode.addEventListener('mouseover', this.$onMouseOver);
this.root.addEventListener('transitionend', this.$onTransitionEnd);
window.addEventListener('contextmenu', this.$onContextMenu, { capture: true });
window.addEventListener('mousedown', this.$onMouseDown, { capture: true });
window.addEventListener('mouseup', this.$onMouseUp, { capture: true });
window.addEventListener('click', this.$onClick, { capture: true });
window.addEventListener('keydown', this.$onKeyDown, { capture: true });
window.addEventListener('keyup', this.$onKeyUp, { capture: true });
window.addEventListener('blur', this.$onBlur, { capture: true });
resolve();
});
}
$tryCancelOpen() {
if (!(typeof this.canceller == 'function'))
return false;
try {
return this.canceller();
}
catch(_e) {
}
return false;
}
updateMenuItem(item) {
this.$updateItems(item);
const submenu = item.querySelector('ul');
if (submenu)
this.$updatePositions(submenu);
}
$updateItems(parent) {
parent.setAttribute('role', 'menu');
for (const item of parent.querySelectorAll('li:not(.separator)')) {
item.setAttribute('tabindex', -1);
item.classList.remove('open');
if (item.classList.contains('checkbox'))
item.setAttribute('role', 'menuitemcheckbox');
else if (item.classList.contains('radio'))
item.setAttribute('role', 'menuitemradio');
else if (item.classList.contains('separator'))
item.setAttribute('role', 'separator');
else
item.setAttribute('role', 'menuitem');
if (item.matches('.checked, .radio')) {
if (item.classList.contains('checked'))
item.setAttribute('aria-checked', 'true');
else
item.setAttribute('aria-checked', 'false');
}
else {
item.removeAttribute('aria-checked');
}
this.$updateAccessKey(item);
const icon = item.querySelector('span.icon') || document.createElement('span');
if (!icon.parentNode) {
icon.classList.add('icon');
item.insertBefore(icon, item.firstChild);
}
if (item.dataset.icon) {
if (item.dataset.iconColor) {
item.style.backgroundImage = '';
icon.style.backgroundColor = item.dataset.iconColor;
icon.style.mask = `url(${JSON.stringify(item.dataset.icon)}) no-repeat center / 100%`;
}
else {
item.style.backgroundImage = `url(${JSON.stringify(item.dataset.icon)})`;
icon.style.backgroundColor =
icon.style.mask = '';
}
}
else {
item.style.backgroundImage =
icon.style.backgroundColor =
icon.style.mask = '';
}
if (item.querySelector('ul'))
item.classList.add('has-submenu');
else
item.classList.remove('has-submenu');
}
}
$updatePositions(parent, options = {}) {
const menus = [parent].concat(Array.from(parent.querySelectorAll('ul')));
for (const menu of menus) {
if (this.animationDuration)
menu.style.transition = `opacity ${this.animationDuration}ms ease-out`;
else
menu.style.transition = '';
this.$updatePosition(menu, options);
}
}
$updatePosition(menu, options = {}) {
let left = options.left;
let top = options.top;
const containerRect = this.$containerRect;
const menuRect = menu.getBoundingClientRect();
if (options.anchor &&
(left === undefined || top === undefined) &&
menu == this.root) {
const anchorRect = options.anchor.getBoundingClientRect();
if (containerRect.bottom - anchorRect.bottom >= menuRect.height) {
top = anchorRect.bottom;
this.$marker.classList.add('top');
this.$marker.classList.remove('bottom');
this.$marker.style.top = `calc(${top}px - 0.4em)`;
}
else if (anchorRect.top - containerRect.top >= menuRect.height) {
top = Math.max(0, anchorRect.top - menuRect.height);
this.$marker.classList.add('bottom');
this.$marker.classList.remove('top');
this.$marker.style.top = `calc(${top}px + ${menuRect.height}px - 0.6em)`;
}
else {
top = Math.max(0, containerRect.top - menuRect.height);
this.$marker.classList.remove('bottom');
this.$marker.classList.remove('top');
this.$marker.style.top = `calc(${top}px + ${menuRect.height}px - 0.6em)`;
}
const canPlaceAtRight = containerRect.right - anchorRect.left >= menuRect.width;
const canPlaceAtLeft = anchorRect.left - containerRect.left >= menuRect.width;
if (canPlaceAtRight || canPlaceAtLeft) {
if (this.isRTL) {
if (canPlaceAtLeft) {
left = Math.max(0, anchorRect.right - menuRect.width);
this.$marker.style.left = `calc(${left}px + ${menuRect.width}px - 1.5em)`;
}
else {
left = anchorRect.left;
this.$marker.style.left = `calc(${left}px + 0.5em)`;
}
}
else {
if (canPlaceAtRight) {
left = anchorRect.left;
this.$marker.style.left = `calc(${left}px + 0.5em)`;
}
else {
left = Math.max(0, anchorRect.right - menuRect.width);
this.$marker.style.left = `calc(${left}px + ${menuRect.width}px - 1.5em)`;
}
}
}
else {
left = Math.max(0, containerRect.left - menuRect.width);
this.$marker.style.left = `calc(${left}px + ${menuRect.width}px - 1.5em)`;
}
}
let parentRect;
if (menu.parentNode.localName == 'li') {
parentRect = menu.parentNode.getBoundingClientRect();
left = this.isRTL ? parentRect.left - menuRect.width : parentRect.right;
top = parentRect.top;
}
if (left === undefined)
left = Math.max(0, (containerRect.width - menuRect.width) / 2);
else if (this.isRTL)
left -= menuRect.width;
if (top === undefined)
top = Math.max(0, (containerRect.height - menuRect.height) / 2);
if (!options.anchor && menu == this.root) {
// reposition to avoid the menu is opened below the cursor
if (containerRect.bottom - top < menuRect.height) {
top = top - menuRect.height;
}
if (containerRect.right - left < menuRect.width) {
left = left - menuRect.width;
}
}
const minMargin = 3;
const overwrap = 4;
const firstTryLeft = Math.max(minMargin, Math.min(left - overwrap, containerRect.width - menuRect.width - minMargin));
if (parentRect &&
firstTryLeft < parentRect.right - overwrap &&
containerRect.left < parentRect.left - menuRect.width + overwrap) {
left = parentRect.left - menuRect.width + overwrap;
}
else {
left = firstTryLeft;
}
menu.style.left = `${left}px`;
top = Math.max(minMargin, Math.min(top, containerRect.height - menuRect.height - minMargin));
if (menu == this.root && this.$marker.classList.contains('top'))
menu.style.top = `calc(${top}px + 0.5em)`;
else if (menu == this.root && this.$marker.classList.contains('bottom'))
menu.style.top = `calc(${top}px - 0.5em)`;
else
menu.style.top = `${top}px`;
}
async close() {
if (!this.opened)
return;
this.$tryCancelOpen();
this.root.classList.remove('open');
this.$screen.classList.remove('open');
if (this.anchor) {
this.anchor.classList.remove('open');
this.$marker.classList.remove('open');
}
this.$mouseDownAfterOpen = false;
this.$lastFocusedItem = null;
this.$lastHoverItem = null;
this.$mouseDownFired = false;
this.anchor = null;
this.canceller = null;
return new Promise((resolve, _reject) => {
this.closeTimeout = setTimeout(() => {
delete this.closeTimeout;
this.$onClosed();
resolve();
}, this.animationDuration);
});
}
$onClosed() {
const menus = [this.root].concat(Array.from(this.root.querySelectorAll('ul')));
for (const menu of menus) {
this.$updatePosition(menu, { left: 0, right: 0 });
}
this.root.parentNode.removeEventListener('mouseover', this.$onMouseOver);
this.root.removeEventListener('transitionend', this.$onTransitionEnd);
window.removeEventListener('contextmenu', this.$onContextMenu, { capture: true });
window.removeEventListener('mousedown', this.$onMouseDown, { capture: true });
window.removeEventListener('mouseup', this.$onMouseUp, { capture: true });
window.removeEventListener('click', this.$onClick, { capture: true });
window.removeEventListener('keydown', this.$onKeyDown, { capture: true });
window.removeEventListener('keyup', this.$onKeyUp, { capture: true });
window.removeEventListener('blur', this.$onBlur, { capture: true });
this.onHidden();
}
get $containerRect() {
const x = 0;
const y = 0;
const width = window.innerWidth;
const height = window.innerHeight;
return {
x, y, width, height,
left: x,
top: y,
right: width,
bottom: height
};
}
focusTo(item) {
this.$lastFocusedItem = this.$lastHoverItem = item;
this.$lastFocusedItem.focus();
this.$lastFocusedItem.scrollIntoView({ block: 'nearest' });
}
$onBlur(event) {
if (event.target == document)
this.close();
}
$onMouseOver(event) {
let item = this.$getEffectiveItem(event.target);
if (this.delayedOpen && this.delayedOpen.item != item) {
clearTimeout(this.delayedOpen.timer);
this.delayedOpen = null;
}
if (item && item.delayedClose) {
clearTimeout(item.delayedClose);
item.delayedClose = null;
}
if (item && item.classList.contains('separator')) {
this.$lastHoverItem = item;
item = null;
}
if (!item) {
if (this.$lastFocusedItem) {
if (this.$lastFocusedItem.parentNode != this.root) {
this.focusTo(this.$lastFocusedItem.parentNode.parentNode);
}
else {
this.$lastFocusedItem.blur();
this.$lastFocusedItem = null;
}
}
this.$setHover(null);
return;
}
this.$setHover(item);
this.$closeOtherSubmenus(item);
this.focusTo(item);
this.delayedOpen = {
item: item,
timer: setTimeout(() => {
this.delayedOpen = null;
this.$openSubmenuFor(item);
}, this.subMenuOpenDelay)
};
}
$setHover(item) {
for (const item of this.root.querySelectorAll('li.hover')) {
if (item != item)
item.classList.remove('hover');
}
if (item)
item.classList.add('hover');
}
$openSubmenuFor(item) {
const items = MenuUI.$evaluateXPath(
`ancestor-or-self::li[${MenuUI.$hasClass('has-submenu')}][not(${MenuUI.$hasClass('disabled')})]`,
item
);
for (const item of MenuUI.$getArrayFromXPathResult(items)) {
item.classList.add('open');
}
}
$closeOtherSubmenus(item) {
const items = MenuUI.$evaluateXPath(
`preceding-sibling::li[${MenuUI.$hasClass('has-submenu')}] |
following-sibling::li[${MenuUI.$hasClass('has-submenu')}] |
preceding-sibling::li/descendant::li[${MenuUI.$hasClass('has-submenu')}] |
following-sibling::li/descendant::li[${MenuUI.$hasClass('has-submenu')}]`,
item
);
for (const item of MenuUI.$getArrayFromXPathResult(items)) {
item.delayedClose = setTimeout(() => {
item.classList.remove('open');
}, this.subMenuCloseDelay);
}
}
$onMouseDown(event) {
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
this.$mouseDownAfterOpen = true;
this.$mouseDownFired = true;
}
$getEffectiveItem(node) {
const target = node.closest('li');
let untransparentTarget = target && target.closest('ul');
while (untransparentTarget) {
if (parseFloat(window.getComputedStyle(untransparentTarget, null).opacity) < 1)
return null;
untransparentTarget = untransparentTarget.parentNode.closest('ul');
if (untransparentTarget == document)
break;
}
return target;
}
$onMouseUp(event) {
if (!this.$mouseDownAfterOpen &&
event.target.closest(`#${this.root.id}`))
this.$onClick(event);
}
async $onClick(event) {
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
const target = this.$getEffectiveItem(event.target);
if (!target ||
target.classList.contains('separator') ||
target.classList.contains('has-submenu') ||
target.classList.contains('disabled')) {
if (this.$mouseDownFired && // ignore "click" event triggered by a mousedown fired before the menu is opened (like long-press)
!event.target.closest(`#${this.root.id}`))
return this.close();
return;
}
this.onCommand(target, event);
}
$getNextFocusedItemByAccesskey(key) {
for (const attribute of ['access-key', 'sub-access-key']) {
const current = this.$lastHoverItem || this.$lastFocusedItem || this.root.firstChild;
const condition = `@data-${attribute}="${key.toLowerCase()}"`;
const item = this.$getNextItem(current, condition);
if (item)
return item;
}
return null;
}
$incrementalSearchNextFocusedItem(key) {
this.$incrementalSearchString += key.toLowerCase();
const current = this.$lastHoverItem || this.$lastFocusedItem || this.root.firstChild;
const condition = `starts-with(@data-lower-cased-text, "${this.$incrementalSearchString.replace(/"/g, '\\"')}")`;
if (this.$isItemMatches(current, condition))
return current;
const item = this.$getNextItem(current, condition);
return item;
}
$shouldSearchIncremental() {
if (!this.incrementalSearch)
return false;
const last = this.$lastKeyInputAt;
const now = this.$lastKeyInputAt = Date.now();
if (last < 0)
return true; // start
if (now - last > this.incrementalSearchTimeout)
this.$incrementalSearchString = '';
return true; // continue
}
$onKeyDown(event) {
switch (event.key) {
case 'ArrowUp':
event.stopPropagation();
event.preventDefault();
this.$advanceFocus(-1);
break;
case 'ArrowDown':
event.stopPropagation();
event.preventDefault();
this.$advanceFocus(1);
break;
case 'ArrowRight':
event.stopPropagation();
event.preventDefault();
if (this.isRTL)
this.$digOut();
else
this.$digIn();
break;
case 'ArrowLeft':
event.stopPropagation();
event.preventDefault();
if (this.isRTL)
this.$digIn();
else
this.$digOut();
break;
case 'Home':
event.stopPropagation();
event.preventDefault();
this.$advanceFocus(1, (
this.$lastHoverItem && this.$lastHoverItem.parentNode ||
this.$lastFocusedItem && this.$lastFocusedItem.parentNode ||
this.root
).lastChild);
break;
case 'End':
event.stopPropagation();
event.preventDefault();
this.$advanceFocus(-1, (
this.$lastHoverItem && this.$lastHoverItem.parentNode ||
this.$lastFocusedItem && this.$lastFocusedItem.parentNode ||
this.root
).firstChild);
break;
case 'Enter': {
event.stopPropagation();
event.preventDefault();
const targetItem = this.$lastHoverItem || this.$lastFocusedItem;
if (targetItem) {
if (targetItem.classList.contains('disabled'))
this.close();
else if (!targetItem.classList.contains('separator'))
this.onCommand(targetItem, event);
}
}; break;
case 'Escape': {
event.stopPropagation();
event.preventDefault();
const targetItem = this.$lastHoverItem || this.$lastFocusedItem;
if (!targetItem ||
targetItem.parentNode == this.root)
this.close();
else
this.$digOut();
}; break;
case 'BackSpace': {
if (this.$shouldSearchIncremental()) {
event.stopPropagation();
event.preventDefault();
this.$incrementalSearchString = this.$incrementalSearchString.slice(0, this.$incrementalSearchString.length - 1);
const item = this.$incrementalSearchNextFocusedItem('');
if (item) {
this.focusTo(item);
this.$setHover(null);
}
}
}; break;
default:
if (event.key.length == 1) {
if (this.$shouldSearchIncremental()) {
event.stopPropagation();
event.preventDefault();
const item = this.$incrementalSearchNextFocusedItem(event.key);
if (item) {
this.focusTo(item);
this.$setHover(null);
}
return;
}
const item = this.$getNextFocusedItemByAccesskey(event.key);
if (item) {
this.focusTo(item);
this.$setHover(null);
if (this.$getNextFocusedItemByAccesskey(event.key) == item &&
!item.classList.contains('disabled')) {
if (item.querySelector('ul'))
this.$digIn();
else
this.onCommand(item, event);
}
}
}
return;
}
}
$onKeyUp(event) {
switch (event.key) {
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowRight':
case 'ArrowLeft':
case 'Home':
case 'End':
case 'Enter':
case 'Escape':
case 'Bakcspace':
event.stopPropagation();
event.preventDefault();
return;
default:
if (event.key.length == 1 &&
(this.$shouldSearchIncremental() ||
this.$getNextFocusedItemByAccesskey(event.key))) {
event.stopPropagation();
event.preventDefault();
}
return;
}
}
$getPreviousItem(base, condition = '') {
const extrcondition = condition ? `[${condition}]` : '' ;
const item = (
MenuUI.$evaluateXPath(
`preceding-sibling::li[not(${MenuUI.$hasClass('separator')})]${extrcondition}[1]`,
base,
XPathResult.FIRST_ORDERED_NODE_TYPE
).singleNodeValue ||
MenuUI.$evaluateXPath(
`following-sibling::li[not(${MenuUI.$hasClass('separator')})]${extrcondition}[last()]`,
base,
XPathResult.FIRST_ORDERED_NODE_TYPE
).singleNodeValue ||
MenuUI.$evaluateXPath(
`self::li[not(${MenuUI.$hasClass('separator')})]${extrcondition}`,
base,
XPathResult.FIRST_ORDERED_NODE_TYPE
).singleNodeValue
);
if (window.getComputedStyle(item, null).display == 'none')
return this.$getPreviousItem(item, condition);
return item;
}
$getNextItem(base, condition = '') {
const extrcondition = condition ? `[${condition}]` : '' ;
const item = (
MenuUI.$evaluateXPath(
`following-sibling::li[not(${MenuUI.$hasClass('separator')})]${extrcondition}[1]`,
base,
XPathResult.FIRST_ORDERED_NODE_TYPE
).singleNodeValue ||
MenuUI.$evaluateXPath(
`preceding-sibling::li[not(${MenuUI.$hasClass('separator')})]${extrcondition}[last()]`,
base,
XPathResult.FIRST_ORDERED_NODE_TYPE
).singleNodeValue ||
MenuUI.$evaluateXPath(
`self::li[not(${MenuUI.$hasClass('separator')})]${extrcondition}`,
base,
XPathResult.FIRST_ORDERED_NODE_TYPE
).singleNodeValue
);
if (item && window.getComputedStyle(item, null).display == 'none')
return this.$getNextItem(item, condition);
return item;
}
$isItemMatches(base, condition = '') {
const extrcondition = condition ? `[${condition}]` : '' ;
return !!MenuUI.$evaluateXPath(
`self::li[not(${MenuUI.$hasClass('separator')})]${extrcondition}`,
base,
XPathResult.FIRST_ORDERED_NODE_TYPE
).singleNodeValue;
}
$advanceFocus(direction, lastFocused = null) {
lastFocused = lastFocused || this.$lastHoverItem || this.$lastFocusedItem;
if (!lastFocused) {
if (direction < 0)
this.$lastFocusedItem = lastFocused = this.root.firstChild;
else
this.$lastFocusedItem = lastFocused = this.root.lastChild;
}
this.focusTo(direction < 0 ? this.$getPreviousItem(lastFocused) : this.$getNextItem(lastFocused));
this.$setHover(null);
}
$digIn() {
if (!this.$lastFocusedItem) {
this.$advanceFocus(1, this.root.lastChild);
return;
}
const submenu = this.$lastFocusedItem.querySelector('ul');
if (!submenu || this.$lastFocusedItem.classList.contains('disabled'))
return;
this.$closeOtherSubmenus(this.$lastFocusedItem);
this.$openSubmenuFor(this.$lastFocusedItem);
this.$advanceFocus(1, submenu.lastChild);
}
$digOut() {
const targetItem = this.$lastHoverItem || this.$lastFocusedItem;
if (!targetItem ||
targetItem.parentNode == this.root)
return;
this.$closeOtherSubmenus(targetItem);
this.$lastFocusedItem = targetItem.parentNode.parentNode;
this.$closeOtherSubmenus(this.$lastFocusedItem);
this.$lastFocusedItem.classList.remove('open');
this.focusTo(targetItem.parentNode.parentNode);
this.$setHover(null);
}
$onTransitionEnd(event) {
const hoverItems = this.root.querySelectorAll('li:hover');
if (hoverItems.length == 0)
return;
const $lastHoverItem = hoverItems[hoverItems.length - 1];
const item = this.$getEffectiveItem($lastHoverItem);
if (!item)
return;
if (item.parentNode != event.target)
return;
this.$setHover(item);
this.focusTo(item);
}
$onContextMenu(event) {
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
}
static $installStyles() {
this.style = document.createElement('style');
this.style.setAttribute('type', 'text/css');
const common = `.${this.$commonClass}`;
this.style.textContent = `
${common}.menu-ui,
${common}.menu-ui ul {
background-color: var(--menu-ui-background-color);
color: var(--menu-ui-text-color);
cursor: default;
margin: 0;
max-height: calc(100% - 6px);
max-width: calc(100% - 6px);
opacity: 0;
overflow: hidden; /* because scrollbars always trap mouse events even if it is invisible. See also: https://github.com/piroor/treestyletab/issues/2386 */
padding: 0;
pointer-events: none;
position: fixed;
z-index: 999999;
}
${common}.menu-ui.rtl {
direction: rtl;
}
${common}.menu-ui.open,
${common}.menu-ui.open li.open > ul {
opacity: 1;
overflow: auto;
pointer-events: auto;
}
${common}.menu-ui li {
list-style: none;
margin: 0;
padding: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
${common}.menu-ui li:not(.separator):focus,
${common}.menu-ui li:not(.separator).open {
background-color: var(--menu-ui-background-color-active);
color: var(--menu-ui-text-color-active);
}
${common}.menu-ui li.radio.checked::before,
${common}.menu-ui li.checkbox.checked::before {
content: "✔";
position: absolute;
inset-inline-start: 0.5em;
}
${common}.menu-ui li.separator {
height: 0.5em;
visibility: hidden;
margin: 0;
padding: 0;
}
${common}.menu-ui li.has-submenu,
${common}.menu-ui.menu li.has-submenu {
padding-inline-end: 1em;
}
${common}.menu-ui li.has-submenu::after {
content: "";
inset-inline-end: 0.25em;
position: absolute;
transform: scale(0.75);
}
${common}.menu-ui .accesskey {
text-decoration: underline;
}
${common}.menu-ui-blocking-screen {
display: none;
}
${common}.menu-ui-blocking-screen.open {
bottom: 0;
display: block;
left: 0;
position: fixed;
right: 0;
top: 0;
z-index: 899999;
}
${common}.menu-ui.menu li:not(.separator):focus,
${common}.menu-ui.menu li:not(.separator).open {
outline: none;
}
${common}.menu-ui.panel li:not(.separator):focus ul li:not(:focus):not(.open),
${common}.menu-ui.panel li:not(.separator).open ul li:not(:focus):not(.open) {
background-color: transparent;
color: var(--menu-ui-text-color);
}
${common}.menu-ui-marker {
opacity: 0;
pointer-events: none;
position: fixed;
}
${common}.menu-ui-marker.open {
border: 0.5em solid transparent;
content: "";
height: 0;
left: 0;
opacity: 1;
top: 0;
width: 0;
z-index: 999999;
}
${common}.menu-ui-marker.top {
border-bottom: 0.5em solid var(--menu-ui-background-color);
}
${common}.menu-ui-marker.bottom {
border-top: 0.5em solid var(--menu-ui-background-color);
}
${common}.menu-ui li.disabled {
opacity: 0.5;
}
${common}.menu-ui li[data-icon],
${common}.menu-ui.menu li[data-icon],
${common}.menu-ui.panel li[data-icon] {
--icon-size: 16px;
background-repeat: no-repeat;
background-size: var(--icon-size);
padding-inline-start: calc(var(--icon-size) + 0.7em);
}
${common}.menu-ui:not(.rtl) li[data-icon],
${common}.menu-ui:not(.rtl).menu li[data-icon],
${common}.menu-ui:not(.rtl).panel li[data-icon] {
background-position: left center;
}
${common}.menu-ui.rtl li[data-icon],
${common}.menu-ui.rtl.menu li[data-icon],
${common}.menu-ui.rtl.panel li[data-icon] {
background-position: right center;
}
${common}.menu-ui li.checkbox,
${common}.menu-ui li.radio,
${common}.menu-ui.menu li.checkbox,
${common}.menu-ui.panel li.checkbox,
${common}.menu-ui.menu li.radio,
${common}.menu-ui.panel li.radio {
padding-inline-start: 1.7em;
}
/* panel-like appearance */
${common}.panel {
--menu-ui-background-color: -moz-dialog;
--menu-ui-text-color: -moz-dialogtext;
--menu-ui-background-color-active: Highlight;
--menu-ui-text-color-active: HighlightText;
}
${common}.panel li.disabled {
--menu-ui-background-color-active: InactiveCaptionText;
--menu-ui-text-color-active: InactiveCaption;
}
${common}.menu-ui.panel,
${common}.menu-ui.panel ul {
box-shadow: 0.1em 0.1em 0.8em rgba(0, 0, 0, 0.65);
padding: 0.25em 0;
}
${common}.menu-ui.panel li {
padding: 0.15em 1em 0.15em 0.7em;
}
/* Menu-like appearance */
${common}.menu {
--menu-ui-background-color: Menu;
--menu-ui-text-color: MenuText;
--menu-ui-background-color-active: Highlight;
--menu-ui-text-color-active: HighlightText;
}
${common}.menu li.disabled {
--menu-ui-background-color-active: InactiveCaptionText;
--menu-ui-text-color-active: InactiveCaption;
}
${common}.menu-ui.menu,
${common}.menu-ui.menu ul {
border: 1px outset Menu;
box-shadow: 0.1em 0.1em 0.5em rgba(0, 0, 0, 0.65);
font: -moz-pull-down-menu;
}
${common}.menu-ui.menu li {
padding: 0.15em 0.5em 0.15em 0.7em;
}
${common}.menu-ui.menu li.separator {
border: 1px inset Menu;
height: 0;
margin: 0 0.5em;
max-height: 0;
opacity: 0.5;
padding: 0;
visibility: visible;
}
${common}.menu-ui.menu:not(.rtl) li[data-icon]:not([data-icon-color]),
${common}.menu-ui.panel:not(.rtl) li[data-icon]:not([data-icon-color]) {
background-position: 0.5em center;
}
${common}.menu-ui.menu.rtl li[data-icon]:not([data-icon-color]),
${common}.menu-ui.panel.rtl li[data-icon]:not([data-icon-color]) {
background-position: calc(100% - 0.5em) center;
}
${common}.menu-ui li:not([data-icon]) .icon,
${common}.menu-ui li[data-icon]:not([data-icon-color]) .icon {
display: none;
}
${common}.menu-ui li[data-icon][data-icon-color] .icon {
display: inline-block;
height: var(--icon-size);
inset-inline-start: 0.5em;
max-height: var(--icon-size);
max-width: var(--icon-size);
position: absolute;
width: var(--icon-size);
}
`;
document.head.appendChild(this.style);
}
static init() {
MenuUI.$uniqueKey = parseInt(Math.random() * Math.pow(2, 16));
MenuUI.$commonClass = `menu-ui-${MenuUI.$uniqueKey}`;
MenuUI.prototype.$uniqueKey = MenuUI.$uniqueKey;
MenuUI.prototype.$commonClass = MenuUI.$commonClass;
MenuUI.$installStyles();
window.MenuUI = MenuUI;
}
};
MenuUI.init();
}
export default MenuUI;