diff options
author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2021-01-23 21:13:01 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-01-23 21:13:01 -0500 |
commit | ef577b88754523abeab3844115506a0b6e914874 (patch) | |
tree | 78f181897afc89904f2df7c3370030a955219c5b /ext/mixed/js/display-audio.js | |
parent | 9fbdb9757b22c2bb9afe5061137bfe4b3b755e91 (diff) |
Audio button menu (#1302)
* Fix popup menus not stoping events
* Ensure non-stale use of buttons
* Enable popup menus on the popup/search pages
* Add audio menu
Diffstat (limited to 'ext/mixed/js/display-audio.js')
-rw-r--r-- | ext/mixed/js/display-audio.js | 185 |
1 files changed, 183 insertions, 2 deletions
diff --git a/ext/mixed/js/display-audio.js b/ext/mixed/js/display-audio.js index e1a9e250..c60831b1 100644 --- a/ext/mixed/js/display-audio.js +++ b/ext/mixed/js/display-audio.js @@ -17,6 +17,7 @@ /* global * AudioSystem + * PopupMenu * api */ @@ -29,6 +30,7 @@ class DisplayAudio { this._autoPlayAudioDelay = 400; this._eventListeners = new EventListenerCollection(); this._cache = new Map(); + this._menuContainer = document.querySelector('#popup-menus'); } get autoPlayAudioDelay() { @@ -58,6 +60,8 @@ class DisplayAudio { for (const button of entry.querySelectorAll('.action-play-audio')) { const expressionIndex = this._getAudioPlayButtonExpressionIndex(button); this._eventListeners.addEventListener(button, 'click', this._onAudioPlayButtonClick.bind(this, definitionIndex, expressionIndex), false); + this._eventListeners.addEventListener(button, 'contextmenu', this._onAudioPlayButtonContextMenu.bind(this, definitionIndex, expressionIndex), false); + this._eventListeners.addEventListener(button, 'menuClose', this._onAudioPlayMenuCloseClick.bind(this, definitionIndex, expressionIndex), false); } } @@ -104,6 +108,8 @@ class DisplayAudio { const expressionReading = this._getExpressionAndReading(definitionIndex, expressionIndex); if (expressionReading === null) { return; } + const buttons = this._getAudioPlayButtons(definitionIndex, expressionIndex); + const {expression, reading} = expressionReading; const audioOptions = this._getAudioOptions(); const {textToSpeechVoice, customSourceUrl, volume} = audioOptions; @@ -136,7 +142,7 @@ class DisplayAudio { // Update details const potentialAvailableAudioCount = this._getPotentialAvailableAudioCount(expression, reading); - for (const button of this._getAudioPlayButtons(definitionIndex, expressionIndex)) { + for (const button of buttons) { const titleDefault = button.dataset.titleDefault || ''; button.title = `${titleDefault}\n${title}`; this._updateAudioPlayButtonBadge(button, potentialAvailableAudioCount); @@ -165,7 +171,37 @@ class DisplayAudio { _onAudioPlayButtonClick(definitionIndex, expressionIndex, e) { e.preventDefault(); - this.playAudio(definitionIndex, expressionIndex); + + if (e.shiftKey) { + this._showAudioMenu(e.currentTarget, definitionIndex, expressionIndex); + } else { + this.playAudio(definitionIndex, expressionIndex); + } + } + + _onAudioPlayButtonContextMenu(definitionIndex, expressionIndex, e) { + e.preventDefault(); + + this._showAudioMenu(e.currentTarget, definitionIndex, expressionIndex); + } + + _onAudioPlayMenuCloseClick(definitionIndex, expressionIndex, e) { + const {detail: {action, item}} = e; + switch (action) { + case 'playAudioFromSource': + { + const {source, index} = item.dataset; + let sourceDetailsMap = null; + if (typeof index !== 'undefined') { + const index2 = Number.parseInt(index, 10); + sourceDetailsMap = new Map([ + [source, {start: index2, end: index2 + 1}] + ]); + } + this.playAudio(definitionIndex, expressionIndex, [source], sourceDetailsMap); + } + break; + } } _getAudioPlayButtonExpressionIndex(button) { @@ -360,4 +396,149 @@ class DisplayAudio { } return count; } + + _showAudioMenu(button, definitionIndex, expressionIndex) { + const expressionReading = this._getExpressionAndReading(definitionIndex, expressionIndex); + if (expressionReading === null) { return; } + + const {expression, reading} = expressionReading; + const popupMenu = this._createMenu(button, expression, reading); + popupMenu.prepare(); + } + + _createMenu(button, expression, reading) { + // Options + const {sources, textToSpeechVoice, customSourceUrl} = this._getAudioOptions(); + const sourceIndexMap = new Map(); + for (let i = 0, ii = sources.length; i < ii; ++i) { + sourceIndexMap.set(sources[i], i); + } + + // Create menu + const menuNode = this._display.displayGenerator.createPopupMenu('audio-button'); + + // Create menu item metadata + const menuItems = []; + const menuItemNodes = menuNode.querySelectorAll('.popup-menu-item'); + for (let i = 0, ii = menuItemNodes.length; i < ii; ++i) { + const node = menuItemNodes[i]; + const {source} = node.dataset; + let optionsIndex = sourceIndexMap.get(source); + if (typeof optionsIndex === 'undefined') { optionsIndex = null; } + menuItems.push({node, source, index: i, optionsIndex}); + } + + // Sort according to source order in options + menuItems.sort((a, b) => { + const ai = a.optionsIndex; + const bi = b.optionsIndex; + if (ai !== null) { + if (bi !== null) { + const i = ai - bi; + if (i !== 0) { return i; } + } else { + return -1; + } + } else { + if (bi !== null) { + return 1; + } + } + return a.index - b.index; + }); + + // Set up items based on cache data + const sourceMap = this._cache.get(this._getExpressionReadingKey(expression, reading)); + const menuEntryMap = new Map(); + let showIcons = false; + for (let i = 0, ii = menuItems.length; i < ii; ++i) { + const {node, source, optionsIndex} = menuItems[i]; + const entries = this._getMenuItemEntries(node, sourceMap, source); + menuEntryMap.set(source, entries); + for (const {node: node2, valid, index} of entries) { + if (valid !== null) { + const icon = node2.querySelector('.popup-menu-item-icon'); + icon.dataset.icon = valid ? 'checkmark' : 'cross'; + showIcons = true; + } + if (index !== null) { + node2.dataset.index = `${index}`; + } + node2.dataset.valid = `${valid}`; + node2.dataset.sourceInOptions = `${optionsIndex !== null}`; + node2.style.order = `${i}`; + } + } + menuNode.dataset.showIcons = `${showIcons}`; + + // Hide options + if (textToSpeechVoice.length === 0) { + this._setMenuItemEntriesHidden(menuEntryMap, 'text-to-speech', true); + this._setMenuItemEntriesHidden(menuEntryMap, 'text-to-speech-reading', true); + } + if (customSourceUrl.length === 0) { + this._setMenuItemEntriesHidden(menuEntryMap, 'custom', true); + } + + // Create popup menu + this._menuContainer.appendChild(menuNode); + return new PopupMenu(button, menuNode); + } + + _getMenuItemEntries(node, sourceMap, source) { + const entries = [{node, valid: null, index: null}]; + + const nextNode = node.nextSibling; + + if (typeof sourceMap === 'undefined') { return entries; } + + const sourceInfo = sourceMap.get(source); + if (typeof sourceInfo === 'undefined') { return entries; } + + const {infoList} = sourceInfo; + if (infoList === null) { return entries; } + + if (infoList.length === 0) { + entries[0].valid = false; + return entries; + } + + const defaultLabel = node.querySelector('.popup-menu-item-label').textContent; + + for (let i = 0, ii = infoList.length; i < ii; ++i) { + // Get/create entry + let entry; + if (i < entries.length) { + entry = entries[i]; + } else { + const node2 = node.cloneNode(true); + nextNode.parentNode.insertBefore(node2, nextNode); + entry = {node: node2, valid: null, index: null}; + entries.push(entry); + } + + // Entry info + entry.index = i; + + const {audio, audioResolved, title} = infoList[i]; + if (audioResolved) { entry.valid = (audio !== null); } + + const labelNode = entry.node.querySelector('.popup-menu-item-label'); + let label = defaultLabel; + if (ii > 1) { label = `${label} ${i + 1}`; } + if (typeof title === 'string' && title.length > 0) { label += `: ${title}`; } + labelNode.textContent = label; + } + + return entries; + } + + _setMenuItemEntriesHidden(menuEntryMap, source, hidden) { + const entries = menuEntryMap.get(source); + if (typeof entries === 'undefined') { return; } + + for (const {node} of entries) { + node.hidden = hidden; + } + } } |