diff options
Diffstat (limited to 'ext/js')
-rw-r--r-- | ext/js/data/options-util.js | 4 | ||||
-rw-r--r-- | ext/js/display/display.js | 6 | ||||
-rw-r--r-- | ext/js/display/element-overflow-controller.js | 129 | ||||
-rw-r--r-- | ext/js/pages/settings/backup-controller.js | 2 | ||||
-rw-r--r-- | ext/js/pages/settings/collapsible-dictionary-controller.js | 159 | ||||
-rw-r--r-- | ext/js/pages/settings/dictionary-controller.js | 19 | ||||
-rw-r--r-- | ext/js/pages/settings/dictionary-import-controller.js | 11 | ||||
-rw-r--r-- | ext/js/pages/settings/settings-main.js | 4 |
8 files changed, 315 insertions, 19 deletions
diff --git a/ext/js/data/options-util.js b/ext/js/data/options-util.js index 5b2e2bd3..ca122e89 100644 --- a/ext/js/data/options-util.js +++ b/ext/js/data/options-util.js @@ -755,9 +755,13 @@ class OptionsUtil { // Removed global option useSettingsV2. // Added part-of-speech field template. // Added an argument to hotkey inputs. + // Added definitionsCollapsible to dictionary options. await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v10.handlebars'); delete options.global.useSettingsV2; for (const profile of options.profiles) { + for (const dictionaryOptions of Object.values(profile.options.dictionaries)) { + dictionaryOptions.definitionsCollapsible = 'not-collapsible'; + } for (const hotkey of profile.options.inputs.hotkeys) { switch (hotkey.action) { case 'previousEntry': diff --git a/ext/js/display/display.js b/ext/js/display/display.js index ee2448d6..b17a6168 100644 --- a/ext/js/display/display.js +++ b/ext/js/display/display.js @@ -23,6 +23,7 @@ * DisplayHistory * DisplayNotification * DocumentUtil + * ElementOverflowController * FrameEndpoint * Frontend * HotkeyHelpController @@ -114,6 +115,7 @@ class Display extends EventDispatcher { this._ankiNoteNotificationEventListeners = null; this._queryPostProcessor = null; this._optionToggleHotkeyHandler = new OptionToggleHotkeyHandler(this); + this._elementOverflowController = new ElementOverflowController(); this._hotkeyHandler.registerActions([ ['close', () => { this._onHotkeyClose(); }], @@ -305,6 +307,7 @@ class Display extends EventDispatcher { this._hotkeyHelpController.setOptions(options); this._displayGenerator.updateHotkeys(); this._hotkeyHelpController.setupNode(document.documentElement); + this._elementOverflowController.setOptions(options); this._queryParser.setOptions({ selectedParser: options.parsing.selectedParser, @@ -535,6 +538,7 @@ class Display extends EventDispatcher { this._hideAnkiNoteErrors(false); this._definitions = []; this._definitionNodes = []; + this._elementOverflowController.clearElements(); // Prepare const urlSearchParams = new URLSearchParams(location.search); @@ -966,6 +970,8 @@ class Display extends EventDispatcher { if (focusEntry === i) { this._focusEntry(i, false); } + + this._elementOverflowController.addElements(entry); } if (typeof scrollX === 'number' || typeof scrollY === 'number') { diff --git a/ext/js/display/element-overflow-controller.js b/ext/js/display/element-overflow-controller.js new file mode 100644 index 00000000..ccea1417 --- /dev/null +++ b/ext/js/display/element-overflow-controller.js @@ -0,0 +1,129 @@ +/* + * Copyright (C) 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/>. + */ + +class ElementOverflowController { + constructor() { + this._elements = []; + this._checkTimer = null; + this._eventListeners = new EventListenerCollection(); + this._windowEventListeners = new EventListenerCollection(); + this._dictionaries = new Map(); + this._updateBind = this._update.bind(this); + this._onWindowResizeBind = this._onWindowResize.bind(this); + this._onToggleButtonClickBind = this._onToggleButtonClick.bind(this); + } + + setOptions(options) { + this._dictionaries.clear(); + for (const [dictionary, {definitionsCollapsible}] of Object.entries(options.dictionaries)) { + let collapsible = false; + let collapsed = false; + switch (definitionsCollapsible) { + case 'expanded': + collapsible = true; + break; + case 'collapsed': + collapsible = true; + collapsed = true; + break; + } + if (!collapsible) { continue; } + this._dictionaries.set(dictionary, {collapsed}); + } + } + + addElements(entry) { + if (this._dictionaries.size === 0) { return; } + + const elements = entry.querySelectorAll('.definition-item-inner'); + let any = false; + for (const element of elements) { + const {dictionary} = element.parentNode.dataset; + const dictionaryInfo = this._dictionaries.get(dictionary); + if (typeof dictionaryInfo === 'undefined') { continue; } + + this._updateElement(element); + this._elements.push(element); + + if (dictionaryInfo.collapsed) { + element.classList.add('collapsed'); + } + + const button = element.querySelector('.definition-item-expansion-button'); + if (button !== null) { + this._eventListeners.addEventListener(button, 'click', this._onToggleButtonClickBind, false); + } + + any = true; + } + + if (any && this._windowEventListeners.size === 0) { + this._windowEventListeners.addEventListener(window, 'resize', this._onWindowResizeBind, false); + } + } + + clearElements() { + if (this._elements.length === 0) { return; } + this._elements = []; + this._windowEventListeners.removeAllEventListeners(); + } + + // Private + + _onWindowResize() { + if (this._checkTimer !== null) { + this._cancelIdleCallback(this._checkTimer); + } + this._checkTimer = this._requestIdleCallback(this._updateBind, 100); + } + + _onToggleButtonClick(e) { + const container = e.currentTarget.closest('.definition-item-inner'); + if (container === null) { return; } + container.classList.toggle('collapsed'); + } + + _update() { + for (const element of this._elements) { + this._updateElement(element); + } + } + + _updateElement(element) { + const {classList} = element; + classList.add('collapse-test'); + const collapsible = element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth; + classList.toggle('collapsible', collapsible); + classList.remove('collapse-test'); + } + + _requestIdleCallback(callback, timeout) { + if (typeof requestIdleCallback === 'function') { + return requestIdleCallback(callback, {timeout}); + } else { + return setTimeout(callback, timeout); + } + } + + _cancelIdleCallback(handle) { + if (typeof cancelIdleCallback === 'function') { + cancelIdleCallback(handle); + } else { + clearTimeout(handle); + } + } +} diff --git a/ext/js/pages/settings/backup-controller.js b/ext/js/pages/settings/backup-controller.js index 6f6614b6..933f0e2a 100644 --- a/ext/js/pages/settings/backup-controller.js +++ b/ext/js/pages/settings/backup-controller.js @@ -404,7 +404,7 @@ class BackupController { const dictionaries = await this._settingsController.getDictionaryInfo(); for (const {options: {dictionaries: optionsDictionaries}} of optionsFull.profiles) { for (const {title} of dictionaries) { - optionsDictionaries[title] = DictionaryController.createDefaultDictionarySettings(); + optionsDictionaries[title] = DictionaryController.createDefaultDictionarySettings(false); } } diff --git a/ext/js/pages/settings/collapsible-dictionary-controller.js b/ext/js/pages/settings/collapsible-dictionary-controller.js new file mode 100644 index 00000000..a3a1df62 --- /dev/null +++ b/ext/js/pages/settings/collapsible-dictionary-controller.js @@ -0,0 +1,159 @@ +/* + * Copyright (C) 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 + * ObjectPropertyAccessor + */ + +class CollapsibleDictionaryController { + constructor(settingsController) { + this._settingsController = settingsController; + this._getDictionaryInfoToken = null; + this._dictionaryInfoMap = new Map(); + this._container = null; + this._selects = []; + this._allSelect = null; + this._eventListeners = new EventListenerCollection(); + } + + async prepare() { + this._container = document.querySelector('#collapsible-dictionary-list'); + + await this._onDatabaseUpdated(); + await this._updateOptions(); + + yomichan.on('databaseUpdated', this._onDatabaseUpdated.bind(this)); + this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); + } + + // Private + + async _onDatabaseUpdated() { + const token = {}; + this._getDictionaryInfoToken = token; + const dictionaries = await this._settingsController.getDictionaryInfo(); + if (this._getDictionaryInfoToken !== token) { return; } + this._getDictionaryInfoToken = null; + + this._dictionaryInfoMap.clear(); + for (const entry of dictionaries) { + this._dictionaryInfoMap.set(entry.title, entry); + } + + await this._updateOptions(); + } + + _onOptionsChanged({options}) { + this._eventListeners.removeAllEventListeners(); + this._selects = []; + + const {dictionaries} = options; + + const fragment = document.createDocumentFragment(); + + this._setupAllSelect(fragment, options); + + for (const dictionary of Object.keys(dictionaries)) { + const dictionaryInfo = this._dictionaryInfoMap.get(dictionary); + if (typeof dictionaryInfo === 'undefined') { continue; } + + const select = this._addSelect(fragment, dictionary, `rev.${dictionaryInfo.revision}`); + select.dataset.setting = ObjectPropertyAccessor.getPathString(['dictionaries', dictionary, 'definitionsCollapsible']); + this._eventListeners.addEventListener(select, 'settingChanged', this._onDefinitionsCollapsibleChange.bind(this), false); + + this._selects.push(select); + } + + this._container.textContent = ''; + this._container.appendChild(fragment); + } + + _onDefinitionsCollapsibleChange() { + this._updateAllSelectFresh(); + } + + _onAllSelectChange(e) { + const {value} = e.currentTarget; + if (value === 'varies') { return; } + this._setDefinitionsCollapsibleAll(value); + } + + _setupAllSelect(fragment, options) { + const select = this._addSelect(fragment, 'All', ''); + + const option = document.createElement('option'); + option.value = 'varies'; + option.textContent = 'Varies'; + option.disabled = true; + select.appendChild(option); + + this._eventListeners.addEventListener(select, 'change', this._onAllSelectChange.bind(this), false); + + this._allSelect = select; + this._updateAllSelect(options); + } + + _addSelect(fragment, dictionary, version) { + const node = this._settingsController.instantiateTemplate('collapsible-dictionary-item'); + fragment.appendChild(node); + + const nameNode = node.querySelector('.dictionary-title'); + nameNode.textContent = dictionary; + + const versionNode = node.querySelector('.dictionary-version'); + versionNode.textContent = version; + + return node.querySelector('.definitions-collapsible'); + } + + async _updateOptions() { + const options = await this._settingsController.getOptions(); + this._onOptionsChanged({options}); + } + + async _updateAllSelectFresh() { + this._updateAllSelect(await this._settingsController.getOptions()); + } + + _updateAllSelect(options) { + let value = null; + let varies = false; + for (const {definitionsCollapsible} of Object.values(options.dictionaries)) { + if (value === null) { + value = definitionsCollapsible; + } else if (value !== definitionsCollapsible) { + varies = true; + break; + } + } + + this._allSelect.value = (varies || value === null ? 'varies' : value); + } + + async _setDefinitionsCollapsibleAll(value) { + const options = await this._settingsController.getOptions(); + const targets = []; + for (const dictionary of Object.keys(options.dictionaries)) { + const path = ObjectPropertyAccessor.getPathString(['dictionaries', dictionary, 'definitionsCollapsible']); + targets.push({action: 'set', path, value}); + } + await this._settingsController.modifyProfileSettings(targets); + for (const select of this._selects) { + select.value = value; + } + } +}
\ No newline at end of file diff --git a/ext/js/pages/settings/dictionary-controller.js b/ext/js/pages/settings/dictionary-controller.js index 17abfa13..65edcb67 100644 --- a/ext/js/pages/settings/dictionary-controller.js +++ b/ext/js/pages/settings/dictionary-controller.js @@ -248,6 +248,15 @@ class DictionaryController { this._updateDictionariesEnabledWarnings(options); } + static createDefaultDictionarySettings(enabled) { + return { + priority: 0, + enabled, + allowSecondarySearches: false, + definitionsCollapsible: 'not-collapsible' + }; + } + // Private _onOptionsChanged({options}) { @@ -535,7 +544,7 @@ class DictionaryController { targets.push({ action: 'set', path, - value: DictionaryController.createDefaultDictionarySettings() + value: DictionaryController.createDefaultDictionarySettings(false) }); } } @@ -548,12 +557,4 @@ class DictionaryController { _triggerStorageChanged() { yomichan.trigger('storageChanged'); } - - static createDefaultDictionarySettings() { - return { - enabled: false, - allowSecondarySearches: false, - priority: 0 - }; - } } diff --git a/ext/js/pages/settings/dictionary-import-controller.js b/ext/js/pages/settings/dictionary-import-controller.js index ce724263..afa45899 100644 --- a/ext/js/pages/settings/dictionary-import-controller.js +++ b/ext/js/pages/settings/dictionary-import-controller.js @@ -16,6 +16,7 @@ */ /* global + * DictionaryController * DictionaryDatabase * DictionaryImporter * ObjectPropertyAccessor @@ -212,7 +213,7 @@ class DictionaryImportController { const profileCount = optionsFull.profiles.length; for (let i = 0; i < profileCount; ++i) { const {options} = optionsFull.profiles[i]; - const value = this._createDictionaryOptions(); + const value = DictionaryController.createDefaultDictionarySettings(true); const path1 = ObjectPropertyAccessor.getPathString(['profiles', i, 'options', 'dictionaries', title]); targets.push({action: 'set', path: path1, value}); @@ -291,14 +292,6 @@ class DictionaryImportController { }); } - _createDictionaryOptions() { - return { - priority: 0, - enabled: true, - allowSecondarySearches: false - }; - } - _errorToString(error) { error = (typeof error.toString === 'function' ? error.toString() : `${error}`); diff --git a/ext/js/pages/settings/settings-main.js b/ext/js/pages/settings/settings-main.js index 3618836c..2560685c 100644 --- a/ext/js/pages/settings/settings-main.js +++ b/ext/js/pages/settings/settings-main.js @@ -20,6 +20,7 @@ * AnkiTemplatesController * AudioController * BackupController + * CollapsibleDictionaryController * DictionaryController * DictionaryImportController * DocumentFocusController @@ -146,6 +147,9 @@ async function setupGenericSettingsController(genericSettingController) { const mecabController = new MecabController(); mecabController.prepare(); + const collapsibleDictionaryController = new CollapsibleDictionaryController(settingsController); + collapsibleDictionaryController.prepare(); + await Promise.all(preparePromises); document.documentElement.dataset.loaded = 'true'; |