diff --git a/ext/bg/js/settings/anki-templates-controller.js b/ext/bg/js/settings/anki-templates-controller.js index c980bfa2..f2e5be43 100644 --- a/ext/bg/js/settings/anki-templates-controller.js +++ b/ext/bg/js/settings/anki-templates-controller.js @@ -17,6 +17,7 @@ /* global * AnkiNoteBuilder + * Modal * TemplateRendererProxy * api */ @@ -28,12 +29,15 @@ class AnkiTemplatesController { this._cachedDefinitionValue = null; this._cachedDefinitionText = null; this._defaultFieldTemplates = null; + this._fieldTemplateResetModal = null; this._templateRenderer = new TemplateRendererProxy(); } async prepare() { this._defaultFieldTemplates = await api.getDefaultAnkiFieldTemplates(); + this._fieldTemplateResetModal = new Modal(document.querySelector('#field-template-reset-modal')); + const markers = new Set([ ...this._ankiController.getFieldMarkers('terms'), ...this._ankiController.getFieldMarkers('kanji') @@ -69,13 +73,13 @@ class AnkiTemplatesController { _onReset(e) { e.preventDefault(); - $('#field-template-reset-modal').modal('show'); + this._fieldTemplateResetModal.setVisible(true); } _onResetConfirm(e) { e.preventDefault(); - $('#field-template-reset-modal').modal('hide'); + this._fieldTemplateResetModal.setVisible(false); const value = this._defaultFieldTemplates; diff --git a/ext/bg/js/settings/backup-controller.js b/ext/bg/js/settings/backup-controller.js index 08ee7070..0676d451 100644 --- a/ext/bg/js/settings/backup-controller.js +++ b/ext/bg/js/settings/backup-controller.js @@ -16,6 +16,7 @@ */ /* global + * Modal * OptionsUtil * api */ @@ -26,12 +27,19 @@ class BackupController { this._settingsExportToken = null; this._settingsExportRevoke = null; this._currentVersion = 0; + this._settingsResetModal = null; + this._settingsImportErrorModal = null; + this._settingsImportWarningModal = null; this._optionsUtil = new OptionsUtil(); } async prepare() { await this._optionsUtil.prepare(); + this._settingsResetModal = new Modal(document.querySelector('#settings-reset-modal')); + this._settingsImportErrorModal = new Modal(document.querySelector('#settings-import-error-modal')); + this._settingsImportWarningModal = new Modal(document.querySelector('#settings-import-warning-modal')); + document.querySelector('#settings-export').addEventListener('click', this._onSettingsExportClick.bind(this), false); document.querySelector('#settings-import').addEventListener('click', this._onSettingsImportClick.bind(this), false); document.querySelector('#settings-import-file').addEventListener('change', this._onSettingsImportFileChange.bind(this), false); @@ -153,14 +161,14 @@ class BackupController { _showSettingsImportError(error) { yomichan.logError(error); document.querySelector('#settings-import-error-modal-message').textContent = `${error}`; - $('#settings-import-error-modal').modal('show'); + this._settingsImportErrorModal.setVisible(true); } async _showSettingsImportWarnings(warnings) { - const modalNode = $('#settings-import-warning-modal'); + const modal = this._settingsImportWarningModal; const buttons = document.querySelectorAll('.settings-import-warning-modal-import-button'); const messageContainer = document.querySelector('#settings-import-warning-modal-message'); - if (modalNode.length === 0 || buttons.length === 0 || messageContainer === null) { + if (buttons.length === 0 || messageContainer === null) { return {result: false}; } @@ -175,7 +183,7 @@ class BackupController { messageContainer.appendChild(fragment); // Show modal - modalNode.modal('show'); + modal.setVisible(true); // Wait for modal to close return new Promise((resolve) => { @@ -185,9 +193,10 @@ class BackupController { result: true, sanitize: e.currentTarget.dataset.importSanitize === 'true' }); - modalNode.modal('hide'); + modal.setVisible(false); }; - const onModalHide = () => { + const onModalVisibilityChanged = ({visible}) => { + if (visible) { return; } complete({result: false}); }; @@ -196,7 +205,7 @@ class BackupController { if (completed) { return; } completed = true; - modalNode.off('hide.bs.modal', onModalHide); + modal.off('visibilityChanged', onModalVisibilityChanged); for (const button of buttons) { button.removeEventListener('click', onButtonClick, false); } @@ -205,7 +214,7 @@ class BackupController { }; // Hook events - modalNode.on('hide.bs.modal', onModalHide); + modal.on('visibilityChanged', onModalVisibilityChanged); for (const button of buttons) { button.addEventListener('click', onButtonClick, false); } @@ -368,11 +377,11 @@ class BackupController { // Resetting _onSettingsResetClick() { - $('#settings-reset-modal').modal('show'); + this._settingsResetModal.setVisible(true); } async _onSettingsResetConfirmClick() { - $('#settings-reset-modal').modal('hide'); + this._settingsResetModal.setVisible(false); // Get default options const optionsFull = this._optionsUtil.getDefault(); diff --git a/ext/bg/js/settings/dictionary-controller.js b/ext/bg/js/settings/dictionary-controller.js index 75022d1f..afc198e2 100644 --- a/ext/bg/js/settings/dictionary-controller.js +++ b/ext/bg/js/settings/dictionary-controller.js @@ -16,6 +16,7 @@ */ /* global + * Modal * ObjectPropertyAccessor * api */ @@ -164,7 +165,7 @@ class DictionaryController { this._checkIntegrityButton = document.querySelector('#dict-check-integrity'); this._dictionaryEntryContainer = document.querySelector('#dict-groups'); this._integrityExtraInfoContainer = document.querySelector('#dict-groups-extra'); - this._deleteDictionaryModal = document.querySelector('#dict-delete-modal'); + this._deleteDictionaryModal = new Modal(document.querySelector('#dict-delete-modal')); yomichan.on('databaseUpdated', this._onDatabaseUpdated.bind(this)); @@ -177,9 +178,9 @@ class DictionaryController { deleteDictionary(dictionaryTitle) { if (this._isDeleting) { return; } const modal = this._deleteDictionaryModal; - modal.dataset.dictionaryTitle = dictionaryTitle; - modal.querySelector('#dict-remove-modal-dict-name').textContent = dictionaryTitle; - this._setModalVisible(modal, true); + modal.node.dataset.dictionaryTitle = dictionaryTitle; + modal.node.querySelector('#dict-remove-modal-dict-name').textContent = dictionaryTitle; + modal.setVisible(true); } // Private @@ -209,11 +210,11 @@ class DictionaryController { e.preventDefault(); const modal = this._deleteDictionaryModal; - this._setModalVisible(modal, false); + modal.setVisible(false); - const title = modal.dataset.dictionaryTitle; + const title = modal.node.dataset.dictionaryTitle; if (typeof title !== 'string') { return; } - delete modal.dataset.dictionaryTitle; + delete modal.node.dataset.dictionaryTitle; this._deleteDictionary(title); } @@ -223,10 +224,6 @@ class DictionaryController { this._checkIntegrity(); } - _setModalVisible(node, visible) { - $(node).modal(visible ? 'show' : 'hide'); - } - _updateMainDictionarySelectOptions(dictionaries) { const fragment = document.createDocumentFragment(); diff --git a/ext/bg/js/settings/dictionary-import-controller.js b/ext/bg/js/settings/dictionary-import-controller.js index dd4889dc..a78378e8 100644 --- a/ext/bg/js/settings/dictionary-import-controller.js +++ b/ext/bg/js/settings/dictionary-import-controller.js @@ -18,6 +18,7 @@ /* global * DictionaryDatabase * DictionaryImporter + * Modal * ObjectPropertyAccessor * api */ @@ -53,7 +54,7 @@ class DictionaryImportController { this._purgeConfirmButton = document.querySelector('#dict-purge-confirm'); this._importFileButton = document.querySelector('#dict-file-button'); this._importFileInput = document.querySelector('#dict-file'); - this._purgeConfirmModal = document.querySelector('#dict-purge-modal'); + this._purgeConfirmModal = new Modal(document.querySelector('#dict-purge-modal')); this._errorContainer = document.querySelector('#dict-error'); this._spinner = document.querySelector('#dict-spinner'); this._progressContainer = document.querySelector('#dict-import-progress'); @@ -75,12 +76,12 @@ class DictionaryImportController { _onPurgeButtonClick(e) { e.preventDefault(); - this._setPurgeModalVisible(true); + this._purgeConfirmModal.setVisible(true); } _onPurgeConfirmButtonClick(e) { e.preventDefault(); - this._setPurgeModalVisible(false); + this._purgeConfirmModal.setVisible(false); this._purgeDatabase(); } @@ -220,11 +221,6 @@ class DictionaryImportController { return await this._modifyGlobalSettings(targets); } - _setPurgeModalVisible(visible) { - const node = $(this._purgeConfirmModal); - node.modal(visible ? 'show' : 'hide'); - } - _setSpinnerVisible(visible) { this._spinner.hidden = !visible; } diff --git a/ext/bg/js/settings/modal.js b/ext/bg/js/settings/modal.js new file mode 100644 index 00000000..42a511ca --- /dev/null +++ b/ext/bg/js/settings/modal.js @@ -0,0 +1,65 @@ +/* + * 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 . + */ + +class Modal extends EventDispatcher { + constructor(node) { + super(); + this._node = node; + this._eventListeners = new EventListenerCollection(); + } + + get node() { + return this._node; + } + + setVisible(value) { + this._getWrappedNode().modal(value ? 'show' : 'hide'); + } + + on(eventName, callback) { + if (eventName === 'visibilityChanged') { + if (this._eventListeners.size === 0) { + const wrappedNode = this._getWrappedNode(); + this._eventListeners.on(wrappedNode, 'hidden.bs.modal', this._onModalHide.bind(this)); + this._eventListeners.on(wrappedNode, 'shown.bs.modal', this._onModalShow.bind(this)); + } + } + return super.on(eventName, callback); + } + + off(eventName, callback) { + const result = super.off(eventName, callback); + if (eventName === 'visibilityChanged' && !this.hasListeners(eventName)) { + this._eventListeners.removeAllEventListeners(); + } + return result; + } + + // Private + + _onModalHide() { + this.trigger('visibilityChanged', {visible: false}); + } + + _onModalShow() { + this.trigger('visibilityChanged', {visible: true}); + } + + _getWrappedNode() { + return $(this._node); + } +} diff --git a/ext/bg/js/settings/profile-controller.js b/ext/bg/js/settings/profile-controller.js index fd7137be..7c6dfae5 100644 --- a/ext/bg/js/settings/profile-controller.js +++ b/ext/bg/js/settings/profile-controller.js @@ -16,6 +16,7 @@ */ /* global + * Modal * ProfileConditionsUI * api */ @@ -51,8 +52,8 @@ class ProfileController { this._profileCopyButton = document.querySelector('#profile-copy'); this._profileMoveUpButton = document.querySelector('#profile-move-up'); this._profileMoveDownButton = document.querySelector('#profile-move-down'); - this._profileRemoveModal = document.querySelector('#profile-remove-modal'); - this._profileCopyModal = document.querySelector('#profile-copy-modal'); + this._profileRemoveModal = new Modal(document.querySelector('#profile-remove-modal')); + this._profileCopyModal = new Modal(document.querySelector('#profile-copy-modal')); this._profileActiveSelect.addEventListener('change', this._onProfileActiveChange.bind(this), false); this._profileTargetSelect.addEventListener('change', this._onProfileTargetChange.bind(this), false); @@ -135,11 +136,11 @@ class ProfileController { const profileIndex = this._settingsController.profileIndex; const profile = this._optionsFull.profiles[profileIndex]; this._removeProfileNameElement.textContent = profile.name; - this._setModalVisible(this._profileRemoveModal, true); + this._profileRemoveModal.setVisible(true); } _onRemoveConfirm() { - this._setModalVisible(this._profileRemoveModal, false); + this._profileRemoveModal.setVisible(false); if (this._optionsFull.profiles.length <= 1) { return; } const profileIndex = this._settingsController.profileIndex; this._removeProfile(profileIndex); @@ -160,11 +161,11 @@ class ProfileController { } this._profileCopySourceSelect.value = `${copyFromIndex}`; - this._setModalVisible(this._profileCopyModal, true); + this._profileCopyModal.setVisible(true); } _onCopyConfirm() { - this._setModalVisible(this._profileCopyModal, false); + this._profileCopyModal.setVisible(false); const profileIndex = this._settingsController.profileIndex; const max = this._optionsFull.profiles.length; @@ -265,10 +266,6 @@ class ProfileController { return null; } - _setModalVisible(node, visible) { - $(node).modal(visible ? 'show' : 'hide'); - } - async _addProfile() { const profileIndex = this._settingsController.profileIndex; const profiles = this._optionsFull.profiles; diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 7141776c..9c8621f7 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -1216,6 +1216,7 @@ + diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js index 351d9371..7eb21659 100644 --- a/ext/mixed/js/core.js +++ b/ext/mixed/js/core.js @@ -325,6 +325,11 @@ class EventDispatcher { } return false; } + + hasListeners(eventName) { + const callbacks = this._eventMap.get(eventName); + return (typeof callbacks !== 'undefined' && callbacks.length > 0); + } } class EventListenerCollection {