diff --git a/ext/js/display/display-audio.js b/ext/js/display/display-audio.js index f1feff5c..bb88c89e 100644 --- a/ext/js/display/display-audio.js +++ b/ext/js/display/display-audio.js @@ -31,6 +31,7 @@ class DisplayAudio { this._cache = new Map(); this._menuContainer = document.querySelector('#popup-menus'); this._entriesToken = {}; + this._openMenus = new Set(); } get autoPlayAudioDelay() { @@ -198,9 +199,12 @@ class DisplayAudio { } _onAudioPlayMenuCloseClick(definitionIndex, expressionIndex, e) { - const {detail: {action, item, menu}} = e; + const {detail: {action, item, menu, shiftKey}} = e; switch (action) { case 'playAudioFromSource': + if (shiftKey) { + e.preventDefault(); + } this._playAudioFromSource(definitionIndex, expressionIndex, item); break; case 'setPrimaryAudio': @@ -306,12 +310,14 @@ class DisplayAudio { for (let i = 0, ii = sources.length; i < ii; ++i) { const source = sources[i]; + let cacheUpdated = false; let infoListPromise; let sourceInfo = sourceMap.get(source); if (typeof sourceInfo === 'undefined') { infoListPromise = this._getExpressionAudioInfoList(source, expression, reading, details); sourceInfo = {infoListPromise, infoList: null}; sourceMap.set(source, sourceInfo); + cacheUpdated = true; } let {infoList} = sourceInfo; @@ -332,14 +338,17 @@ class DisplayAudio { } } - const audio = await this._createAudioFromInfoList(source, infoList, start, end); - if (audio !== null) { return audio; } + const {result, cacheUpdated: cacheUpdated2} = await this._createAudioFromInfoList(source, infoList, start, end); + if (cacheUpdated || cacheUpdated2) { this._updateOpenMenu(); } + if (result !== null) { return result; } } return null; } async _createAudioFromInfoList(source, infoList, start, end) { + let result = null; + let cacheUpdated = false; for (let i = start; i < end; ++i) { const item = infoList[i]; @@ -352,6 +361,8 @@ class DisplayAudio { item.audioPromise = audioPromise; } + cacheUpdated = true; + try { audio = await audioPromise; } catch (e) { @@ -363,11 +374,12 @@ class DisplayAudio { item.audio = audio; } - if (audio === null) { continue; } - - return {audio, source, infoListIndex: i}; + if (audio !== null) { + result = {audio, source, infoListIndex: i}; + break; + } } - return null; + return {result, cacheUpdated}; } async _createAudioFromInfo(info, source) { @@ -471,7 +483,13 @@ class DisplayAudio { const {expression, reading} = expressionReading; const popupMenu = this._createMenu(button, expression, reading); + this._openMenus.add(popupMenu); popupMenu.prepare(); + popupMenu.on('close', this._onPopupMenuClose.bind(this)); + } + + _onPopupMenuClose({menu}) { + this._openMenus.delete(menu); } _sourceIsDownloadable(source) { @@ -533,21 +551,36 @@ class DisplayAudio { } _createMenu(sourceButton, expression, reading) { - // Options - const sources = this._getAudioSources(this._getAudioOptions()); - // Create menu - const {displayGenerator} = this._display; - const menuNode = displayGenerator.instantiateTemplate('audio-button-popup-menu'); - const menuBodyNode = menuNode.querySelector('.popup-menu-body'); + const menuContainerNode = this._display.displayGenerator.instantiateTemplate('audio-button-popup-menu'); + const menuBodyNode = menuContainerNode.querySelector('.popup-menu-body'); + menuContainerNode.dataset.expression = expression; + menuContainerNode.dataset.reading = reading; // Set up items based on options and cache data + this._createMenuItems(menuContainerNode, menuBodyNode, expression, reading); + + // Update primary card audio display + this._updateMenuPrimaryCardAudio(menuBodyNode, expression, reading); + + // Create popup menu + this._menuContainer.appendChild(menuContainerNode); + return new PopupMenu(sourceButton, menuContainerNode); + } + + _createMenuItems(menuContainerNode, menuItemContainer, expression, reading) { + const sources = this._getAudioSources(this._getAudioOptions()); + const {displayGenerator} = this._display; let showIcons = false; + const currentItems = [...menuItemContainer.children]; for (const {source, displayName, isInOptions, downloadable} of sources) { const entries = this._getMenuItemEntries(source, expression, reading); for (let i = 0, ii = entries.length; i < ii; ++i) { const {valid, index, name} = entries[i]; - const node = displayGenerator.instantiateTemplate('audio-button-popup-menu-item'); + let node = this._getOrCreateMenuItem(currentItems, source, index); + if (node === null) { + node = displayGenerator.instantiateTemplate('audio-button-popup-menu-item'); + } const labelNode = node.querySelector('.popup-menu-item-audio-button .popup-menu-item-label'); let label = displayName; @@ -571,17 +604,32 @@ class DisplayAudio { node.dataset.sourceInOptions = `${isInOptions}`; node.dataset.downloadable = `${downloadable}`; - menuBodyNode.appendChild(node); + menuItemContainer.appendChild(node); } } - menuNode.dataset.showIcons = `${showIcons}`; + for (const node of currentItems) { + const {parentNode} = node; + if (parentNode === null) { continue; } + parentNode.removeChild(node); + } + menuContainerNode.dataset.showIcons = `${showIcons}`; + } - // Update primary card audio display - this._updateMenuPrimaryCardAudio(menuBodyNode, expression, reading); + _getOrCreateMenuItem(currentItems, source, index) { + if (index === null) { index = 0; } + index = `${index}`; + for (let i = 0, ii = currentItems.length; i < ii; ++i) { + const node = currentItems[i]; + if (source !== node.dataset.source) { continue; } - // Create popup menu - this._menuContainer.appendChild(menuNode); - return new PopupMenu(sourceButton, menuNode); + let index2 = node.dataset.index; + if (typeof index2 === 'undefined') { index2 = '0'; } + if (index !== index2) { continue; } + + currentItems.splice(i, 1); + return node; + } + return null; } _getMenuItemEntries(source, expression, reading) { @@ -631,4 +679,13 @@ class DisplayAudio { node.dataset.isPrimaryCardAudio = `${isPrimaryCardAudio}`; } } + + _updateOpenMenu() { + for (const menu of this._openMenus) { + const menuContainerNode = menu.containerNode; + const {expression, reading} = menuContainerNode.dataset; + this._createMenuItems(menuContainerNode, menu.bodyNode, expression, reading); + menu.updatePosition(); + } + } } diff --git a/ext/js/dom/popup-menu.js b/ext/js/dom/popup-menu.js index 9ad4e260..af076baa 100644 --- a/ext/js/dom/popup-menu.js +++ b/ext/js/dom/popup-menu.js @@ -24,6 +24,7 @@ class PopupMenu extends EventDispatcher { this._bodyNode = containerNode.querySelector('.popup-menu-body'); this._isClosed = false; this._eventListeners = new EventListenerCollection(); + this._itemEventListeners = new EventListenerCollection(); } get sourceElement() { @@ -47,17 +48,13 @@ class PopupMenu extends EventDispatcher { } prepare() { - const items = this._bodyNode.querySelectorAll('.popup-menu-item'); this._setPosition(); this._containerNode.focus(); this._eventListeners.addEventListener(window, 'resize', this._onWindowResize.bind(this), false); this._eventListeners.addEventListener(this._containerNode, 'click', this._onMenuContainerClick.bind(this), false); - const onMenuItemClick = this._onMenuItemClick.bind(this); - for (const item of items) { - this._eventListeners.addEventListener(item, 'click', onMenuItemClick, false); - } + this.updateMenuItems(); PopupMenu.openMenus.add(this); @@ -69,7 +66,20 @@ class PopupMenu extends EventDispatcher { } close(cancelable=true) { - return this._close(null, 'close', cancelable); + return this._close(null, 'close', cancelable, {}); + } + + updateMenuItems() { + this._itemEventListeners.removeAllEventListeners(); + const items = this._bodyNode.querySelectorAll('.popup-menu-item'); + const onMenuItemClick = this._onMenuItemClick.bind(this); + for (const item of items) { + this._itemEventListeners.addEventListener(item, 'click', onMenuItemClick, false); + } + } + + updatePosition() { + this._setPosition(); } // Private @@ -78,7 +88,7 @@ class PopupMenu extends EventDispatcher { if (e.currentTarget !== e.target) { return; } e.stopPropagation(); e.preventDefault(); - this._close(null, 'outside', true); + this._close(null, 'outside', true, e); } _onMenuItemClick(e) { @@ -86,11 +96,11 @@ class PopupMenu extends EventDispatcher { if (item.disabled) { return; } e.stopPropagation(); e.preventDefault(); - this._close(item, 'item', true); + this._close(item, 'item', true, e); } _onWindowResize() { - this._close(null, 'resize', true); + this._close(null, 'resize', true, {}); } _setPosition() { @@ -172,15 +182,20 @@ class PopupMenu extends EventDispatcher { menu.style.top = `${y}px`; } - _close(item, cause, cancelable) { + _close(item, cause, cancelable, originalEvent) { if (this._isClosed) { return true; } const action = (item !== null ? item.dataset.menuAction : null); + const {altKey=false, ctrlKey=false, metaKey=false, shiftKey=false} = originalEvent; const detail = { menu: this, item, action, - cause + cause, + altKey, + ctrlKey, + metaKey, + shiftKey }; const result = this._sourceElement.dispatchEvent(new CustomEvent('menuClose', {bubbles: false, cancelable, detail})); if (cancelable && !result) { return false; } @@ -189,6 +204,7 @@ class PopupMenu extends EventDispatcher { this._isClosed = true; this._eventListeners.removeAllEventListeners(); + this._itemEventListeners.removeAllEventListeners(); if (this._containerNode.parentNode !== null) { this._containerNode.parentNode.removeChild(this._containerNode); }