summaryrefslogtreecommitdiff
path: root/ext/bg/js/settings2
diff options
context:
space:
mode:
Diffstat (limited to 'ext/bg/js/settings2')
-rw-r--r--ext/bg/js/settings2/nested-popups-controller.js71
-rw-r--r--ext/bg/js/settings2/secondary-search-dictionary-controller.js61
-rw-r--r--ext/bg/js/settings2/settings-display-controller.js348
-rw-r--r--ext/bg/js/settings2/settings-main.js128
4 files changed, 608 insertions, 0 deletions
diff --git a/ext/bg/js/settings2/nested-popups-controller.js b/ext/bg/js/settings2/nested-popups-controller.js
new file mode 100644
index 00000000..41bf0e11
--- /dev/null
+++ b/ext/bg/js/settings2/nested-popups-controller.js
@@ -0,0 +1,71 @@
+/*
+ * 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
+ * DOMDataBinder
+ */
+
+class NestedPopupsController {
+ constructor(settingsController) {
+ this._settingsController = settingsController;
+ this._popupNestingMaxDepth = 0;
+ }
+
+ async prepare() {
+ this._nestedPopupsEnabled = document.querySelector('#nested-popups-enabled');
+ this._nestedPopupsCount = document.querySelector('#nested-popups-count');
+ this._nestedPopupsEnabledMoreOptions = document.querySelector('#nested-popups-enabled-more-options');
+
+ const options = await this._settingsController.getOptions();
+
+ this._nestedPopupsEnabled.addEventListener('change', this._onNestedPopupsEnabledChange.bind(this), false);
+ this._nestedPopupsCount.addEventListener('change', this._onNestedPopupsCountChange.bind(this), false);
+ this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
+ this._onOptionsChanged({options});
+ }
+
+ // Private
+
+ _onOptionsChanged({options}) {
+ this._updatePopupNestingMaxDepth(options.scanning.popupNestingMaxDepth);
+ }
+
+ _onNestedPopupsEnabledChange(e) {
+ const value = e.currentTarget.checked;
+ if (value && this._popupNestingMaxDepth > 0) { return; }
+ this._setPopupNestingMaxDepth(value ? 1 : 0);
+ }
+
+ _onNestedPopupsCountChange(e) {
+ const node = e.currentTarget;
+ const value = Math.max(1, DOMDataBinder.convertToNumber(node.value, node));
+ this._setPopupNestingMaxDepth(value);
+ }
+
+ _updatePopupNestingMaxDepth(value) {
+ const enabled = (value > 0);
+ this._popupNestingMaxDepth = value;
+ this._nestedPopupsEnabled.checked = enabled;
+ this._nestedPopupsCount.value = `${value}`;
+ this._nestedPopupsEnabledMoreOptions.hidden = !enabled;
+ }
+
+ async _setPopupNestingMaxDepth(value) {
+ this._updatePopupNestingMaxDepth(value);
+ await this._settingsController.setProfileSetting('scanning.popupNestingMaxDepth', value);
+ }
+}
diff --git a/ext/bg/js/settings2/secondary-search-dictionary-controller.js b/ext/bg/js/settings2/secondary-search-dictionary-controller.js
new file mode 100644
index 00000000..d3820364
--- /dev/null
+++ b/ext/bg/js/settings2/secondary-search-dictionary-controller.js
@@ -0,0 +1,61 @@
+/*
+ * 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
+ * ObjectPropertyAccessor
+ */
+
+class SecondarySearchDictionaryController {
+ constructor(settingsController) {
+ this._settingsController = settingsController;
+ this._getDictionaryInfoToken = null;
+ this._container = null;
+ }
+
+ async prepare() {
+ this._container = document.querySelector('#secondary-search-dictionary-list');
+
+ yomichan.on('databaseUpdated', this._onDatabaseUpdated.bind(this));
+
+ await this._onDatabaseUpdated();
+ }
+
+ // Private
+
+ async _onDatabaseUpdated() {
+ const token = {};
+ this._getDictionaryInfoToken = token;
+ const dictionaries = await this._settingsController.getDictionaryInfo();
+ if (this._getDictionaryInfoToken !== token) { return; }
+ this._getDictionaryInfoToken = null;
+
+ const fragment = document.createDocumentFragment();
+ for (const {title} of dictionaries) {
+ const node = this._settingsController.instantiateTemplate('secondary-search-dictionary');
+ fragment.appendChild(node);
+
+ const nameNode = node.querySelector('.dictionary-name');
+ nameNode.textContent = title;
+
+ const toggle = node.querySelector('.dictionary-allow-secondary-searches');
+ toggle.dataset.setting = ObjectPropertyAccessor.getPathString(['dictionaries', title, 'allowSecondarySearches']);
+ }
+
+ this._container.textContent = '';
+ this._container.appendChild(fragment);
+ }
+} \ No newline at end of file
diff --git a/ext/bg/js/settings2/settings-display-controller.js b/ext/bg/js/settings2/settings-display-controller.js
new file mode 100644
index 00000000..2be7bf92
--- /dev/null
+++ b/ext/bg/js/settings2/settings-display-controller.js
@@ -0,0 +1,348 @@
+/*
+ * 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
+ * PopupMenu
+ * SelectorObserver
+ */
+
+class SettingsDisplayController {
+ constructor(settingsController, modalController) {
+ this._settingsController = settingsController;
+ this._modalController = modalController;
+ this._contentNode = null;
+ this._topLink = null;
+ this._menuContainer = null;
+ this._openPopupMenus = new Set();
+ this._onMoreToggleClickBind = null;
+ this._onMenuButtonClickBind = null;
+ }
+
+ prepare() {
+ this._contentNode = document.querySelector('.content');
+ this._topLink = document.querySelector('.sidebar-top-link');
+ this._menuContainer = document.querySelector('#popup-menus');
+
+ const onFabButtonClick = this._onFabButtonClick.bind(this);
+ for (const fabButton of document.querySelectorAll('.fab-button')) {
+ fabButton.addEventListener('click', onFabButtonClick, false);
+ }
+
+ const onModalAction = this._onModalAction.bind(this);
+ for (const node of document.querySelectorAll('[data-modal-action]')) {
+ node.addEventListener('click', onModalAction, false);
+ }
+
+ const onSelectOnClickElementClick = this._onSelectOnClickElementClick.bind(this);
+ for (const node of document.querySelectorAll('[data-select-on-click]')) {
+ node.addEventListener('click', onSelectOnClickElementClick, false);
+ }
+
+ const onInputTabActionKeyDown = this._onInputTabActionKeyDown.bind(this);
+ for (const node of document.querySelectorAll('[data-tab-action]')) {
+ node.addEventListener('keydown', onInputTabActionKeyDown, false);
+ }
+
+ this._onMoreToggleClickBind = this._onMoreToggleClick.bind(this);
+ const moreSelectorObserver = new SelectorObserver({
+ selector: '.more-toggle',
+ onAdded: this._onMoreSetup.bind(this),
+ onRemoved: this._onMoreCleanup.bind(this)
+ });
+ moreSelectorObserver.observe(document.documentElement, false);
+
+ this._onMenuButtonClickBind = this._onMenuButtonClick.bind(this);
+ const menuSelectorObserver = new SelectorObserver({
+ selector: '[data-menu]',
+ onAdded: this._onMenuSetup.bind(this),
+ onRemoved: this._onMenuCleanup.bind(this)
+ });
+ menuSelectorObserver.observe(document.documentElement, false);
+
+ this._contentNode.addEventListener('scroll', this._onScroll.bind(this), {passive: true});
+ this._topLink.addEventListener('click', this._onTopLinkClick.bind(this), false);
+
+ window.addEventListener('keydown', this._onKeyDown.bind(this), false);
+ window.addEventListener('popstate', this._onPopState.bind(this), false);
+ this._updateScrollTarget();
+ }
+
+ // Private
+
+ _onMoreSetup(element) {
+ element.addEventListener('click', this._onMoreToggleClickBind, false);
+ return null;
+ }
+
+ _onMoreCleanup(element) {
+ element.removeEventListener('click', this._onMoreToggleClickBind, false);
+ }
+
+ _onMenuSetup(element) {
+ element.addEventListener('click', this._onMenuButtonClickBind, false);
+ return null;
+ }
+
+ _onMenuCleanup(element) {
+ element.removeEventListener('click', this._onMenuButtonClickBind, false);
+ }
+
+ _onMenuButtonClick(e) {
+ const element = e.currentTarget;
+ const {menu} = element.dataset;
+ this._showMenu(element, menu);
+ }
+
+ _onScroll(e) {
+ const content = e.currentTarget;
+ const topLink = this._topLink;
+ const scrollTop = content.scrollTop;
+ topLink.hidden = (scrollTop < 100);
+ }
+
+ _onFabButtonClick(e) {
+ const action = e.currentTarget.dataset.action;
+ switch (action) {
+ case 'toggle-sidebar':
+ document.body.classList.toggle('sidebar-visible');
+ break;
+ case 'toggle-preview-sidebar':
+ document.body.classList.toggle('preview-sidebar-visible');
+ break;
+ }
+ }
+
+ _onMoreToggleClick(e) {
+ const container = this._getMoreContainer(e.currentTarget);
+ if (container === null) { return; }
+
+ const more = container.querySelector('.more');
+ if (more === null) { return; }
+
+ const moreVisible = more.hidden;
+ more.hidden = !moreVisible;
+ for (const moreToggle of container.querySelectorAll('.more-toggle')) {
+ const container2 = this._getMoreContainer(moreToggle);
+ if (container2 === null) { continue; }
+
+ const more2 = container2.querySelector('.more');
+ if (more2 === null || more2 !== more) { continue; }
+
+ moreToggle.dataset.expanded = `${moreVisible}`;
+ }
+
+ e.preventDefault();
+ return false;
+ }
+
+ _onPopState() {
+ this._updateScrollTarget();
+ }
+
+ _onKeyDown(e) {
+ switch (e.code) {
+ case 'Escape':
+ if (!this._isElementAnInput(document.activeElement)) {
+ this._closeTopMenuOrModal();
+ e.preventDefault();
+ }
+ break;
+ }
+ }
+
+ _onModalAction(e) {
+ const node = e.currentTarget;
+ const {modalAction} = node.dataset;
+ if (typeof modalAction !== 'string') { return; }
+
+ let [action, target] = modalAction.split(',');
+ if (typeof target === 'undefined') {
+ const currentModal = node.closest('.modal-container');
+ if (currentModal === null) { return; }
+ target = currentModal.id;
+ }
+
+ const modal = this._modalController.getModal(target);
+ if (typeof modal === 'undefined') { return; }
+
+ switch (action) {
+ case 'show':
+ modal.setVisible(true);
+ break;
+ case 'hide':
+ modal.setVisible(false);
+ break;
+ case 'toggle':
+ modal.setVisible(!modal.isVisible());
+ break;
+ }
+
+ e.preventDefault();
+ }
+
+ _onSelectOnClickElementClick(e) {
+ if (e.button !== 0) { return; }
+
+ const node = e.currentTarget;
+ const range = document.createRange();
+ range.selectNode(node);
+
+ const selection = window.getSelection();
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ e.preventDefault();
+ e.stopPropagation();
+ return false;
+ }
+
+ _onTopLinkClick(e) {
+ if (window.location.hash.length > 0) {
+ const {pathname, search} = window.location;
+ const url = `${pathname}${search}`;
+ history.pushState(null, '', url);
+ }
+
+ const content = this._contentNode;
+ content.scrollTop = 0;
+
+ e.preventDefault();
+ e.stopPropagation();
+ return false;
+ }
+
+ _onClosePopupMenu({popupMenu, onClose}) {
+ this._openPopupMenus.delete(popupMenu);
+ popupMenu.off('closed', onClose);
+ }
+
+ _onInputTabActionKeyDown(e) {
+ if (e.key !== 'Tab' || e.ctrlKey) { return; }
+
+ const node = e.currentTarget;
+ const {tabAction} = node.dataset;
+ if (typeof tabAction !== 'string') { return; }
+
+ const args = tabAction.split(',');
+ switch (args[0]) {
+ case 'ignore':
+ e.preventDefault();
+ break;
+ case 'indent':
+ e.preventDefault();
+ this._indentInput(e, node, args);
+ break;
+ }
+ }
+
+ _updateScrollTarget() {
+ const hash = window.location.hash;
+ if (!hash.startsWith('#!')) { return; }
+
+ const content = this._contentNode;
+ const target = document.getElementById(hash.substring(2));
+ if (content === null || target === null) { return; }
+
+ const rect1 = content.getBoundingClientRect();
+ const rect2 = target.getBoundingClientRect();
+ content.scrollTop += rect2.top - rect1.top;
+ this._onScroll({currentTarget: content});
+ }
+
+ _getMoreContainer(link) {
+ const v = link.dataset.parentDistance;
+ const distance = v ? parseInt(v, 10) : 1;
+ if (Number.isNaN(distance)) { return null; }
+
+ for (let i = 0; i < distance; ++i) {
+ link = link.parentNode;
+ if (link === null) { break; }
+ }
+ return link;
+ }
+
+ _closeTopMenuOrModal() {
+ for (const popupMenu of this._openPopupMenus) {
+ popupMenu.close();
+ return;
+ }
+
+ const modal = this._modalController.getTopVisibleModal();
+ if (modal !== null) {
+ modal.setVisible(false);
+ }
+ }
+
+ _showMenu(element, menuName) {
+ const menu = this._settingsController.instantiateTemplate(menuName);
+ if (menu === null) { return; }
+
+ this._menuContainer.appendChild(menu);
+
+ const popupMenu = new PopupMenu(element, menu);
+ this._openPopupMenus.add(popupMenu);
+
+ const data = {popupMenu, onClose: null};
+ data.onClose = this._onClosePopupMenu.bind(this, data);
+
+ popupMenu.on('closed', data.onClose);
+ popupMenu.prepare();
+ }
+
+ _indentInput(e, node, args) {
+ let indent = '\t';
+ if (args.length > 1) {
+ const count = parseInt(args[1], 10);
+ indent = (Number.isFinite(count) && count >= 0 ? ' '.repeat(count) : args[1]);
+ }
+
+ const {selectionStart: start, selectionEnd: end, value} = node;
+ const lineStart = value.substring(0, start).lastIndexOf('\n') + 1;
+ const lineWhitespace = /^[ \t]*/.exec(value.substring(lineStart))[0];
+
+ if (e.shiftKey) {
+ const whitespaceLength = Math.max(0, Math.floor((lineWhitespace.length - 1) / 4) * 4);
+ const selectionStartNew = lineStart + whitespaceLength;
+ const selectionEndNew = lineStart + lineWhitespace.length;
+ const removeCount = selectionEndNew - selectionStartNew;
+ if (removeCount > 0) {
+ node.selectionStart = selectionStartNew;
+ node.selectionEnd = selectionEndNew;
+ document.execCommand('delete', false);
+ node.selectionStart = Math.max(lineStart, start - removeCount);
+ node.selectionEnd = Math.max(lineStart, end - removeCount);
+ }
+ } else {
+ if (indent.length > 0) {
+ const indentLength = (Math.ceil((start - lineStart + 1) / indent.length) * indent.length - (start - lineStart));
+ document.execCommand('insertText', false, indent.substring(0, indentLength));
+ }
+ }
+ }
+
+ _isElementAnInput(element) {
+ const type = element !== null ? element.nodeName.toUpperCase() : null;
+ switch (type) {
+ case 'INPUT':
+ case 'TEXTAREA':
+ case 'SELECT':
+ return true;
+ default:
+ return false;
+ }
+ }
+}
diff --git a/ext/bg/js/settings2/settings-main.js b/ext/bg/js/settings2/settings-main.js
new file mode 100644
index 00000000..da2d1c2d
--- /dev/null
+++ b/ext/bg/js/settings2/settings-main.js
@@ -0,0 +1,128 @@
+/*
+ * 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
+ * AnkiController
+ * AnkiTemplatesController
+ * AudioController
+ * BackupController
+ * ClipboardPopupsController
+ * DictionaryController
+ * DictionaryImportController
+ * GenericSettingController
+ * ModalController
+ * NestedPopupsController
+ * PopupPreviewController
+ * ProfileController
+ * ScanInputsController
+ * ScanInputsSimpleController
+ * SecondarySearchDictionaryController
+ * SettingsController
+ * SettingsDisplayController
+ * StatusFooter
+ * StorageController
+ * api
+ */
+
+async function setupEnvironmentInfo() {
+ const {browser, platform} = await api.getEnvironmentInfo();
+ document.documentElement.dataset.browser = browser;
+ document.documentElement.dataset.os = platform.os;
+}
+
+async function setupGenericSettingsController(genericSettingController) {
+ await genericSettingController.prepare();
+ await genericSettingController.refresh();
+}
+
+(async () => {
+ try {
+ document.querySelector('#content-scroll-focus').focus();
+
+ const statusFooter = new StatusFooter(document.querySelector('.status-footer-container'));
+ statusFooter.prepare();
+
+ api.forwardLogsToBackend();
+ await yomichan.prepare();
+
+ setupEnvironmentInfo();
+
+ const optionsFull = await api.optionsGetFull();
+
+ const preparePromises = [];
+
+ const modalController = new ModalController();
+ modalController.prepare();
+
+ const settingsController = new SettingsController(optionsFull.profileCurrent);
+ settingsController.prepare();
+
+ const storageController = new StorageController();
+ storageController.prepare();
+
+ const dictionaryController = new DictionaryController(settingsController, modalController, statusFooter);
+ dictionaryController.prepare();
+
+ const dictionaryImportController = new DictionaryImportController(settingsController, modalController, storageController, statusFooter);
+ dictionaryImportController.prepare();
+
+ const genericSettingController = new GenericSettingController(settingsController);
+ preparePromises.push(setupGenericSettingsController(genericSettingController));
+
+ const audioController = new AudioController(settingsController);
+ audioController.prepare();
+
+ const profileController = new ProfileController(settingsController, modalController);
+ profileController.prepare();
+
+ const settingsBackup = new BackupController(settingsController, modalController);
+ settingsBackup.prepare();
+
+ const ankiController = new AnkiController(settingsController);
+ ankiController.prepare();
+
+ const ankiTemplatesController = new AnkiTemplatesController(settingsController, modalController, ankiController);
+ ankiTemplatesController.prepare();
+
+ const popupPreviewController = new PopupPreviewController(settingsController);
+ popupPreviewController.prepare();
+
+ const scanInputsController = new ScanInputsController(settingsController);
+ scanInputsController.prepare();
+
+ const simpleScanningInputController = new ScanInputsSimpleController(settingsController);
+ simpleScanningInputController.prepare();
+
+ const nestedPopupsController = new NestedPopupsController(settingsController);
+ nestedPopupsController.prepare();
+
+ const clipboardPopupsController = new ClipboardPopupsController(settingsController);
+ clipboardPopupsController.prepare();
+
+ const secondarySearchDictionaryController = new SecondarySearchDictionaryController(settingsController);
+ secondarySearchDictionaryController.prepare();
+
+ await Promise.all(preparePromises);
+
+ document.documentElement.dataset.loaded = 'true';
+
+ const settingsDisplayController = new SettingsDisplayController(settingsController, modalController);
+ settingsDisplayController.prepare();
+ } catch (e) {
+ yomichan.logError(e);
+ }
+})();