/* * Copyright (C) 2020 Yomichan Authors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ /* global * DocumentUtil */ class KeyboardMouseInputField extends EventDispatcher { constructor(inputNode, mouseButton, os, isPointerTypeSupported=null) { super(); this._inputNode = inputNode; this._mouseButton = mouseButton; this._isPointerTypeSupported = isPointerTypeSupported; this._keySeparator = ' + '; this._inputNameMap = new Map(DocumentUtil.getModifierKeys(os)); this._keyPriorities = new Map([ ['meta', -4], ['ctrl', -3], ['alt', -2], ['shift', -1] ]); this._mouseInputNamePattern = /^mouse(\d+)$/; this._eventListeners = new EventListenerCollection(); this._value = ''; this._type = null; this._penPointerIds = new Set(); } get value() { return this._value; } prepare(value, type) { this.cleanup(); this._value = value; const modifiers = this._splitValue(value); const {displayValue} = this._getInputStrings(modifiers); const events = [ [this._inputNode, 'keydown', this._onModifierKeyDown.bind(this), false] ]; if (type === 'modifierInputs' && this._mouseButton !== null) { events.push( [this._mouseButton, 'mousedown', this._onMouseButtonMouseDown.bind(this), false], [this._mouseButton, 'pointerdown', this._onMouseButtonPointerDown.bind(this), false], [this._mouseButton, 'pointerover', this._onMouseButtonPointerOver.bind(this), false], [this._mouseButton, 'pointerout', this._onMouseButtonPointerOut.bind(this), false], [this._mouseButton, 'pointercancel', this._onMouseButtonPointerCancel.bind(this), false], [this._mouseButton, 'mouseup', this._onMouseButtonMouseUp.bind(this), false], [this._mouseButton, 'contextmenu', this._onMouseButtonContextMenu.bind(this), false] ); } this._inputNode.value = displayValue; for (const args of events) { this._eventListeners.addEventListener(...args); } } cleanup() { this._eventListeners.removeAllEventListeners(); this._value = ''; this._type = null; this._penPointerIds.clear(); } clearInputs() { this._updateInputs([]); } // Private _splitValue(value) { return value.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0); } _sortInputs(inputs) { const pattern = this._mouseInputNamePattern; const keyPriorities = this._keyPriorities; const inputInfos = inputs.map((value, index) => { const match = pattern.exec(value); if (match !== null) { return [value, 1, Number.parseInt(match[1], 10), index]; } else { let priority = keyPriorities.get(value); if (typeof priority === 'undefined') { priority = 0; } return [value, 0, priority, index]; } }); inputInfos.sort((a, b) => { let i = a[1] - b[1]; if (i !== 0) { return i; } i = a[2] - b[2]; if (i !== 0) { return i; } i = a[0].localeCompare(b[0], 'en-US'); // Ensure an invariant culture if (i !== 0) { return i; } i = a[3] - b[3]; return i; }); return inputInfos.map(([value]) => value); } _getInputStrings(inputs) { let value = ''; let displayValue = ''; let first = true; for (const input of inputs) { const {name} = this._getInputName(input); if (first) { first = false; } else { value += ', '; displayValue += this._keySeparator; } value += input; displayValue += name; } return {value, displayValue}; } _getInputName(value) { const pattern = this._mouseInputNamePattern; const match = pattern.exec(value); if (match !== null) { return {name: `Mouse ${match[1]}`, type: 'mouse'}; } let name = this._inputNameMap.get(value); if (typeof name === 'undefined') { name = value; } return {name, type: 'key'}; } _getModifierKeys(e) { const modifiers = new Set(DocumentUtil.getActiveModifiers(e)); // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/metaKey // https://askubuntu.com/questions/567731/why-is-shift-alt-being-mapped-to-meta // It works with mouse events on some platforms, so try to determine if metaKey is pressed. // This is a hack and only works when both Shift and Alt are not pressed. if ( !modifiers.has('meta') && DocumentUtil.getKeyFromEvent(e) === 'Meta' && !( modifiers.size === 2 && modifiers.has('shift') && modifiers.has('alt') ) ) { modifiers.add('meta'); } return modifiers; } _onModifierKeyDown(e) { e.preventDefault(); const key = DocumentUtil.getKeyFromEvent(e); switch (key) { case 'Escape': case 'Backspace': this.clearInputs(); break; default: this._addInputs(this._getModifierKeys(e)); break; } } _onMouseButtonMouseDown(e) { e.preventDefault(); this._addInputs(DocumentUtil.getActiveButtons(e)); } _onMouseButtonPointerDown(e) { if (!e.isPrimary) { return; } let {pointerType, pointerId} = e; // Workaround for Firefox bug not detecting certain 'touch' events as 'pen' events. if (this._penPointerIds.has(pointerId)) { pointerType = 'pen'; } if ( typeof this._isPointerTypeSupported !== 'function' || !this._isPointerTypeSupported(pointerType) ) { return; } e.preventDefault(); this._addInputs(DocumentUtil.getActiveButtons(e)); } _onMouseButtonPointerOver(e) { const {pointerType, pointerId} = e; if (pointerType === 'pen') { this._penPointerIds.add(pointerId); } } _onMouseButtonPointerOut(e) { const {pointerId} = e; this._penPointerIds.delete(pointerId); } _onMouseButtonPointerCancel(e) { this._onMouseButtonPointerOut(e); } _onMouseButtonMouseUp(e) { e.preventDefault(); } _onMouseButtonContextMenu(e) { e.preventDefault(); } _addInputs(newInputs) { const inputs = new Set(this._splitValue(this._value)); for (const input of newInputs) { inputs.add(input); } this._updateInputs([...inputs]); } _updateInputs(inputs) { inputs = this._sortInputs(inputs); const node = this._inputNode; const {value, displayValue} = this._getInputStrings(inputs); node.value = displayValue; if (this._value === value) { return; } this._value = value; this.trigger('change', {value, displayValue}); } }