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:
parent
cda04b576d
commit
e7035dcff4
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user