diff options
Diffstat (limited to 'ext/js/settings/profile-conditions-ui.js')
-rw-r--r-- | ext/js/settings/profile-conditions-ui.js | 712 |
1 files changed, 712 insertions, 0 deletions
diff --git a/ext/js/settings/profile-conditions-ui.js b/ext/js/settings/profile-conditions-ui.js new file mode 100644 index 00000000..5fda1dc0 --- /dev/null +++ b/ext/js/settings/profile-conditions-ui.js @@ -0,0 +1,712 @@ +/* + * Copyright (C) 2020-2021 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 + * KeyboardMouseInputField + */ + +class ProfileConditionsUI extends EventDispatcher { + constructor(settingsController) { + super(); + this._settingsController = settingsController; + this._os = null; + this._conditionGroupsContainer = null; + this._addConditionGroupButton = null; + this._children = []; + this._eventListeners = new EventListenerCollection(); + this._defaultType = 'popupLevel'; + this._profileIndex = 0; + this._descriptors = new Map([ + [ + 'popupLevel', + { + displayName: 'Popup Level', + defaultOperator: 'equal', + operators: new Map([ + ['equal', {displayName: '=', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}], + ['notEqual', {displayName: '\u2260', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}], + ['lessThan', {displayName: '<', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}], + ['greaterThan', {displayName: '>', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}], + ['lessThanOrEqual', {displayName: '\u2264', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}], + ['greaterThanOrEqual', {displayName: '\u2265', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}] + ]) + } + ], + [ + 'url', + { + displayName: 'URL', + defaultOperator: 'matchDomain', + operators: new Map([ + ['matchDomain', {displayName: 'Matches Domain', type: 'string', defaultValue: 'example.com', resetDefaultOnChange: true, validate: this._validateDomains.bind(this), normalize: this._normalizeDomains.bind(this)}], + ['matchRegExp', {displayName: 'Matches RegExp', type: 'string', defaultValue: 'example\\.com', resetDefaultOnChange: true, validate: this._validateRegExp.bind(this)}] + ]) + } + ], + [ + 'modifierKeys', + { + displayName: 'Modifier Keys', + defaultOperator: 'are', + operators: new Map([ + ['are', {displayName: 'Are', type: 'modifierKeys', defaultValue: ''}], + ['areNot', {displayName: 'Are Not', type: 'modifierKeys', defaultValue: ''}], + ['include', {displayName: 'Include', type: 'modifierKeys', defaultValue: ''}], + ['notInclude', {displayName: 'Don\'t Include', type: 'modifierKeys', defaultValue: ''}] + ]) + } + ] + ]); + } + + get settingsController() { + return this._settingsController; + } + + get profileIndex() { + return this._profileIndex; + } + + get os() { + return this._os; + } + + set os(value) { + this._os = value; + } + + async prepare(profileIndex) { + const options = await this._settingsController.getOptionsFull(); + const {profiles} = options; + if (profileIndex < 0 || profileIndex >= profiles.length) { return; } + const {conditionGroups} = profiles[profileIndex]; + + this._profileIndex = profileIndex; + this._conditionGroupsContainer = document.querySelector('#profile-condition-groups'); + this._addConditionGroupButton = document.querySelector('#profile-add-condition-group'); + + for (let i = 0, ii = conditionGroups.length; i < ii; ++i) { + this._addConditionGroup(conditionGroups[i], i); + } + + this._eventListeners.addEventListener(this._addConditionGroupButton, 'click', this._onAddConditionGroupButtonClick.bind(this), false); + } + + cleanup() { + this._eventListeners.removeAllEventListeners(); + + for (const child of this._children) { + child.cleanup(); + } + this._children = []; + + this._conditionGroupsContainer = null; + this._addConditionGroupButton = null; + } + + instantiateTemplate(names) { + return this._settingsController.instantiateTemplate(names); + } + + getDescriptorTypes() { + const results = []; + for (const [name, {displayName}] of this._descriptors.entries()) { + results.push({name, displayName}); + } + return results; + } + + getDescriptorOperators(type) { + const info = this._descriptors.get(type); + const results = []; + if (typeof info !== 'undefined') { + for (const [name, {displayName}] of info.operators.entries()) { + results.push({name, displayName}); + } + } + return results; + } + + getDefaultType() { + return this._defaultType; + } + + getDefaultOperator(type) { + const info = this._descriptors.get(type); + return (typeof info !== 'undefined' ? info.defaultOperator : ''); + } + + getOperatorDetails(type, operator) { + const info = this._getOperatorDetails(type, operator); + + const { + displayName=operator, + type: type2='string', + defaultValue='', + resetDefaultOnChange=false, + validate=null, + normalize=null + } = (typeof info === 'undefined' ? {} : info); + + return { + displayName, + type: type2, + defaultValue, + resetDefaultOnChange, + validate, + normalize + }; + } + + getDefaultCondition() { + const type = this.getDefaultType(); + const operator = this.getDefaultOperator(type); + const {defaultValue: value} = this.getOperatorDetails(type, operator); + return {type, operator, value}; + } + + removeConditionGroup(child) { + const index = child.index; + if (index < 0 || index >= this._children.length) { return false; } + + const child2 = this._children[index]; + if (child !== child2) { return false; } + + this._children.splice(index, 1); + child.cleanup(); + + for (let i = index, ii = this._children.length; i < ii; ++i) { + this._children[i].index = i; + } + + this.settingsController.modifyGlobalSettings([{ + action: 'splice', + path: this.getPath('conditionGroups'), + start: index, + deleteCount: 1, + items: [] + }]); + + this._triggerConditionGroupCountChanged(this._children.length); + + return true; + } + + splitValue(value) { + return value.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0); + } + + getPath(property) { + property = (typeof property === 'string' ? `.${property}` : ''); + return `profiles[${this.profileIndex}]${property}`; + } + + createKeyboardMouseInputField(inputNode, mouseButton) { + return new KeyboardMouseInputField(inputNode, mouseButton, this._os); + } + + // Private + + _onAddConditionGroupButtonClick() { + const conditionGroup = { + conditions: [this.getDefaultCondition()] + }; + const index = this._children.length; + + this._addConditionGroup(conditionGroup, index); + + this.settingsController.modifyGlobalSettings([{ + action: 'splice', + path: this.getPath('conditionGroups'), + start: index, + deleteCount: 0, + items: [conditionGroup] + }]); + + this._triggerConditionGroupCountChanged(this._children.length); + } + + _addConditionGroup(conditionGroup, index) { + const child = new ProfileConditionGroupUI(this, index); + child.prepare(conditionGroup); + this._children.push(child); + this._conditionGroupsContainer.appendChild(child.node); + return child; + } + + _getOperatorDetails(type, operator) { + const info = this._descriptors.get(type); + return (typeof info !== 'undefined' ? info.operators.get(operator) : void 0); + } + + _validateInteger(value) { + const number = Number.parseFloat(value); + return Number.isFinite(number) && Math.floor(number) === number; + } + + _validateDomains(value) { + return this.splitValue(value).length > 0; + } + + _validateRegExp(value) { + try { + new RegExp(value, 'i'); + return true; + } catch (e) { + return false; + } + } + + _normalizeInteger(value) { + const number = Number.parseFloat(value); + return `${number}`; + } + + _normalizeDomains(value) { + return this.splitValue(value).join(', '); + } + + _triggerConditionGroupCountChanged(count) { + this.trigger('conditionGroupCountChanged', {count, profileIndex: this._profileIndex}); + } +} + +class ProfileConditionGroupUI { + constructor(parent, index) { + this._parent = parent; + this._index = index; + this._node = null; + this._conditionContainer = null; + this._addConditionButton = null; + this._children = []; + this._eventListeners = new EventListenerCollection(); + } + + get settingsController() { + return this._parent.settingsController; + } + + get parent() { + return this._parent; + } + + get index() { + return this._index; + } + + set index(value) { + this._index = value; + } + + get node() { + return this._node; + } + + get childCount() { + return this._children.length; + } + + prepare(conditionGroup) { + this._node = this._parent.instantiateTemplate('profile-condition-group'); + this._conditionContainer = this._node.querySelector('.profile-condition-list'); + this._addConditionButton = this._node.querySelector('.profile-condition-add-button'); + + const conditions = conditionGroup.conditions; + for (let i = 0, ii = conditions.length; i < ii; ++i) { + this._addCondition(conditions[i], i); + } + + this._eventListeners.addEventListener(this._addConditionButton, 'click', this._onAddConditionButtonClick.bind(this), false); + } + + cleanup() { + this._eventListeners.removeAllEventListeners(); + + for (const child of this._children) { + child.cleanup(); + } + this._children = []; + + if (this._node === null) { return; } + + const node = this._node; + this._node = null; + this._conditionContainer = null; + this._addConditionButton = null; + + if (node.parentNode !== null) { + node.parentNode.removeChild(node); + } + } + + removeCondition(child) { + const index = child.index; + if (index < 0 || index >= this._children.length) { return false; } + + const child2 = this._children[index]; + if (child !== child2) { return false; } + + this._children.splice(index, 1); + child.cleanup(); + + for (let i = index, ii = this._children.length; i < ii; ++i) { + this._children[i].index = i; + } + + this.settingsController.modifyGlobalSettings([{ + action: 'splice', + path: this.getPath('conditions'), + start: index, + deleteCount: 1, + items: [] + }]); + + if (this._children.length === 0) { + this.removeSelf(); + } + + return true; + } + + getPath(property) { + property = (typeof property === 'string' ? `.${property}` : ''); + return this._parent.getPath(`conditionGroups[${this._index}]${property}`); + } + + removeSelf() { + this._parent.removeConditionGroup(this); + } + + // Private + + _onAddConditionButtonClick() { + const condition = this._parent.getDefaultCondition(); + const index = this._children.length; + + this._addCondition(condition, index); + + this.settingsController.modifyGlobalSettings([{ + action: 'splice', + path: this.getPath('conditions'), + start: index, + deleteCount: 0, + items: [condition] + }]); + } + + _addCondition(condition, index) { + const child = new ProfileConditionUI(this, index); + child.prepare(condition); + this._children.push(child); + this._conditionContainer.appendChild(child.node); + return child; + } +} + +class ProfileConditionUI { + constructor(parent, index) { + this._parent = parent; + this._index = index; + this._node = null; + this._typeInput = null; + this._operatorInput = null; + this._valueInputContainer = null; + this._removeButton = null; + this._mouseButton = null; + this._mouseButtonContainer = null; + this._menuButton = null; + this._value = ''; + this._kbmInputField = null; + this._eventListeners = new EventListenerCollection(); + this._inputEventListeners = new EventListenerCollection(); + } + + get settingsController() { + return this._parent.parent.settingsController; + } + + get parent() { + return this._parent; + } + + get index() { + return this._index; + } + + set index(value) { + this._index = value; + } + + get node() { + return this._node; + } + + prepare(condition) { + const {type, operator, value} = condition; + + this._node = this._parent.parent.instantiateTemplate('profile-condition'); + this._typeInput = this._node.querySelector('.profile-condition-type'); + this._typeOptionContainer = this._typeInput.querySelector('optgroup'); + this._operatorInput = this._node.querySelector('.profile-condition-operator'); + this._operatorOptionContainer = this._operatorInput.querySelector('optgroup'); + this._valueInput = this._node.querySelector('.profile-condition-input'); + this._removeButton = this._node.querySelector('.profile-condition-remove'); + this._mouseButton = this._node.querySelector('.mouse-button'); + this._mouseButtonContainer = this._node.querySelector('.mouse-button-container'); + this._menuButton = this._node.querySelector('.profile-condition-menu-button'); + + const operatorDetails = this._getOperatorDetails(type, operator); + this._updateTypes(type); + this._updateOperators(type, operator); + this._updateValueInput(value, operatorDetails); + + this._eventListeners.addEventListener(this._typeInput, 'change', this._onTypeChange.bind(this), false); + this._eventListeners.addEventListener(this._operatorInput, 'change', this._onOperatorChange.bind(this), false); + if (this._removeButton !== null) { this._eventListeners.addEventListener(this._removeButton, 'click', this._onRemoveButtonClick.bind(this), false); } + if (this._menuButton !== null) { + this._eventListeners.addEventListener(this._menuButton, 'menuOpen', this._onMenuOpen.bind(this), false); + this._eventListeners.addEventListener(this._menuButton, 'menuClose', this._onMenuClose.bind(this), false); + } + } + + cleanup() { + this._eventListeners.removeAllEventListeners(); + this._value = ''; + + if (this._node === null) { return; } + + const node = this._node; + this._node = null; + this._typeInput = null; + this._operatorInput = null; + this._valueInputContainer = null; + this._removeButton = null; + + if (node.parentNode !== null) { + node.parentNode.removeChild(node); + } + } + + getPath(property) { + property = (typeof property === 'string' ? `.${property}` : ''); + return this._parent.getPath(`conditions[${this._index}]${property}`); + } + + // Private + + _onTypeChange(e) { + const type = e.currentTarget.value; + this._setType(type); + } + + _onOperatorChange(e) { + const type = this._typeInput.value; + const operator = e.currentTarget.value; + this._setOperator(type, operator); + } + + _onValueInputChange({validate, normalize}, e) { + const node = e.currentTarget; + const value = node.value; + const okay = this._validateValue(value, validate); + this._value = value; + if (okay) { + const normalizedValue = this._normalizeValue(value, normalize); + node.value = normalizedValue; + this.settingsController.setGlobalSetting(this.getPath('value'), normalizedValue); + } + } + + _onModifierInputChange({validate, normalize}, {modifiers}) { + modifiers = this._joinModifiers(modifiers); + const okay = this._validateValue(modifiers, validate); + this._value = modifiers; + if (okay) { + const normalizedValue = this._normalizeValue(modifiers, normalize); + this.settingsController.setGlobalSetting(this.getPath('value'), normalizedValue); + } + } + + _onRemoveButtonClick() { + this._removeSelf(); + } + + _onMenuOpen(e) { + const bodyNode = e.detail.menu.bodyNode; + const deleteGroup = bodyNode.querySelector('.popup-menu-item[data-menu-action="deleteGroup"]'); + if (deleteGroup !== null) { + deleteGroup.hidden = (this._parent.childCount <= 1); + } + } + + _onMenuClose(e) { + switch (e.detail.action) { + case 'delete': + this._removeSelf(); + break; + case 'deleteGroup': + this._parent.removeSelf(); + break; + case 'resetValue': + this._resetValue(); + break; + } + } + + _getDescriptorTypes() { + return this._parent.parent.getDescriptorTypes(); + } + + _getDescriptorOperators(type) { + return this._parent.parent.getDescriptorOperators(type); + } + + _getOperatorDetails(type, operator) { + return this._parent.parent.getOperatorDetails(type, operator); + } + + _updateTypes(type) { + const types = this._getDescriptorTypes(); + this._updateSelect(this._typeInput, this._typeOptionContainer, types, type); + } + + _updateOperators(type, operator) { + const operators = this._getDescriptorOperators(type); + this._updateSelect(this._operatorInput, this._operatorOptionContainer, operators, operator); + } + + _updateSelect(select, optionContainer, values, value) { + optionContainer.textContent = ''; + for (const {name, displayName} of values) { + const option = document.createElement('option'); + option.value = name; + option.textContent = displayName; + optionContainer.appendChild(option); + } + select.value = value; + } + + _updateValueInput(value, {type, validate, normalize}) { + this._inputEventListeners.removeAllEventListeners(); + if (this._kbmInputField !== null) { + this._kbmInputField.cleanup(); + this._kbmInputField = null; + } + + let inputType = 'text'; + let inputValue = value; + let inputStep = null; + let showMouseButton = false; + const events = []; + const inputData = {validate, normalize}; + const node = this._valueInput; + + switch (type) { + case 'integer': + inputType = 'number'; + inputStep = '1'; + events.push(['addEventListener', node, 'change', this._onValueInputChange.bind(this, inputData), false]); + break; + case 'modifierKeys': + case 'modifierInputs': + inputValue = null; + showMouseButton = (type === 'modifierInputs'); + this._kbmInputField = this._parent.parent.createKeyboardMouseInputField(node, this._mouseButton); + this._kbmInputField.prepare(null, this._splitModifiers(value), showMouseButton, false); + events.push(['on', this._kbmInputField, 'change', this._onModifierInputChange.bind(this, inputData), false]); + break; + default: // 'string' + events.push(['addEventListener', node, 'change', this._onValueInputChange.bind(this, inputData), false]); + break; + } + + this._value = value; + delete node.dataset.invalid; + node.type = inputType; + if (inputValue !== null) { + node.value = inputValue; + } + if (typeof inputStep === 'string') { + node.step = inputStep; + } else { + node.removeAttribute('step'); + } + this._mouseButtonContainer.hidden = !showMouseButton; + for (const args of events) { + this._inputEventListeners.addGeneric(...args); + } + + this._validateValue(value, validate); + } + + _validateValue(value, validate) { + const okay = (validate === null || validate(value)); + this._valueInput.dataset.invalid = `${!okay}`; + return okay; + } + + _normalizeValue(value, normalize) { + return (normalize !== null ? normalize(value) : value); + } + + _removeSelf() { + this._parent.removeCondition(this); + } + + _splitModifiers(modifiersString) { + return modifiersString.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0); + } + + _joinModifiers(modifiersArray) { + return modifiersArray.join(', '); + } + + async _setType(type, operator) { + const operators = this._getDescriptorOperators(type); + if (typeof operator === 'undefined') { + operator = operators.length > 0 ? operators[0].name : ''; + } + const operatorDetails = this._getOperatorDetails(type, operator); + const {defaultValue} = operatorDetails; + this._updateSelect(this._operatorInput, this._operatorOptionContainer, operators, operator); + this._updateValueInput(defaultValue, operatorDetails); + await this.settingsController.modifyGlobalSettings([ + {action: 'set', path: this.getPath('type'), value: type}, + {action: 'set', path: this.getPath('operator'), value: operator}, + {action: 'set', path: this.getPath('value'), value: defaultValue} + ]); + } + + async _setOperator(type, operator) { + const operatorDetails = this._getOperatorDetails(type, operator); + const settingsModifications = [{action: 'set', path: this.getPath('operator'), value: operator}]; + if (operatorDetails.resetDefaultOnChange) { + const {defaultValue} = operatorDetails; + const okay = this._updateValueInput(defaultValue, operatorDetails); + if (okay) { + settingsModifications.push({action: 'set', path: this.getPath('value'), value: defaultValue}); + } + } + await this.settingsController.modifyGlobalSettings(settingsModifications); + } + + async _resetValue() { + const type = this._typeInput.value; + const operator = this._operatorInput.value; + await this._setType(type, operator); + } +} |