From f53228facd3a2404bad4d27ba3a6d68d836c9f00 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Tue, 24 Dec 2024 13:18:42 -0500 Subject: [PATCH] Add Tauri polyfill for web version --- src/index.html | 8 +- src/jsMenus.css | 221 +++++++++++ src/jsMenus.js | 893 ++++++++++++++++++++++++++++++++++++++++++ src/main.js | 19 +- src/tauri_polyfill.js | 76 ++++ 5 files changed, 1207 insertions(+), 10 deletions(-) create mode 100644 src/jsMenus.css create mode 100644 src/jsMenus.js create mode 100644 src/tauri_polyfill.js diff --git a/src/index.html b/src/index.html index aa67aa8..174c5a7 100644 --- a/src/index.html +++ b/src/index.html @@ -4,8 +4,14 @@ - Tauri App + Lightningbeam + + + + + + diff --git a/src/jsMenus.css b/src/jsMenus.css new file mode 100644 index 0000000..db94868 --- /dev/null +++ b/src/jsMenus.css @@ -0,0 +1,221 @@ +html, body { + margin: 0; + height: 100%; +} +body { display: flex; flex-direction: column } +.menubar { flex: 0 0 22px } +div.below-menubar { flex: 1 1 0; min-height: 0;} + +.menu-item > [role=button] { + display: flex; + width: 100%; + border: none; + padding: 0px; + color: inherit; + background-color: inherit; + appearance: none; + outline: none; +} + +.nwjs-menu { + font-family: 'Helvetica Neue', HelveticaNeue, 'TeX Gyre Heros', TeXGyreHeros, FreeSans, 'Nimbus Sans L', 'Liberation Sans', Arimo, Helvetica, Arial, sans-serif; + font-size: 14px; + color: #2c2c2c; + -webkit-user-select: none; + user-select: none; + -webkit-font-smoothing: subpixel-antialiased; + font-weight: 400; + white-space: pre; +} + +.contextmenu { + min-width: 100px; + background-color: #fafafa; + position: fixed; + opacity: 0; + transition: opacity 250ms; + margin: 0; + padding: 0 0; + list-style: none; + pointer-events: none; + border: 1px rgba(191, 191, 191, 0.8) solid; + border-radius: 4px; + box-shadow: rgba(43, 43, 43, 0.34) 1px 1px 11px 0px; + z-index: 2147483647; +} + +.contextmenu { + opacity: 1; + transition: opacity 30ms; + pointer-events: all; +} + +.contextmenu.submenu { + transition: opacity 250ms; +} + +.contextmenu.submenu { + transition: opacity 150ms; + transition-timing-function: step-end; +} + +.menu-item.normal, +.menu-item.checkbox, +.menu-item.radio { + cursor: default; + margin: 2px 0; + padding: 0 0; + box-sizing: border-box; + position: relative; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + align-content: stretch; + align-items: flex-start; + width: 100%; +} + +.contextmenu .menu-item.active, +.menu-item.normal.submenu-active, .menu-item.normal.submenu-active:hover { + background-color: #499BFE; + color: #fff; +} +.menu-item:hover { background-color: inherit; } + +.menu-item.normal > div, +.menu-item.checkbox > div, +.menu-item.radio > div { + align-self: center; + vertical-align: middle; + display: inline-flex; + justify-content: flex-start; + flex-shrink: 0; +} + +.menu-item.normal .icon { + display: inline-flex; + vertical-align: middle; + max-width: 16px; + max-height: 16px; + align-self: center; +} + +li.menu-item.separator { + height: 2px; + background-color: rgba(128, 128, 128, 0.2); + margin: 5px 0; +} + +.menu-item .modifiers, +.menu-item .keys, +.menu-item .icon-wrap, +.menu-item .checkmark { + display: inline-flex; + align-items: center; + vertical-align: middle; +} + +.menu-item .keys { + opacity: 0.4; +} + +.menu-item .checkmark { + width: 22px; +} +.menu-item > [role=button] > .modifiers, +.menu-item > [role=button] > .keys { + box-sizing: border-box; + padding: 0 6px; + text-align: right; + order: 0; + flex: 0 0 auto; + align-self: center; +} + +.menu-item > [role=button] > .label { + padding: 0 22px 0 0; + order: 0; + flex: 1 0 auto; + align-self: center; + text-align: left; +} + +.menu-item.disabled, +.menu-item.disabled:hover, +.contextmenu .menu-item.disabled:hover { + color: #ababab; +} + +.menu-item.disabled:hover, +.contextmenu .menu-item.disabled:hover { + background-color: transparent; +} + +.menu-item .icon-wrap { + padding: 0 6px 0 0; + display: inline-flex; + align-self: center; +} + +.menu-item .label-text { + align-items: center; + vertical-align: middle; +} + +.menu-item.checkbox.checked .checkmark::before { + content: '✔'; + text-align: center; + width: 100%; +} + +.menu-item.radio.checked .checkmark::before { + content: '⊚'; + text-align: center; + width: 100%; +} + +.menubar { + height: 22px; + margin: 0; + padding: 0; + top: 0; + left: 0; + right: 0; + background-color: #eee; + z-index: 2147483647; +} + +.menubar > .menu-item.normal { + display: inline-block; + width: auto; + height: 100%; +} + +.menubar .menu-item.normal > div { + vertical-align: top; +} +.menubar > .menu-item { + margin: 0 4px 0 0; +} +.menubar > .menu-item.normal .checkmark, +.menubar > .menu-item.normal .modifiers { + display: none; +} + +.menubar .menu-item.normal .label { + padding: 0 3px; +} + +.contextmenu.menubar-submenu { + transition: opacity 0ms; +} + +/* Mac only? +.contextmenu { + border-radius: 7px; +} +.contextmenu.menubar-submenu { + border-radius: 0 0 7px 7px; +} +*/ diff --git a/src/jsMenus.js b/src/jsMenus.js new file mode 100644 index 0000000..b67ae84 --- /dev/null +++ b/src/jsMenus.js @@ -0,0 +1,893 @@ +class Menu { + constructor(settings = {}, itemArgs = []) { + const typeEnum = ['contextmenu', 'menubar']; + let items = []; + let type = isValidType(settings.type) ? settings.type : 'contextmenu'; + let beforeShow = settings.beforeShow; + Object.defineProperty(this, 'items', { + get: () => { + return items; + } + }); + + Object.defineProperty(this, 'beforeShow', { + get: () => { + return beforeShow; + } + }); + + Object.defineProperty(this, 'type', { + get: () => { + return type; + }, + set: (typeIn) => { + type = isValidType(typeIn) ? typeIn : type; + } + }); + + this.append = item => { + if(!(item instanceof MenuItem)) { + console.error('appended item must be an instance of MenuItem'); + return false; + } + let index = items.push(item); + return index; + }; + + this.insert = (item, index) => { + if(!(item instanceof MenuItem)) { + console.error('inserted item must be an instance of MenuItem'); + return false; + } + + items.splice(index, 0, item); + return true; + }; + + this.remove = item => { + if(!(item instanceof MenuItem)) { + console.error('item to be removed is not an instance of MenuItem'); + return false; + } + + let index = items.indexOf(item); + if(index < 0) { + console.error('item to be removed was not found in this.items'); + return false; + } else { + items.splice(index, 0); + return true; + } + }; + + this.removeAt = index => { + items.splice(index, 0); + return true; + }; + + this.node = null; + let nitems = itemArgs.length; + for(let i = 0; i < nitems; i++) { + let item = itemArgs[i]; + if (item instanceof MenuItem) + items.push(item); + else + items.push(new MenuItem(item)); + } + + function isValidType(typeIn = '', debug = false) { + if(typeEnum.indexOf(typeIn) < 0) { + if(debug) console.error(`${typeIn} is not a valid type`); + return false; + } + return true; + } + + } + + createMacBuiltin() { + console.error('This method is not available in browser :('); + return false; + } + + popup(x, y, itemNode = null, menubarSubmenu = false) { + Menu._keydownListen(true); + + let setRight = false; + + let submenu = itemNode != null || this.submenu; + this.submenu = menubarSubmenu; + + menubarSubmenu = menubarSubmenu || this.menubarSubmenu; + this.menubarSubmenu = menubarSubmenu; + let top = Menu.contextMenuParent || document.body; + if (! Menu._topSheet && Menu.topsheetZindex > 0) { + let topSheet = document.createElement("div"); + topSheet.setAttribute("style", + "position: fixed; top: 0px; bottom: 0px; left: 0px; right: 0px; z-index: "+Menu.topsheetZindex); + top.appendChild(topSheet); + Menu._topSheet = topSheet; + top = topSheet; + } + if (! Menu._topmostMenu) { + Menu._topmostMenu = this; + Menu._listenerElement = top; + top.addEventListener('mouseup', Menu._mouseHandler, false); + top.addEventListener('mousedown', Menu._mouseHandler, false); + } + + let menuNode = this.buildMenu(submenu, menubarSubmenu); + menuNode.jsMenu = this; + this.node = menuNode; + Menu._currentMenuNode = menuNode; + + if(this.node.parentNode) { + if(menuNode === this.node) return; + this.node.parentNode.replaceChild(menuNode, this.node); + } else { + ((! (menubarSubmenu && Menu._topSheet) && itemNode) + || top).appendChild(this.node); + } + + let width = menuNode.clientWidth; + let height = menuNode.clientHeight; + let wwidth = top.offsetWidth; + const base_x = x, base_y = y; + if ((x + width) > wwidth) { + setRight = true; + if(submenu && ! menubarSubmenu) { + x = wwidth - itemNode.parentNode.offsetLeft + 2; + if (width + x > wwidth) { + x = 0; + setRight = false; + } + } else { + x = 0; + } + } + + let wheight = top.offsetHeight; + if((y + height) > wheight) { + y = wheight - height; + if (y < -0.5) + y = wheight - height; + } + + if(!setRight) { + menuNode.style.left = x + 'px'; + menuNode.style.right = 'auto'; + } else { + menuNode.style.right = x + 'px'; + menuNode.style.left = 'auto'; + } + // Don't have topSheet cover menubar, so we can catch mouseenter + if (Menu._menubarNode && Menu._topSheet) + Menu._topSheet.style.top = `${Menu._menubarNode.offsetHeight}px`; + + menuNode.style.top = y + 'px'; + if (! Menu.showMenuNode + || ! Menu.showMenuNode(this, menuNode, width, height, base_x, base_y, x, y)) { + menuNode.classList.add('show'); + } + } + + popdown() { + this.items.forEach(item => { + if(item.submenu) { + item.submenu.popdown(); + } else { + item.node = null; + } + }); + if(this.node && this.type !== 'menubar') { + Menu._currentMenuNode = this.node.parentMenuNode; + if (this.menubarSubmenu) + Menu.showSubmenuActive(this.node.menuItem, false); + if (Menu.hideMenuNode) + Menu.hideMenuNode(this, this.node); + this.node.parentNode.removeChild(this.node); + if (Menu._topSheet && Menu._topSheet.firstChild == null) { + Menu._topSheet.parentNode.removeChild(Menu._topSheet); + Menu._topSheet = undefined; + } + this.node = null; + } + if (this == Menu._topmostMenu) { + Menu._topmostMenu = null; + let el = Menu._listenerElement; + if (el) { + el.removeEventListener('mouseup', Menu._mouseHandler, false); + el.removeEventListener('mousedown', Menu._mouseHandler, false); + Menu._listenerElement = null; + } + } + + if(this.type === 'menubar') { + this.clearActiveSubmenuStyling(); + } + } + + static showSubmenuActive(node, active) { + if (active) + node.classList.add('submenu-active'); + else + node.classList.remove('submenu-active'); + if (node.firstChild instanceof Element) + node.firstChild.setAttribute('aria-expanded', + active?'true':'false'); + } + + static popdownAll() { + Menu._topmostMenu.popdown(); + return; + } + + buildMenu(submenu = false, menubarSubmenu = false) { + if (this.beforeShow) + (this.beforeShow)(this); + let menuNode = document.createElement('ul'); + menuNode.classList.add('nwjs-menu', this.type); + menuNode.spellcheck = false; + // make focusable + menuNode.setAttribute('contenteditable', 'true'); + menuNode.setAttribute('role', + this.type === 'menubar' ? 'menubar' : 'menu'); // ARIA recommended + if(submenu) menuNode.classList.add('submenu'); + if(menubarSubmenu) menuNode.classList.add('menubar-submenu'); + + menuNode.jsMenu = this; + menuNode.parentMenuNode = Menu._currentMenuNode; + this.items.forEach(item => { + if (item.beforeShow) + (item.beforeShow)(item); + if (item.visible) { + item.buildItem(menuNode, + this.type === 'menubar'); + } + }); + return menuNode; + } + + static isDescendant(parent, child) { + let node = child.parentNode; + while(node !== null) { + if(node === parent) { + return true; + } + node = node.parentNode; + } + return false; + } + + static _inMenubar(node) { + if (Menu._menubarNode === null) + return false; + while(node instanceof Element + && ! node.classList.contains('submenu')) { + if(node === Menu._menubarNode) + return true; + node = node.parentNode; + } + return false; + } + + static _activateSubmenu(miNode) { + let item = miNode.jsMenuItem; + let wasActive = item.node.classList.contains('submenu-active'); + Menu.showSubmenuActive(item.node, !wasActive); + // FIXME use select method + if(item.submenu) { + if(! wasActive) { + miNode.jsMenu.node.activeItemNode = item.node; + let rect = item.node.getBoundingClientRect(); + item.popupSubmenu(rect.left, rect.bottom, true); + } else { + item.submenu.popdown(); + miNode.jsMenu.node.currentSubmenu = null; + miNode.jsMenu.node.activeItemNode = null; + } + } + } + + static _mouseHandler(e) { + e.preventDefault(); // prevent focus change on mousedown + let inMenubar = Menu._inMenubar(e.target); + let menubarHandler = e.currentTarget == Menu._menubarNode; + let miNode = e.target; + while (miNode && ! miNode.jsMenuItem) + miNode = miNode.parentNode; + /* mouseenter: + if selected sibling: unhighlight (and popdown if submenu) + select item and if submenu popup + mouseout (or mouseleave): + if (! submenu) unhighlight + mousedown: + if (miNode) select + else popdownAll + */ + if (e.type=="mousedown" && inMenubar == menubarHandler + && (!miNode || miNode.jsMenuItem.menuBarTopLevel)) { + if (Menu._topmostMenu) { + Menu.popdownAll(); + if (Menu.menuDone) + Menu.menuDone(null); + } + } + if ((inMenubar == menubarHandler) && miNode) { + if (e.type=="mousedown") { + Menu._activateSubmenu(miNode); + } + if (e.type=="mouseup") { + miNode.jsMenuItem.doit(miNode); + } + } + } + + static focusMenubar() { + const items = Menu._menubar?.items; + if (items && items.length > 0 && items[0].node + && Menu._menubarNode) { + Menu._activateSubmenu(items[0].node); + return true; + } + return false; + } + + static setApplicationMenu(menubar, parent=document.body, before=undefined) { + let oldNode = Menu._menubarNode; + if (oldNode) { + let oldParent = oldNode.parentNode; + if (oldParent != null) + oldParent.removeChild(oldNode); + oldNode.removeEventListener('mousedown', Menu._mouseHandler, false); + Menu._menubarNode = null; + } + if (before==undefined) { + before = parent.firstChild + } + if (menubar != null) { + let newNode = menubar.buildMenu(); + newNode.jsMenuItem = null; + parent.insertBefore(newNode, before); + newNode.addEventListener('mousedown', Menu._mouseHandler, false); + Menu._menubarNode = newNode; + menubar.node = newNode; + } + Menu._menubar = menubar; + } + + clearActiveSubmenuStyling(notThisNode) { + if (! this.node) + return; + let submenuActive = this.node.querySelectorAll('.submenu-active'); + for(let node of submenuActive) { + if(node === notThisNode) continue; + Menu.showSubmenuActive(node, false); + } + } + + static recursiveNodeFind(menu, node) { + if(menu.node === node) { + return true; + } else if(Menu.isDescendant(menu.node, node)) { + return true; + } else if(menu.items.length > 0) { + for(var i=0; i < menu.items.length; i++) { + let menuItem = menu.items[i]; + if(!menuItem.node) continue; + + if(menuItem.node === node) { + return true; + } else if(Menu.isDescendant(menuItem.node, node)) { + return true; + } else { + if(menuItem.submenu) { + if(recursiveNodeFind(menuItem.submenu, node)) { + return true; + } else { + continue; + } + } + } + } + } else { + return false; + } + return false; + } + + isNodeInChildMenuTree(node = false) { + if(!node) return false; + return recursiveNodeFind(this, node); + } +} + +// Parent node for context menu popup. If null, document.body is the default. +Menu.contextMenuParent = null; + +Menu._currentMenuNode = null; + +Menu._keydownListener = function(e) { + function nextItem(menuNode, curNode, forwards) { + let nullSeen = false; + let next = curNode; + for (;;) { + next = !next ? null + : forwards ? next.nextSibling + : next.previousSibling; + if (! next) { + next = forwards ? menuNode.firstChild + : menuNode.lastChild; + if (nullSeen || !next) + return null; + nullSeen = true; + } + if (next instanceof Element + && next.classList.contains("menu-item") + && next.jsMenuItem.type != 'separator' + && ! (next.classList.contains("disabled"))) + return next; + } + } + function nextMenu(menuNode, forwards) { + let menubarNode = menuNode.menuItem.parentNode; + let next = nextItem(menubarNode, + menubarNode.activeItemNode, + forwards); + if (next) + next.jsMenuItem.select(next, true, true, true); + return next; + + } + function openSubmenu(active) { + active.jsMenuItem.selectSubmenu(active); + menuNode = Menu._currentMenuNode; + let next = nextItem(menuNode, null, true); + if (next) + next.jsMenuItem.select(next, true, false); + } + let menuNode = Menu._currentMenuNode + if (menuNode) { + let active = menuNode.activeItemNode; + switch (e.keyCode) { + case 27: // Escape + case 37: // Left + e.preventDefault(); + e.stopPropagation(); + if (e.keyCode == 37 + && menuNode.jsMenu.menubarSubmenu + && nextMenu(menuNode, false)) + return; + menuNode.jsMenu.popdown(); + if (! Menu._topmostMenu && Menu.menuDone) + Menu.menuDone(null); + break; + case 32: // Space + case 13: // Enter + e.preventDefault(); + e.stopPropagation(); + if (active) { + if (active.jsMenuItem.submenu) + openSubmenu(active); + else + active.jsMenuItem.doit(active); + } + break; + case 39: // Right + e.preventDefault(); + e.stopPropagation(); + if (active && active.jsMenuItem.submenu) + openSubmenu(active); + else if (Menu._topmostMenu.menubarSubmenu) + nextMenu(menuNode, true); + break; + case 38: // Up + case 40: // Down + e.preventDefault(); + e.stopPropagation(); + let next = nextItem(menuNode, + menuNode.activeItemNode, + e.keyCode == 40); + if (next) + next.jsMenuItem.select(next, true, false); + break; + } + } +} +Menu._keydownListening = false; +Menu._keydownListen = function(value) { + if (value != Menu._keydownListening) { + if (value) + document.addEventListener('keydown', Menu._keydownListener, true); + else + document.removeEventListener('keydown', Menu._keydownListener, true); + } + Menu._keydownListening = value; +} + +Menu._isMac = typeof navigator != "undefined" ? /Mac/.test(navigator.platform) + : typeof os != "undefined" ? os.platform() == "darwin" : false + +// If positive, create a "sheet" above the Menu.contextMenuParent +// at the given z-index. +// Used to capture mouse-clicks - needed if there are iframes involved. +Menu.topsheetZindex = 5; + +class MenuItem { + constructor(settings = {}) { + + const typeEnum = ['separator', 'checkbox', 'radio', 'normal']; + let type = isValidType(settings.type) ? settings.type : 'normal'; + let submenu = settings.submenu || null; + if (submenu && ! (submenu instanceof Menu)) + submenu = new Menu({}, submenu); + let click = settings.click || null; + this.modifiers = settings.modifiers; + let label = settings.label || ''; + let enabled = settings.enabled; + if(typeof settings.enabled === 'undefined') enabled = true; + let visible = settings.visible; + if(typeof settings.visible === 'undefined') visible = true; + let beforeShow = settings.beforeShow; + + Object.defineProperty(this, 'type', { + get: () => { + return type; + } + }); + + Object.defineProperty(this, 'beforeShow', { + get: () => { + return beforeShow; + } + }); + + Object.defineProperty(this, 'submenu', { + get: () => { + return submenu; + }, + set: (inputMenu) => { + console.warn('submenu should be set on initialisation, changing this at runtime could be slow on some platforms.'); + if(!(inputMenu instanceof Menu)) { + console.error('submenu must be an instance of Menu'); + return; + } else { + submenu = inputMenu; + } + } + }); + + Object.defineProperty(this, 'click', { + get: () => { + return click; + }, + set: (inputCallback) => { + if(typeof inputCallback !== 'function') { + console.error('click must be a function'); + return; + } else { + click = inputCallback; + } + } + }); + + Object.defineProperty(this, 'enabled', { + get: () => { + return enabled; + }, + set: (inputEnabled) => { + enabled = inputEnabled; + } + }); + + Object.defineProperty(this, 'visible', { + get: () => { + return visible; + }, + set: (inputVisible) => { + visible = inputVisible; + } + }); + + Object.defineProperty(this, 'label', { + get: () => { + return label; + }, + set: (inputLabel) => { + label = inputLabel; + } + }); + + this.icon = settings.icon || null; + this.iconIsTemplate = settings.iconIsTemplate || false; + this.tooltip = settings.tooltip || ''; + this.checked = settings.checked || false; + + this.key = settings.key || null; + let accelerator = settings.accelerator; + if (! accelerator && settings.key) { + accelerator = (settings.modifiers ? (settings.modifiers + "+") : "") + settings.key; + } + if (accelerator instanceof Array) + accelerator = accelerator.join(" "); + if (accelerator) { + accelerator = accelerator + .replace(/Command[+]/i, "Cmd+") + .replace(/Control[+]/i, "Ctrl+") + .replace(/(Mod|((Command|Cmd)OrCtrl))[+]/i, + Menu._isMac ? "Cmd+" : "Ctrl+"); + } + this.accelerator = accelerator; + if (accelerator && ! settings.key) { + const plus = + accelerator.lastIndexOf("+", accelerator.length-2); + if (plus > 0) { + this.modifiers = accelerator.substring(0, plus); + this.key = accelerator.substring(plus+1); + } else { + settings.key = accelerator; + } + } + this.node = null; + + if(this.key) { + this.key = this.key.toUpperCase(); + } + + function isValidType(typeIn = '', debug = false) { + if(typeEnum.indexOf(typeIn) < 0) { + if(debug) console.error(`${typeIn} is not a valid type`); + return false; + } + return true; + } + } + + toString() { + return this.type+"["+this.label+"]"; + } + + _mouseoverHandle_menubarTop() { + let pmenu = this.node.jsMenuNode; + if (pmenu.activeItemNode) { + pmenu.activeItemNode.classList.remove('active'); + pmenu.activeItemNode = null; + } + if (pmenu && pmenu.querySelector('.submenu-active')) { + if(this.node.classList.contains('submenu-active')) return; + Menu.showSubmenuActive(this.node, true); + this.select(this.node, true, true, true); + } + } + + doit(node) { + if (! this.submenu) { + Menu.popdownAll(); + if(this.type === 'checkbox') + this.checked = !this.checked; + else if (this.type === 'radio') { + this.checked = true; + for (let dir = 0; dir <= 1; dir++) { + for (let n = node; ; ) { + n = dir ? n.nextSibling + : n.previousSibling; + if (! (n instanceof Element + && n.classList.contains("radio"))) + break; + n.jsMenuItem.checked = false; + } + } + } + if(this.click) this.click(this); + if (Menu.menuDone) + Menu.menuDone(this); + } + } + + select(node, turnOn, popupSubmenu, menubarSubmenu = false) { + let pmenu = node.jsMenuNode; + if (pmenu.activeItemNode) { + pmenu.activeItemNode.classList.remove('active'); + Menu.showSubmenuActive(pmenu.activeItemNode, false); + pmenu.activeItemNode = null; + } + if(pmenu.currentSubmenu) { + pmenu.currentSubmenu.popdown(); + pmenu.currentSubmenu = null; + } + if(this.submenu && popupSubmenu) + this.selectSubmenu(node, menubarSubmenu); + else + node.classList.add('active'); + node.jsMenuNode.activeItemNode = node; + } + + selectSubmenu(node, menubarSubmenu) { + node.jsMenuNode.currentSubmenu = this.submenu; + if(this.submenu.node) + return; + + let parentNode = node.parentNode; + let x, y; + if (menubarSubmenu) { + let rect = node.getBoundingClientRect(); + x = rect.left; + y = rect.bottom; + } else { + x = parentNode.offsetWidth + parentNode.offsetLeft - 2; + y = parentNode.offsetTop + node.offsetTop - 4; + } + this.popupSubmenu(x, y, menubarSubmenu); + Menu.showSubmenuActive(node, true); + } + + buildItem(menuNode, menuBarTopLevel = false) { + let node = document.createElement('li'); + node.setAttribute('role', this.type === 'separator' ? 'separator' : 'menuitem'); + node.jsMenuNode = menuNode; + node.jsMenu = menuNode.jsMenu; + node.jsMenuItem = this; + node.classList.add('menu-item', this.type); + + menuBarTopLevel = menuBarTopLevel || this.menuBarTopLevel || false; + this.menuBarTopLevel = menuBarTopLevel; + + if(menuBarTopLevel) { + node.addEventListener('mouseenter', this._mouseoverHandle_menubarTop.bind(this)); + } + + let iconWrapNode = document.createElement('div'); + iconWrapNode.classList.add('icon-wrap'); + + if(this.icon) { + let iconNode = new Image(); + iconNode.src = this.icon; + iconNode.classList.add('icon'); + iconWrapNode.appendChild(iconNode); + } + + let labelNode = document.createElement('span'); + labelNode.classList.add('label'); + + let checkmarkNode = document.createElement('span'); + checkmarkNode.classList.add('checkmark'); + + if(this.checked && !menuBarTopLevel) + node.classList.add('checked'); + + if(this.submenu) + node.setAttribute('aria-haspopup', 'true'); + + if(this.submenu && !menuBarTopLevel) { + node.addEventListener('mouseleave', (e) => { + if(node !== e.target) { + if(!Menu.isDescendant(node, e.target)) + this.submenu.popdown(); + } + }); + } + + if(!this.enabled) { + node.classList.add('disabled'); + } + + if(this.icon) labelNode.appendChild(iconWrapNode); + + let buttonNode; + if (this.type !== 'separator') { + buttonNode = document.createElement('span'); + buttonNode.setAttribute('role', 'button'); + node.appendChild(buttonNode); + if (this.submenu) + buttonNode.setAttribute('aria-expanded', + 'false'); + if(!menuBarTopLevel) { + buttonNode.addEventListener('mouseenter', () => { + this.select(node, true, true); + }); + } + } else + buttonNode = node; + + let textLabelNode = document.createElement('span'); + textLabelNode.textContent = this.label; + textLabelNode.classList.add('label-text'); + + buttonNode.appendChild(checkmarkNode); + + labelNode.appendChild(textLabelNode); + buttonNode.appendChild(labelNode); + + if(this.submenu && !menuBarTopLevel) { + const n = document.createElement('span'); + n.classList.add('modifiers'); + n.append(MenuItem.submenuSymbol); + buttonNode.appendChild(n); + } + let accelerator = this.accelerator; + if (accelerator) { + let keyNode = document.createElement('span'); + keyNode.classList.add('keys'); + let i = 0; + const len = accelerator.length; + for (;;) { + if (i > 0) { + keyNode.append(" "); + } + let sp = accelerator.indexOf(' ', i); + let key = accelerator.substring(i, sp < 0 ? len : sp); + let pl = key.lastIndexOf('+', key.length-2); + if (pl > 0) { + let mod = key.substring(0, pl); + let modNode = document.createElement('span'); + modNode.classList.add('modifiers'); + if (MenuItem.useModifierSymbols) { + let mods = mod.toLowerCase().split('+'); + mod = ""; + // Looping this way to keep order of symbols - required by macOS + for(let symbol in MenuItem.modifierSymbols) { + if(mods.indexOf(symbol) >= 0) { + mod += MenuItem.modifierSymbols[symbol]; + } + } + } else + mod += "+"; + modNode.append(mod); + keyNode.append(modNode); + key = key.substring(pl+1); + } + keyNode.append(key); + if (sp < 0) + break; + i = sp + 1; + } + keyNode.normalize(); + buttonNode.appendChild(keyNode); + } + + node.title = this.tooltip; + this.node = node; + menuNode.appendChild(node); + } + + popupSubmenu(x, y, menubarSubmenu = false) { + this.submenu.popup(x, y, this.node, menubarSubmenu); + this.submenu.node.menuItem = this.node; + this.node.jsMenuNode.currentSubmenu = this.submenu; + } +} + +MenuItem.submenuSymbol = '\u27a7'; // '➧' Squat Black Rightwards Arrow[ + +MenuItem.modifierSymbols = { + shift: '⇧', + ctrl: '⌃', + alt: '⌥', + cmd: '⌘', + super: '⌘', + command: '⌘' +}; + +MenuItem.keySymbols = { + up: '↑', + esc: '⎋', + tab: '⇥', + left: '←', + down: '↓', + right: '→', + pageUp: '⇞', + escape: '⎋', + pageDown: '⇟', + backspace: '⌫', + space: 'Space' +}; +MenuItem.useModifierSymbols = Menu._isMac; + +if (typeof module !== "undefined" && module.exports) { + module.exports = { Menu: Menu, MenuItem: MenuItem }; +} + +// Local Variables: +// js-indent-level: 8 +// indent-tabs-mode: t +// End: diff --git a/src/main.js b/src/main.js index 0d75ebd..bcd0700 100644 --- a/src/main.js +++ b/src/main.js @@ -28,10 +28,10 @@ function forwardConsole(fnName, logger) { } // forwardConsole('log', trace); -forwardConsole('debug', debug); -forwardConsole('info', info); -forwardConsole('warn', warn); -forwardConsole('error', error); +// forwardConsole('debug', debug); +// forwardConsole('info', info); +// forwardConsole('warn', warn); +// forwardConsole('error', error); // Debug flags const debugQuadtree = false @@ -281,8 +281,9 @@ function getShortcut(shortcut) { // Load the configuration from the file system async function loadConfig() { try { - const configPath = await join(await appLocalDataDir(), CONFIG_FILE_PATH); - const configData = await readTextFile(configPath); + // const configPath = await join(await appLocalDataDir(), CONFIG_FILE_PATH); + // const configData = await readTextFile(configPath); + const configData = localStorage.getItem("lightningbeamConfig") || "{}" config = deepMerge({...config}, JSON.parse(configData)); updateUI() } catch (error) { @@ -293,8 +294,9 @@ async function loadConfig() { // Save the configuration to a file async function saveConfig() { try { - const configPath = await join(await appLocalDataDir(), CONFIG_FILE_PATH); - await writeTextFile(configPath, JSON.stringify(config, null, 2)); + // const configPath = await join(await appLocalDataDir(), CONFIG_FILE_PATH); + // await writeTextFile(configPath, JSON.stringify(config, null, 2)); + localStorage.setItem("lightningbeamConfig", JSON.stringify(config, null, 2)) } catch (error) { console.error('Error saving config:', error); } @@ -476,7 +478,6 @@ let actions = { }); } let img = await loadImage(action.src) - console.log(img.crossOrigin) // img.onload = function() { let ct = { ...context, diff --git a/src/tauri_polyfill.js b/src/tauri_polyfill.js new file mode 100644 index 0000000..d1214ea --- /dev/null +++ b/src/tauri_polyfill.js @@ -0,0 +1,76 @@ +if (!window.__TAURI__) { + // We are in a browser environment + window.__TAURI__ = { + core: { + invoke: () => {} + }, + fs: { + writeFile: () => {}, + readFile: () => {}, + writeTextFile: () => {}, + readTextFile: () => {} + }, + dialog: { + open: () => {}, + save: () => {}, + message: () => {}, + confirm: () => {}, + }, + path: { + documentDir: () => {}, + join: () => {}, + basename: () => {}, + appLocalDataDir: () => {} + }, + menu: { + Menu: { + new: (params) => { + let items = params.items + let menubar = new Menu({type: "menubar"}) + for (let i in items) { + let item = items[i] + menubar.append(new MenuItem({label: item.text, submenu: item})) + } + menubar.setAsWindowMenu = () => { + Menu.setApplicationMenu(menubar) + } + menubar.setAsAppMenu = menubar.setAsWindowMenu + return menubar + } + }, + MenuItem: MenuItem, + PredefinedMenuItem: () => {}, + Submenu: { + new: (params) => { + const items = params.items + menu = new Menu() + for (let i in items) { + let item = items[i] + menuItem = new MenuItem({ + label: item.text, + enabled: item.enabled, + click: item.action, + accelerator: item.accelerator + }) + menu.append(menuItem) + } + menu.text = params.text + return menu + } + } + }, + window: { + getCurrentWindow: () => {} + }, + app: { + getVersion: () => {} + }, + log: { + warn: () => {}, + debug: () => {}, + trace: () => {}, + info: () => {}, + error: () => {}, + } + } +} \ No newline at end of file