Enable audio menu shift click (#1555)

* Expose modifier keys

* Add updateMenuItems

* Don't close menu if shift key is held

* Add _createMenuItems

* Simplification

* Maintain a list of open popup menus

* Expose expression/reading

* Reuse existing items

* Update menu after a cache update

* Update menu position
This commit is contained in:
toasted-nutbread 2021-03-25 19:22:34 -04:00 committed by GitHub
parent cda04b576d
commit e7035dcff4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 105 additions and 32 deletions

View File

@ -31,6 +31,7 @@ class DisplayAudio {
this._cache = new Map(); this._cache = new Map();
this._menuContainer = document.querySelector('#popup-menus'); this._menuContainer = document.querySelector('#popup-menus');
this._entriesToken = {}; this._entriesToken = {};
this._openMenus = new Set();
} }
get autoPlayAudioDelay() { get autoPlayAudioDelay() {
@ -198,9 +199,12 @@ class DisplayAudio {
} }
_onAudioPlayMenuCloseClick(definitionIndex, expressionIndex, e) { _onAudioPlayMenuCloseClick(definitionIndex, expressionIndex, e) {
const {detail: {action, item, menu}} = e; const {detail: {action, item, menu, shiftKey}} = e;
switch (action) { switch (action) {
case 'playAudioFromSource': case 'playAudioFromSource':
if (shiftKey) {
e.preventDefault();
}
this._playAudioFromSource(definitionIndex, expressionIndex, item); this._playAudioFromSource(definitionIndex, expressionIndex, item);
break; break;
case 'setPrimaryAudio': case 'setPrimaryAudio':
@ -306,12 +310,14 @@ class DisplayAudio {
for (let i = 0, ii = sources.length; i < ii; ++i) { for (let i = 0, ii = sources.length; i < ii; ++i) {
const source = sources[i]; const source = sources[i];
let cacheUpdated = false;
let infoListPromise; let infoListPromise;
let sourceInfo = sourceMap.get(source); let sourceInfo = sourceMap.get(source);
if (typeof sourceInfo === 'undefined') { if (typeof sourceInfo === 'undefined') {
infoListPromise = this._getExpressionAudioInfoList(source, expression, reading, details); infoListPromise = this._getExpressionAudioInfoList(source, expression, reading, details);
sourceInfo = {infoListPromise, infoList: null}; sourceInfo = {infoListPromise, infoList: null};
sourceMap.set(source, sourceInfo); sourceMap.set(source, sourceInfo);
cacheUpdated = true;
} }
let {infoList} = sourceInfo; let {infoList} = sourceInfo;
@ -332,14 +338,17 @@ class DisplayAudio {
} }
} }
const audio = await this._createAudioFromInfoList(source, infoList, start, end); const {result, cacheUpdated: cacheUpdated2} = await this._createAudioFromInfoList(source, infoList, start, end);
if (audio !== null) { return audio; } if (cacheUpdated || cacheUpdated2) { this._updateOpenMenu(); }
if (result !== null) { return result; }
} }
return null; return null;
} }
async _createAudioFromInfoList(source, infoList, start, end) { async _createAudioFromInfoList(source, infoList, start, end) {
let result = null;
let cacheUpdated = false;
for (let i = start; i < end; ++i) { for (let i = start; i < end; ++i) {
const item = infoList[i]; const item = infoList[i];
@ -352,6 +361,8 @@ class DisplayAudio {
item.audioPromise = audioPromise; item.audioPromise = audioPromise;
} }
cacheUpdated = true;
try { try {
audio = await audioPromise; audio = await audioPromise;
} catch (e) { } catch (e) {
@ -363,11 +374,12 @@ class DisplayAudio {
item.audio = audio; item.audio = audio;
} }
if (audio === null) { continue; } if (audio !== null) {
result = {audio, source, infoListIndex: i};
return {audio, source, infoListIndex: i}; break;
}
} }
return null; return {result, cacheUpdated};
} }
async _createAudioFromInfo(info, source) { async _createAudioFromInfo(info, source) {
@ -471,7 +483,13 @@ class DisplayAudio {
const {expression, reading} = expressionReading; const {expression, reading} = expressionReading;
const popupMenu = this._createMenu(button, expression, reading); const popupMenu = this._createMenu(button, expression, reading);
this._openMenus.add(popupMenu);
popupMenu.prepare(); popupMenu.prepare();
popupMenu.on('close', this._onPopupMenuClose.bind(this));
}
_onPopupMenuClose({menu}) {
this._openMenus.delete(menu);
} }
_sourceIsDownloadable(source) { _sourceIsDownloadable(source) {
@ -533,21 +551,36 @@ class DisplayAudio {
} }
_createMenu(sourceButton, expression, reading) { _createMenu(sourceButton, expression, reading) {
// Options
const sources = this._getAudioSources(this._getAudioOptions());
// Create menu // Create menu
const {displayGenerator} = this._display; const menuContainerNode = this._display.displayGenerator.instantiateTemplate('audio-button-popup-menu');
const menuNode = displayGenerator.instantiateTemplate('audio-button-popup-menu'); const menuBodyNode = menuContainerNode.querySelector('.popup-menu-body');
const menuBodyNode = menuNode.querySelector('.popup-menu-body'); menuContainerNode.dataset.expression = expression;
menuContainerNode.dataset.reading = reading;
// Set up items based on options and cache data // 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; let showIcons = false;
const currentItems = [...menuItemContainer.children];
for (const {source, displayName, isInOptions, downloadable} of sources) { for (const {source, displayName, isInOptions, downloadable} of sources) {
const entries = this._getMenuItemEntries(source, expression, reading); const entries = this._getMenuItemEntries(source, expression, reading);
for (let i = 0, ii = entries.length; i < ii; ++i) { for (let i = 0, ii = entries.length; i < ii; ++i) {
const {valid, index, name} = entries[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'); const labelNode = node.querySelector('.popup-menu-item-audio-button .popup-menu-item-label');
let label = displayName; let label = displayName;
@ -571,17 +604,32 @@ class DisplayAudio {
node.dataset.sourceInOptions = `${isInOptions}`; node.dataset.sourceInOptions = `${isInOptions}`;
node.dataset.downloadable = `${downloadable}`; 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 _getOrCreateMenuItem(currentItems, source, index) {
this._updateMenuPrimaryCardAudio(menuBodyNode, expression, reading); 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 let index2 = node.dataset.index;
this._menuContainer.appendChild(menuNode); if (typeof index2 === 'undefined') { index2 = '0'; }
return new PopupMenu(sourceButton, menuNode); if (index !== index2) { continue; }
currentItems.splice(i, 1);
return node;
}
return null;
} }
_getMenuItemEntries(source, expression, reading) { _getMenuItemEntries(source, expression, reading) {
@ -631,4 +679,13 @@ class DisplayAudio {
node.dataset.isPrimaryCardAudio = `${isPrimaryCardAudio}`; 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();
}
}
} }

View File

@ -24,6 +24,7 @@ class PopupMenu extends EventDispatcher {
this._bodyNode = containerNode.querySelector('.popup-menu-body'); this._bodyNode = containerNode.querySelector('.popup-menu-body');
this._isClosed = false; this._isClosed = false;
this._eventListeners = new EventListenerCollection(); this._eventListeners = new EventListenerCollection();
this._itemEventListeners = new EventListenerCollection();
} }
get sourceElement() { get sourceElement() {
@ -47,17 +48,13 @@ class PopupMenu extends EventDispatcher {
} }
prepare() { prepare() {
const items = this._bodyNode.querySelectorAll('.popup-menu-item');
this._setPosition(); this._setPosition();
this._containerNode.focus(); this._containerNode.focus();
this._eventListeners.addEventListener(window, 'resize', this._onWindowResize.bind(this), false); this._eventListeners.addEventListener(window, 'resize', this._onWindowResize.bind(this), false);
this._eventListeners.addEventListener(this._containerNode, 'click', this._onMenuContainerClick.bind(this), false); this._eventListeners.addEventListener(this._containerNode, 'click', this._onMenuContainerClick.bind(this), false);
const onMenuItemClick = this._onMenuItemClick.bind(this); this.updateMenuItems();
for (const item of items) {
this._eventListeners.addEventListener(item, 'click', onMenuItemClick, false);
}
PopupMenu.openMenus.add(this); PopupMenu.openMenus.add(this);
@ -69,7 +66,20 @@ class PopupMenu extends EventDispatcher {
} }
close(cancelable=true) { 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 // Private
@ -78,7 +88,7 @@ class PopupMenu extends EventDispatcher {
if (e.currentTarget !== e.target) { return; } if (e.currentTarget !== e.target) { return; }
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this._close(null, 'outside', true); this._close(null, 'outside', true, e);
} }
_onMenuItemClick(e) { _onMenuItemClick(e) {
@ -86,11 +96,11 @@ class PopupMenu extends EventDispatcher {
if (item.disabled) { return; } if (item.disabled) { return; }
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this._close(item, 'item', true); this._close(item, 'item', true, e);
} }
_onWindowResize() { _onWindowResize() {
this._close(null, 'resize', true); this._close(null, 'resize', true, {});
} }
_setPosition() { _setPosition() {
@ -172,15 +182,20 @@ class PopupMenu extends EventDispatcher {
menu.style.top = `${y}px`; menu.style.top = `${y}px`;
} }
_close(item, cause, cancelable) { _close(item, cause, cancelable, originalEvent) {
if (this._isClosed) { return true; } if (this._isClosed) { return true; }
const action = (item !== null ? item.dataset.menuAction : null); const action = (item !== null ? item.dataset.menuAction : null);
const {altKey=false, ctrlKey=false, metaKey=false, shiftKey=false} = originalEvent;
const detail = { const detail = {
menu: this, menu: this,
item, item,
action, action,
cause cause,
altKey,
ctrlKey,
metaKey,
shiftKey
}; };
const result = this._sourceElement.dispatchEvent(new CustomEvent('menuClose', {bubbles: false, cancelable, detail})); const result = this._sourceElement.dispatchEvent(new CustomEvent('menuClose', {bubbles: false, cancelable, detail}));
if (cancelable && !result) { return false; } if (cancelable && !result) { return false; }
@ -189,6 +204,7 @@ class PopupMenu extends EventDispatcher {
this._isClosed = true; this._isClosed = true;
this._eventListeners.removeAllEventListeners(); this._eventListeners.removeAllEventListeners();
this._itemEventListeners.removeAllEventListeners();
if (this._containerNode.parentNode !== null) { if (this._containerNode.parentNode !== null) {
this._containerNode.parentNode.removeChild(this._containerNode); this._containerNode.parentNode.removeChild(this._containerNode);
} }