From bcbd413e571d772a4438f57138169ad1a6a3b5a8 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Wed, 31 Mar 2021 18:17:28 -0400 Subject: Definition collapsing when overflowing (#1575) * Add double-down-chevron.svg * Add options * Update dictionary importers * Update settings * Add support for collapsible definitions * Improve case when there is a very small amount of overflow * Fix incorrect enabled state of newly imported dictionaries --- ext/js/display/display.js | 6 ++ ext/js/display/element-overflow-controller.js | 129 ++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 ext/js/display/element-overflow-controller.js (limited to 'ext/js/display') 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 . + */ + +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); + } + } +} -- cgit v1.2.3