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