diff --git a/ext/css/settings.css b/ext/css/settings.css index 7e6c4bc158..dd1aeb6022 100644 --- a/ext/css/settings.css +++ b/ext/css/settings.css @@ -2299,29 +2299,31 @@ button.hotkey-list-item-enabled-button[data-scope-count='0'] { /* Dictionary settings */ .dictionary-list { width: 100%; + display: flex; + flex-direction: column; + margin-top: 0.5em; +} + +.dictionary-item { display: grid; grid-template-columns: auto auto 1fr auto auto auto auto; grid-template-rows: auto; place-items: center start; - margin-top: 0.5em; + --dictionary-item-index-margin: 0.5em; + --dictionary-item-index-width: 1.2em; } -:root:not([data-advanced=true]) .dictionary-list { - grid-template-columns: auto auto 1fr auto auto auto; +.dictionary-item.dragging { + opacity: 0.5; } .dictionary-list-index { - margin-right: 0.5em; -} -.dictionary-list[data-count='0']>.dictionary-item-top { - display: none; + margin-right: var(--dictionary-item-index-margin); } .dictionary-item-button-height { height: var(--icon-button-size); } -.dictionary-item { - display: flex; - flex-flow: row nowrap; - align-items: center; - border-top: var(--thin-border-size) solid var(--separator-color2); +.dictionary-item .generic-list-index-prefix::after { + display: block; + width: var(--dictionary-item-index-width); } .dictionary-item-enabled-toggle-container { margin-right: 0.5em; @@ -2338,13 +2340,10 @@ button.hotkey-list-item-enabled-button[data-scope-count='0'] { color: inherit; transition: color var(--animation-duration) ease-in-out; } -.dictionary-item[data-enabled=false] .dictionary-title { - color: var(--text-color-light2); -} -input[type=number].dictionary-priority { - margin-top: 0; - margin-right: 0.5em; +.dictionary-item.top { + padding-left: calc(var(--dictionary-item-index-width) + var(--dictionary-item-index-margin)); } + .dictionary-outdated-button, .dictionary-update-available, .dictionary-integrity-button { @@ -2408,10 +2407,6 @@ input[type=number].dictionary-priority { width: 100%; } -#dictionary-move-up>span.icon-button-inner, -#dictionary-move-down>span.icon-button-inner { - width: 26px; -} /* Secondary search dictionary settings */ .secondary-search-dictionary-list { @@ -2663,14 +2658,6 @@ input[type=number].dictionary-priority { /* Mobile overrides */ -/* Treat devices that can't hover as mobile devices */ -@media (hover: none) { - #dictionary-move-up>span.icon-button-inner, - #dictionary-move-down>span.icon-button-inner { - width: 36px; - } -} - /* Dark mode before themes are applied DO NOT use this for normal theming */ @media (prefers-color-scheme: dark) { diff --git a/ext/js/pages/settings/dictionary-controller.js b/ext/js/pages/settings/dictionary-controller.js index 5706caf37e..25c2b28c9c 100644 --- a/ext/js/pages/settings/dictionary-controller.js +++ b/ext/js/pages/settings/dictionary-controller.js @@ -26,6 +26,24 @@ import {querySelectorNotNull} from '../../dom/query-selector.js'; const ajvSchemas = /** @type {import('dictionary-importer').CompiledSchemaValidators} */ (/** @type {unknown} */ (ajvSchemas0)); +/** + * Throttles a function to be called at most once per `wait` milliseconds. + * @param {Function} func The function to be throttled. + * @param {number} wait The minimum time (in milliseconds) to wait between function calls. + * @param {unknown} self The value to be passed as the `this` parameter to the throttled function. + * @returns {(this: unknown, ...args: any[]) => void} The throttled function. + */ +function throttle(func, wait, self) { + let lastCall = 0; + return (...args) => { + const now = Date.now(); + if (now - lastCall >= wait) { + lastCall = now; + return func.apply(self, args); + } + }; +} + class DictionaryEntry { /** * @param {DictionaryController} dictionaryController @@ -46,14 +64,10 @@ class DictionaryEntry { this._counts = null; /** @type {ChildNode[]} */ this._nodes = [...fragment.childNodes]; + /** @type {HTMLElement} */ + this._dictionaryItem = querySelectorNotNull(fragment, '.dictionary-item'); /** @type {HTMLInputElement} */ this._enabledCheckbox = querySelectorNotNull(fragment, '.dictionary-enabled'); - /** @type {HTMLInputElement} */ - this._priorityInput = querySelectorNotNull(fragment, '.dictionary-priority'); - /** @type {HTMLButtonElement} */ - this._upButton = querySelectorNotNull(fragment, '#dictionary-move-up'); - /** @type {HTMLButtonElement} */ - this._downButton = querySelectorNotNull(fragment, '#dictionary-move-down'); /** @type {HTMLButtonElement} */ this._menuButton = querySelectorNotNull(fragment, '.dictionary-menu-button'); /** @type {HTMLButtonElement} */ @@ -75,6 +89,16 @@ class DictionaryEntry { return this._dictionaryInfo.title; } + /** @type {HTMLElement} */ + get dictionaryItem() { + return this._dictionaryItem; + } + + /** @type {import('dictionary-importer').Summary} */ + get dictionaryInfo() { + return this._dictionaryInfo; + } + /** */ prepare() { // @@ -84,16 +108,15 @@ class DictionaryEntry { this._aliasNode.dataset.setting = `dictionaries[${index}].alias`; this._versionNode.textContent = `rev.${revision}`; this._outdatedButton.hidden = (version >= 3); - this._priorityInput.dataset.setting = `dictionaries[${index}].priority`; this._enabledCheckbox.dataset.setting = `dictionaries[${index}].enabled`; this._eventListeners.addEventListener(this._enabledCheckbox, 'settingChanged', this._onEnabledChanged.bind(this), false); this._eventListeners.addEventListener(this._menuButton, 'menuOpen', this._onMenuOpen.bind(this), false); this._eventListeners.addEventListener(this._menuButton, 'menuClose', this._onMenuClose.bind(this), false); - this._eventListeners.addEventListener(this._upButton, 'click', (() => { this._move(-1); }).bind(this), false); - this._eventListeners.addEventListener(this._downButton, 'click', (() => { this._move(1); }).bind(this), false); this._eventListeners.addEventListener(this._outdatedButton, 'click', this._onOutdatedButtonClick.bind(this), false); this._eventListeners.addEventListener(this._integrityButton, 'click', this._onIntegrityButtonClick.bind(this), false); this._eventListeners.addEventListener(this._updatesAvailable, 'click', this._onUpdateButtonClick.bind(this), false); + this._eventListeners.addEventListener(this._dictionaryItem, 'dragstart', this._onDragStart.bind(this), false); + this._eventListeners.addEventListener(this._dictionaryItem, 'dragend', this._onDragEnd.bind(this), false); } /** */ @@ -211,6 +234,16 @@ class DictionaryEntry { this._dictionaryController.updateDictionary(this.dictionaryTitle, downloadUrl); } + /** */ + _onDragStart() { + this._dictionaryItem.classList.add('dragging'); + } + + /** */ + _onDragEnd() { + this._dictionaryItem.classList.remove('dragging'); + } + /** */ _onIntegrityButtonClick() { this._showDetails(); @@ -319,13 +352,6 @@ class DictionaryEntry { this._dictionaryController.deleteDictionary(this.dictionaryTitle); } - /** - * @param {number} offset - */ - _move(offset) { - void this._dictionaryController.moveDictionaryOptions(this._index, this._index + offset); - } - /** * @param {Element} menu * @param {string} action @@ -505,6 +531,8 @@ export class DictionaryController { this._extraInfo = null; /** @type {boolean} */ this._isDeleting = false; + /** @type {number|null} */ + this._dragging = null; } /** @type {import('./modal-controller.js').ModalController} */ @@ -532,7 +560,7 @@ export class DictionaryController { const dictionaryMoveButton = querySelectorNotNull(document, '#dictionary-move-button'); /** @type {HTMLButtonElement} */ - const dictiontaryResetAliasButton = querySelectorNotNull(document, '#dictionary-reset-alias-button'); + const dictionaryResetAliasButton = querySelectorNotNull(document, '#dictionary-reset-alias-button'); /** @type {HTMLButtonElement} */ const dictionarySetAliasButton = querySelectorNotNull(document, '#dictionary-set-alias-button'); @@ -545,7 +573,10 @@ export class DictionaryController { dictionaryMoveButton.addEventListener('click', this._onDictionaryMoveButtonClick.bind(this), false); dictionarySetAliasButton.addEventListener('click', this._onDictionarySetAliasButtonClick.bind(this), false); - dictiontaryResetAliasButton.addEventListener('click', this._onDictionaryResetAliasButtonClick.bind(this), false); + dictionaryResetAliasButton.addEventListener('click', this._onDictionaryResetAliasButtonClick.bind(this), false); + + const onDragOverThrottled = throttle(this._onDragOver.bind(this), 100, this); + this._dictionaryEntryContainer.addEventListener('dragover', onDragOverThrottled.bind(this), false); if (this._checkUpdatesButton !== null) { this._checkUpdatesButton.addEventListener('click', this._onCheckUpdatesButtonClick.bind(this), false); @@ -615,7 +646,19 @@ export class DictionaryController { const event = {source: this}; this._settingsController.trigger('dictionarySettingsReordered', event); - await this._updateEntries(); + const movedEntry = this._dictionaryEntries.splice(currentIndex, 1)[0]; + this._dictionaryEntries.splice(targetIndex, 0, movedEntry); + + const nextNeighborIndex = targetIndex + 1; + if (nextNeighborIndex < this._dictionaryEntries.length) { + this._dictionaryEntryContainer.removeChild(movedEntry.dictionaryItem); + const fragment = this._createDictionaryEntry(targetIndex, movedEntry.dictionaryInfo); + this._dictionaryEntryContainer.insertBefore(fragment, this._dictionaryEntries[nextNeighborIndex].dictionaryItem); + } else { + this._dictionaryEntryContainer.removeChild(movedEntry.dictionaryItem); + const fragment = this._createDictionaryEntry(targetIndex, movedEntry.dictionaryInfo); + this._dictionaryEntryContainer.appendChild(fragment); + } } /** @@ -788,7 +831,8 @@ export class DictionaryController { const {name} = dictionaryOptionsArray[i]; const dictionaryInfo = dictionaryInfoMap.get(name); if (typeof dictionaryInfo === 'undefined') { continue; } - this._createDictionaryEntry(i, dictionaryInfo); + const fragment = this._createDictionaryEntry(i, dictionaryInfo); + this._appendDictionaryEntryFragment(fragment); } } @@ -902,6 +946,45 @@ export class DictionaryController { void this.moveDictionaryOptions(indexNumber, target); } + /** + * @param {DragEvent} e + */ + _onDragOver(e) { + // get dragged item from event + console.log('dragover', e); + const draggingIndex = this._dictionaryEntries.findIndex((entry) => entry.dictionaryItem.classList.contains('dragging')); + if (draggingIndex === -1) { return; } + let nextDictionaryIndex = this._getDragOverDictionaryItem(draggingIndex, e.clientY); + if (nextDictionaryIndex === draggingIndex) { return; } + if (nextDictionaryIndex === null) { + nextDictionaryIndex = this._dictionaryEntries.length - 1; + } + void this.moveDictionaryOptions(draggingIndex, nextDictionaryIndex); + } + + /** + * @param {number} draggingIndex + * @param {number} y + * @returns {number|null} + */ + _getDragOverDictionaryItem(draggingIndex, y) { + const neighbors = [draggingIndex - 1, draggingIndex + 1] + .filter((index) => index >= 0 && index < this._dictionaryEntries.length); + + /** @type {{index: number|null, offset: number}} */ + const currentBest = {index: null, offset: Number.NEGATIVE_INFINITY}; + for (const index of neighbors) { + const item = this._dictionaryEntries[index].dictionaryItem; + const {top, height} = item.getBoundingClientRect(); + const offset = y - (top + height / 2); + if (offset < 0 && offset > currentBest.offset) { + currentBest.index = index; + currentBest.offset = offset; + } + } + return currentBest.index; + } + /** */ _onDictionaryResetAliasButtonClick() { const modal = /** @type {import('./modal.js').Modal} */ (this._modalController.getModal('dictionary-set-alias')); @@ -1031,6 +1114,7 @@ export class DictionaryController { /** * @param {number} index * @param {import('dictionary-importer').Summary} dictionaryInfo + * @returns {DocumentFragment} */ _createDictionaryEntry(index, dictionaryInfo) { const fragment = this.instantiateTemplateFragment('dictionary'); @@ -1039,6 +1123,13 @@ export class DictionaryController { this._dictionaryEntries.push(entry); entry.prepare(); + return fragment; + } + + /** + * @param {DocumentFragment} fragment + */ + _appendDictionaryEntryFragment(fragment) { const container = /** @type {HTMLElement} */ (this._dictionaryEntryContainer); const relative = container.querySelector('.dictionary-item-bottom'); container.insertBefore(fragment, relative); diff --git a/ext/settings.html b/ext/settings.html index 1774faad17..071103c9a4 100644 --- a/ext/settings.html +++ b/ext/settings.html @@ -2500,16 +2500,13 @@

Yomitan Settings

-
- -
All
-
Priority
-
-
-
+
+ +
All
+
+
- - +