From 5b5b45b88efdd625e5d117c8317bee5bf047b4ff Mon Sep 17 00:00:00 2001
From: toasted-nutbread <toasted-nutbread@users.noreply.github.com>
Date: Sun, 11 Oct 2020 17:31:58 -0400
Subject: Dictionary controller refactor (#912)

* Support multiple main dictionary selects

* Update progress container

* Change class name

* Simplify dictionary info

* Update outdated node visibility

* Simplify node usage

* Simplify title

* Update classes

* Update IDs/classes

* Remove details container visibility assignment

* Use a template for dictionary details

* Fix progress bar visibility

* Fix incorrect property

* Rename dict-details to dictionary-details

* Remove unused classes

* Update class names for dictionary templates

* Rename templates

* More id renaming

* Remove unused id

* Rename more IDs
---
 ext/bg/css/settings.css                            |  31 ++--
 ext/bg/js/settings/dictionary-controller.js        | 156 ++++++++++-----------
 ext/bg/js/settings/dictionary-import-controller.js |  22 +--
 ext/bg/settings.html                               |  87 ++++++------
 4 files changed, 149 insertions(+), 147 deletions(-)

diff --git a/ext/bg/css/settings.css b/ext/bg/css/settings.css
index 9ec2e964..b755b33b 100644
--- a/ext/bg/css/settings.css
+++ b/ext/bg/css/settings.css
@@ -23,7 +23,7 @@
 html:root:not([data-options-anki-enable=true]) #anki-general,
 html:root:not([data-options-general-debug-info=true]) .debug,
 html:root:not([data-options-general-show-advanced=true]) .options-advanced,
-html:root:not([data-options-general-result-output-mode=merge]) #dict-main-group {
+html:root:not([data-options-general-result-output-mode=merge]) #dictionary-main-group {
     display: none;
 }
 
@@ -309,23 +309,26 @@ input[type=checkbox].storage-button-checkbox {
     height: 320px;
 }
 
-.dict-delete-table {
+.dictionary-delete-table {
     display: table;
     width: 100%;
 }
-.dict-delete-table>*:first-child {
+.dictionary-delete-table>*:first-child {
     display: table-cell;
     vertical-align: middle;
     padding-right: 1em;
 }
-.dict-delete-table>*:nth-child(n+2) {
+.dictionary-delete-table>*:nth-child(n+2) {
     display: table-cell;
     width: 100%;
     vertical-align: middle;
 }
-.dict-delete-table .progress {
+.dictionary-delete-table .progress {
     margin: 0;
 }
+.dictionary-delete-table>*[hidden] {
+    display: none;
+}
 
 .error-data-show-button {
     display: inline-block;
@@ -377,44 +380,44 @@ html:root[data-operating-system=openbsd] [data-hide-for-operating-system~=openbs
     display: none;
 }
 
-#dict-groups {
+#dictionary-list {
     display: flex;
     flex-flow: column;
 }
 
-.dict-details-container {
+.dictionary-details-container {
     margin: 0.5em 0;
 }
 
-.dict-details-toggle-link {
+.dictionary-details-toggle-link {
     cursor: pointer;
 }
 
-.dict-details {
+.dictionary-details {
     margin-left: 1em;
 }
 
-.dict-details-table {
+.dictionary-details-table {
     display: table;
     width: 100%
 }
 
-.dict-details-entry {
+.dictionary-details-entry {
     display: table-row;
 }
 
-.dict-details-entry+.dict-details-entry>* {
+.dictionary-details-entry+.dictionary-details-entry>* {
     padding-top: 0.25em;
 }
 
-.dict-details-entry-label {
+.dictionary-details-entry-label {
     display: table-cell;
     font-weight: bold;
     white-space: nowrap;
     padding-right: 0.5em;
 }
 
-.dict-details-entry-info {
+.dictionary-details-entry-info {
     display: table-cell;
     white-space: pre-line;
 }
diff --git a/ext/bg/js/settings/dictionary-controller.js b/ext/bg/js/settings/dictionary-controller.js
index 34a4b933..3db16979 100644
--- a/ext/bg/js/settings/dictionary-controller.js
+++ b/ext/bg/js/settings/dictionary-controller.js
@@ -27,15 +27,8 @@ class DictionaryEntry {
         this._dictionaryController = dictionaryController;
         this._node = node;
         this._dictionaryInfo = dictionaryInfo;
-        this._dictionaryTitle = dictionaryInfo.title;
         this._eventListeners = new EventListenerCollection();
-        this._enabledCheckbox = node.querySelector('.dict-enabled');
-        this._allowSecondarySearchesCheckbox = node.querySelector('.dict-allow-secondary-searches');
-        this._priorityInput = node.querySelector('.dict-priority');
-        this._deleteButton = node.querySelector('.dict-delete-button');
-        this._detailsToggleLink = node.querySelector('.dict-details-toggle-link');
-        this._detailsContainer = node.querySelector('.dict-details');
-        this._detailsTable = node.querySelector('.dict-details-table');
+        this._detailsContainer = null;
     }
 
     get node() {
@@ -43,31 +36,42 @@ class DictionaryEntry {
     }
 
     get dictionaryTitle() {
-        return this._dictionaryTitle;
+        return this._dictionaryInfo.title;
     }
 
     prepare() {
         const node = this._node;
-        const dictionaryInfo = this._dictionaryInfo;
-        const {title, revision, prefixWildcardsSupported} = dictionaryInfo;
+        const {title, revision, prefixWildcardsSupported, version} = this._dictionaryInfo;
 
-        if (dictionaryInfo.version < 3) {
-            node.querySelector('.dict-outdated').hidden = false;
-        }
+        this._detailsContainer = node.querySelector('.dictionary-details');
+
+        const enabledCheckbox = node.querySelector('.dictionary-enabled');
+        const allowSecondarySearchesCheckbox = node.querySelector('.dictionary-allow-secondary-searches');
+        const priorityInput = node.querySelector('.dictionary-priority');
+        const deleteButton = node.querySelector('.dictionary-delete-button');
+        const detailsTable = node.querySelector('.dictionary-details-table');
+        const detailsToggleLink = node.querySelector('.dictionary-details-toggle-link');
+        const outdatedContainer = node.querySelector('.dictionary-outdated-notification');
+        const titleNode = node.querySelector('.dictionary-title');
+        const versionNode = node.querySelector('.dictionary-version');
+        const wildcardSupportedCheckbox = node.querySelector('.dictionary-prefix-wildcard-searches-supported');
+
+        const hasDetails = this._setupDetails(detailsTable);
 
-        node.querySelector('.dict-title').textContent = title;
-        node.querySelector('.dict-revision').textContent = `rev.${revision}`;
-        node.querySelector('.dict-prefix-wildcard-searches-supported').checked = !!prefixWildcardsSupported;
+        titleNode.textContent = title;
+        versionNode.textContent = `rev.${revision}`;
+        wildcardSupportedCheckbox.checked = !!prefixWildcardsSupported;
 
-        this._setupDetails(dictionaryInfo);
+        outdatedContainer.hidden = (version >= 3);
+        detailsToggleLink.hidden = !hasDetails;
 
-        this._enabledCheckbox.dataset.setting = ObjectPropertyAccessor.getPathString(['dictionaries', title, 'enabled']);
-        this._allowSecondarySearchesCheckbox.dataset.setting = ObjectPropertyAccessor.getPathString(['dictionaries', title, 'allowSecondarySearches']);
-        this._priorityInput.dataset.setting = ObjectPropertyAccessor.getPathString(['dictionaries', title, 'priority']);
+        enabledCheckbox.dataset.setting = ObjectPropertyAccessor.getPathString(['dictionaries', title, 'enabled']);
+        allowSecondarySearchesCheckbox.dataset.setting = ObjectPropertyAccessor.getPathString(['dictionaries', title, 'allowSecondarySearches']);
+        priorityInput.dataset.setting = ObjectPropertyAccessor.getPathString(['dictionaries', title, 'priority']);
 
-        this._eventListeners.addEventListener(this._deleteButton, 'click', this._onDeleteButtonClicked.bind(this), false);
-        this._eventListeners.addEventListener(this._detailsToggleLink, 'click', this._onDetailsToggleLinkClicked.bind(this), false);
-        this._eventListeners.addEventListener(this._priorityInput, 'settingChanged', this._onPriorityChanged.bind(this), false);
+        this._eventListeners.addEventListener(deleteButton, 'click', this._onDeleteButtonClicked.bind(this), false);
+        this._eventListeners.addEventListener(detailsToggleLink, 'click', this._onDetailsToggleLinkClicked.bind(this), false);
+        this._eventListeners.addEventListener(priorityInput, 'settingChanged', this._onPriorityChanged.bind(this), false);
     }
 
     cleanup() {
@@ -79,7 +83,7 @@ class DictionaryEntry {
     }
 
     setCounts(counts) {
-        const node = this._node.querySelector('.dict-counts');
+        const node = this._node.querySelector('.dictionary-counts');
         node.textContent = JSON.stringify({info: this._dictionaryInfo, counts}, null, 4);
         node.hidden = false;
     }
@@ -88,7 +92,7 @@ class DictionaryEntry {
 
     _onDeleteButtonClicked(e) {
         e.preventDefault();
-        this._dictionaryController.deleteDictionary(this._dictionaryTitle);
+        this._dictionaryController.deleteDictionary(this.dictionaryTitle);
     }
 
     _onDetailsToggleLinkClicked(e) {
@@ -101,7 +105,7 @@ class DictionaryEntry {
         this._node.style.order = `${-value}`;
     }
 
-    _setupDetails(dictionaryInfo) {
+    _setupDetails(detailsTable) {
         const targets = [
             ['Author', 'author'],
             ['URL', 'url'],
@@ -109,37 +113,24 @@ class DictionaryEntry {
             ['Attribution', 'attribution']
         ];
 
+        const dictionaryInfo = this._dictionaryInfo;
         const fragment = document.createDocumentFragment();
-        let count = 0;
+        let any = false;
         for (const [label, key] of targets) {
             const info = dictionaryInfo[key];
             if (typeof info !== 'string') { continue; }
 
-            const n1 = document.createElement('div');
-            n1.className = 'dict-details-entry';
-            n1.dataset.type = key;
-
-            const n2 = document.createElement('span');
-            n2.className = 'dict-details-entry-label';
-            n2.textContent = `${label}:`;
-            n1.appendChild(n2);
-
-            const n3 = document.createElement('span');
-            n3.className = 'dict-details-entry-info';
-            n3.textContent = info;
-            n1.appendChild(n3);
-
-            fragment.appendChild(n1);
+            const details = this._dictionaryController.instantiateTemplate('dictionary-details-entry');
+            details.dataset.type = key;
+            details.querySelector('.dictionary-details-entry-label').textContent = `${label}:`;
+            details.querySelector('.dictionary-details-entry-info').textContent = info;
+            fragment.appendChild(details);
 
-            ++count;
+            any = true;
         }
 
-        if (count > 0) {
-            this._detailsTable.appendChild(fragment);
-        } else {
-            this._detailsContainer.hidden = true;
-            this._detailsToggleLink.hidden = true;
-        }
+        detailsTable.appendChild(fragment);
+        return any;
     }
 }
 
@@ -152,7 +143,6 @@ class DictionaryController {
         this._databaseStateToken = null;
         this._checkingIntegrity = false;
         this._warningNode = null;
-        this._mainDictionarySelect = null;
         this._checkIntegrityButton = null;
         this._dictionaryEntryContainer = null;
         this._integrityExtraInfoContainer = null;
@@ -162,16 +152,15 @@ class DictionaryController {
     }
 
     async prepare() {
-        this._warningNode = document.querySelector('#dict-warning');
-        this._mainDictionarySelect = document.querySelector('#dict-main');
-        this._checkIntegrityButton = document.querySelector('#dict-check-integrity');
-        this._dictionaryEntryContainer = document.querySelector('#dict-groups');
-        this._integrityExtraInfoContainer = document.querySelector('#dict-groups-extra');
-        this._deleteDictionaryModal = this._modalController.getModal('dict-delete-modal');
+        this._warningNode = document.querySelector('#dictionary-warning');
+        this._checkIntegrityButton = document.querySelector('#dictionary-check-integrity');
+        this._dictionaryEntryContainer = document.querySelector('#dictionary-list');
+        this._integrityExtraInfoContainer = document.querySelector('#dictionary-list-extra');
+        this._deleteDictionaryModal = this._modalController.getModal('dictionary-confirm-delete');
 
         yomichan.on('databaseUpdated', this._onDatabaseUpdated.bind(this));
 
-        document.querySelector('#dict-delete-confirm').addEventListener('click', this._onDictionaryConfirmDelete.bind(this), false);
+        document.querySelector('#dictionary-confirm-delete-button').addEventListener('click', this._onDictionaryConfirmDelete.bind(this), false);
         this._checkIntegrityButton.addEventListener('click', this._onCheckIntegrityButtonClick.bind(this), false);
 
         await this._onDatabaseUpdated();
@@ -181,10 +170,14 @@ class DictionaryController {
         if (this._isDeleting) { return; }
         const modal = this._deleteDictionaryModal;
         modal.node.dataset.dictionaryTitle = dictionaryTitle;
-        modal.node.querySelector('#dict-remove-modal-dict-name').textContent = dictionaryTitle;
+        modal.node.querySelector('#dictionary-confirm-delete-name').textContent = dictionaryTitle;
         modal.setVisible(true);
     }
 
+    instantiateTemplate(name) {
+        return this._settingsController.instantiateTemplate(name);
+    }
+
     // Private
 
     async _onDatabaseUpdated() {
@@ -227,25 +220,26 @@ class DictionaryController {
     }
 
     _updateMainDictionarySelectOptions(dictionaries) {
-        const fragment = document.createDocumentFragment();
+        for (const select of document.querySelectorAll('[data-setting="general.mainDictionary"]')) {
+            const fragment = document.createDocumentFragment();
 
-        let option = document.createElement('option');
-        option.className = 'text-muted';
-        option.value = '';
-        option.textContent = 'Not selected';
-        fragment.appendChild(option);
-
-        for (const {title, sequenced} of dictionaries) {
-            if (!sequenced) { continue; }
-            option = document.createElement('option');
-            option.value = title;
-            option.textContent = title;
+            let option = document.createElement('option');
+            option.className = 'text-muted';
+            option.value = '';
+            option.textContent = 'Not selected';
             fragment.appendChild(option);
-        }
 
-        const select = this._mainDictionarySelect;
-        select.textContent = ''; // Empty
-        select.appendChild(fragment);
+            for (const {title, sequenced} of dictionaries) {
+                if (!sequenced) { continue; }
+                option = document.createElement('option');
+                option.value = title;
+                option.textContent = title;
+                fragment.appendChild(option);
+            }
+
+            select.textContent = ''; // Empty
+            select.appendChild(fragment);
+        }
     }
 
     async _checkIntegrity() {
@@ -294,12 +288,12 @@ class DictionaryController {
     }
 
     _createExtra(totalCounts, remainders, totalRemainder) {
-        const node = this._settingsController.instantiateTemplate('dict-extra');
+        const node = this.instantiateTemplate('dictionary-extra');
         this._integrityExtraInfoNode = node;
 
-        node.querySelector('.dict-total-count').textContent = `${totalRemainder} item${totalRemainder !== 1 ? 's' : ''}`;
+        node.querySelector('.dictionary-total-count').textContent = `${totalRemainder} item${totalRemainder !== 1 ? 's' : ''}`;
 
-        const n = node.querySelector('.dict-counts');
+        const n = node.querySelector('.dictionary-counts');
         n.textContent = JSON.stringify({counts: totalCounts, remainders}, null, 4);
         n.hidden = false;
 
@@ -318,7 +312,7 @@ class DictionaryController {
     }
 
     _createDictionaryEntry(dictionary) {
-        const node = this._settingsController.instantiateTemplate('dict');
+        const node = this.instantiateTemplate('dictionary');
         this._dictionaryEntryContainer.appendChild(node);
 
         const entry = new DictionaryEntry(this, node, dictionary);
@@ -334,7 +328,7 @@ class DictionaryController {
 
         const entry = this._dictionaryEntries[index];
         const node = entry.node;
-        const progress = node.querySelector('.progress');
+        const progress = node.querySelector('.progress-container');
         const progressBar = node.querySelector('.progress-bar');
         const prevention = this._settingsController.preventPageExit();
         try {
@@ -365,7 +359,7 @@ class DictionaryController {
 
     _setButtonsEnabled(value) {
         value = !value;
-        for (const node of document.querySelectorAll('.dictionary-modifying-input')) {
+        for (const node of document.querySelectorAll('.dictionary-database-mutating-input')) {
             node.disabled = value;
         }
     }
diff --git a/ext/bg/js/settings/dictionary-import-controller.js b/ext/bg/js/settings/dictionary-import-controller.js
index 2c954ef8..e0ee0ee3 100644
--- a/ext/bg/js/settings/dictionary-import-controller.js
+++ b/ext/bg/js/settings/dictionary-import-controller.js
@@ -51,17 +51,17 @@ class DictionaryImportController {
     }
 
     async prepare() {
-        this._purgeButton = document.querySelector('#dict-purge-button');
-        this._purgeConfirmButton = document.querySelector('#dict-purge-confirm');
-        this._importFileButton = document.querySelector('#dict-file-button');
-        this._importFileInput = document.querySelector('#dict-file');
-        this._purgeConfirmModal = this._modalController.getModal('dict-purge-modal');
-        this._errorContainer = document.querySelector('#dict-error');
-        this._spinner = document.querySelector('#dict-spinner');
-        this._progressContainer = document.querySelector('#dict-import-progress');
+        this._purgeButton = document.querySelector('#dictionary-delete-all-button');
+        this._purgeConfirmButton = document.querySelector('#dictionary-confirm-delete-all-button');
+        this._importFileButton = document.querySelector('#dictionary-import-file-button');
+        this._importFileInput = document.querySelector('#dictionary-import-file-input');
+        this._purgeConfirmModal = this._modalController.getModal('dictionary-confirm-delete-all');
+        this._errorContainer = document.querySelector('#dictionary-error');
+        this._spinner = document.querySelector('#dictionary-spinner');
+        this._progressContainer = document.querySelector('#dictionary-import-progress-container');
         this._progressBar = this._progressContainer.querySelector('.progress-bar');
-        this._purgeNotification = document.querySelector('#dict-purge');
-        this._importInfo = document.querySelector('#dict-import-info');
+        this._purgeNotification = document.querySelector('#dictionary-delete-all-status');
+        this._importInfo = document.querySelector('#dictionary-import-info');
 
         this._purgeButton.addEventListener('click', this._onPurgeButtonClick.bind(this), false);
         this._purgeConfirmButton.addEventListener('click', this._onPurgeConfirmButtonClick.bind(this), false);
@@ -301,7 +301,7 @@ class DictionaryImportController {
 
     _setButtonsEnabled(value) {
         value = !value;
-        for (const node of document.querySelectorAll('.dictionary-modifying-input')) {
+        for (const node of document.querySelectorAll('.dictionary-database-mutating-input')) {
             node.disabled = value;
         }
     }
diff --git a/ext/bg/settings.html b/ext/bg/settings.html
index 18e3e389..d513ab57 100644
--- a/ext/bg/settings.html
+++ b/ext/bg/settings.html
@@ -656,7 +656,7 @@
 
             <div class="ignore-form-changes">
                 <div>
-                    <img src="/mixed/img/spinner.gif" class="pull-right" id="dict-spinner" alt hidden>
+                    <img src="/mixed/img/spinner.gif" class="pull-right" id="dictionary-spinner" alt hidden>
                     <h3>Dictionaries</h3>
                 </div>
 
@@ -664,38 +664,38 @@
                     Yomichan can import and use a variety of dictionary formats. Unneeded dictionaries can be disabled.
                 </p>
 
-                <div class="form-group" id="dict-main-group">
-                    <label for="dict-main">Main dictionary for merged mode</label>
-                    <select class="form-control" id="dict-main" data-setting="general.mainDictionary"></select>
+                <div class="form-group" id="dictionary-main-group">
+                    <label for="dictionary-main">Main dictionary for merged mode</label>
+                    <select class="form-control" id="dictionary-main" data-setting="general.mainDictionary"></select>
                 </div>
 
-                <div class="text-danger" id="dict-purge" hidden>Dictionary data is being purged, please be patient...</div>
-                <div class="alert alert-warning" id="dict-warning" hidden>No dictionaries have been installed</div>
-                <div class="alert alert-danger" id="dict-error" hidden></div>
+                <div class="text-danger" id="dictionary-delete-all-status" hidden>Dictionary data is being purged, please be patient...</div>
+                <div class="alert alert-warning" id="dictionary-warning" hidden>No dictionaries have been installed</div>
+                <div class="alert alert-danger" id="dictionary-error" hidden></div>
 
-                <div id="dict-groups"></div>
-                <div id="dict-groups-extra"></div>
+                <div id="dictionary-list"></div>
+                <div id="dictionary-list-extra"></div>
 
-                <div id="dict-import-progress" hidden>
+                <div id="dictionary-import-progress-container" hidden>
                     Dictionary data is being imported, please be patient...
-                    <span id="dict-import-info" hidden></span>
+                    <span id="dictionary-import-info" hidden></span>
                     <div class="progress">
                         <div class="progress-bar progress-bar-striped" style="width: 0%"></div>
                     </div>
                 </div>
 
-                <div id="dict-importer">
+                <div>
                     <p class="help-block">
                         Select a dictionary to import for use below. Please visit the Yomichan homepage to
                         <a href="https://foosoft.net/projects/yomichan" target="_blank" rel="noopener">download free dictionaries</a>
                         for use with this extension and to learn about importing proprietary EPWING dictionaries.
                     </p>
                     <div>
-                        <button class="btn btn-primary dictionary-modifying-input" id="dict-file-button">Import Dictionary</button>
-                        <button class="btn btn-danger dictionary-modifying-input" id="dict-purge-button">Purge Database</button>
-                        <button class="btn btn-default dictionary-modifying-input" id="dict-check-integrity">Check integrity</button>
+                        <button class="btn btn-primary dictionary-database-mutating-input" id="dictionary-import-file-button">Import Dictionary</button>
+                        <button class="btn btn-danger dictionary-database-mutating-input" id="dictionary-delete-all-button">Purge Database</button>
+                        <button class="btn btn-default dictionary-database-mutating-input" id="dictionary-check-integrity">Check integrity</button>
                     </div>
-                    <div hidden><input type="file" id="dict-file" accept=".zip,application/zip" multiple></div>
+                    <div hidden><input type="file" id="dictionary-import-file-input" accept=".zip,application/zip" multiple></div>
                 </div>
 
                 <div>
@@ -710,7 +710,7 @@
                     </p>
                 </div>
 
-                <div class="modal fade" tabindex="-1" role="dialog" id="dict-purge-modal">
+                <div class="modal fade" tabindex="-1" role="dialog" id="dictionary-confirm-delete-all">
                     <div class="modal-dialog modal-dialog-centered">
                         <div class="modal-content">
                             <div class="modal-header">
@@ -722,13 +722,13 @@
                             </div>
                             <div class="modal-footer">
                                 <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
-                                <button type="button" class="btn btn-danger" id="dict-purge-confirm">Purge Database</button>
+                                <button type="button" class="btn btn-danger" id="dictionary-confirm-delete-all-button">Purge Database</button>
                             </div>
                         </div>
                     </div>
                 </div>
 
-                <div class="modal fade" tabindex="-1" role="dialog" id="dict-delete-modal">
+                <div class="modal fade" tabindex="-1" role="dialog" id="dictionary-confirm-delete">
                     <div class="modal-dialog modal-dialog-centered">
                         <div class="modal-content">
                             <div class="modal-header">
@@ -736,58 +736,63 @@
                                 <h4 class="modal-title">Confirm dictionary deletion</h4>
                             </div>
                             <div class="modal-body">
-                                Are you sure you want to delete the dictionary <em id="dict-remove-modal-dict-name"></em>?
+                                Are you sure you want to delete the dictionary <em id="dictionary-confirm-delete-name"></em>?
                                 This operation may take some time and the responsiveness of this browser tab may be reduced.
                             </div>
                             <div class="modal-footer">
                                 <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
-                                <button type="button" class="btn btn-danger dictionary-modifying-input" id="dict-delete-confirm">Delete Dictionary</button>
+                                <button type="button" class="btn btn-danger dictionary-database-mutating-input" id="dictionary-confirm-delete-button">Delete Dictionary</button>
                             </div>
                         </div>
                     </div>
                 </div>
 
-                <template id="dict-template"><div class="dict-group well well-sm">
-                    <h4><span class="text-muted glyphicon glyphicon-book"></span> <span class="dict-title"></span> <small class="dict-revision"></small></h4>
-                    <p class="text-warning dict-outdated" hidden>This dictionary is outdated and may not support new extension features; please import the latest version.</p>
+                <template id="dictionary-template"><div class="well well-sm">
+                    <h4><span class="text-muted glyphicon glyphicon-book"></span> <span class="dictionary-title"></span> <small class="dictionary-version"></small></h4>
+                    <p class="text-warning dictionary-outdated-notification" hidden>This dictionary is outdated and may not support new extension features; please import the latest version.</p>
 
                     <div class="checkbox">
-                        <label><input type="checkbox" class="dict-enabled"> Enable search</label>
+                        <label><input type="checkbox" class="dictionary-enabled"> Enable search</label>
                     </div>
                     <div class="checkbox options-advanced">
-                        <label><input type="checkbox" class="dict-allow-secondary-searches"> Allow secondary searches</label>
+                        <label><input type="checkbox" class="dictionary-allow-secondary-searches"> Allow secondary searches</label>
                     </div>
-                    <div class="checkbox dict-prefix-wildcard-searches-supported-container">
-                        <label><input type="checkbox" class="dict-prefix-wildcard-searches-supported" disabled> Prefix wildcard searches supported</label>
+                    <div class="checkbox">
+                        <label><input type="checkbox" class="dictionary-prefix-wildcard-searches-supported" disabled> Prefix wildcard searches supported</label>
                     </div>
                     <div class="form-group options-advanced">
-                        <label class="dict-result-priority-label">Result priority</label>
-                        <input type="number" class="form-control dict-priority">
+                        <label>Result priority</label>
+                        <input type="number" class="form-control dictionary-priority">
                     </div>
-                    <div class="dict-details-container">
-                        <a class="dict-details-toggle-link">Details...</a>
-                        <div class="dict-details" hidden><div class="dict-details-table"></div></div>
+                    <div class="dictionary-details-container">
+                        <a class="dictionary-details-toggle-link">Details...</a>
+                        <div class="dictionary-details" hidden><div class="dictionary-details-table"></div></div>
                     </div>
-                    <div class="dict-delete-table">
+                    <div class="dictionary-delete-table">
                         <div>
-                            <button class="btn btn-default dict-delete-button dictionary-modifying-input">Delete Dictionary</button>
+                            <button class="btn btn-default dictionary-delete-button dictionary-database-mutating-input">Delete Dictionary</button>
                         </div>
-                        <div>
-                            <div class="progress" hidden>
+                        <div class="progress-container" hidden>
+                            <div class="progress">
                                 <div class="progress-bar progress-bar-striped" style="width: 0%"></div>
                             </div>
                         </div>
                     </div>
-                    <pre class="debug dict-counts" hidden></pre>
+                    <pre class="debug dictionary-counts" hidden></pre>
+                </div></template>
+
+                <template id="dictionary-details-entry-template"><div class="dictionary-details-entry">
+                    <span class="dictionary-details-entry-label"></span>
+                    <span class="dictionary-details-entry-info"></span>
                 </div></template>
 
-                <template id="dict-extra-template"><div class="well well-sm">
-                    <h4><span class="text-muted glyphicon glyphicon-alert"></span> <span class="dict-title">Unassociated Data</span> <small class="dict-total-count"></small></h4>
+                <template id="dictionary-extra-template"><div class="well well-sm">
+                    <h4><span class="text-muted glyphicon glyphicon-alert"></span> <span class="dictionary-title">Unassociated Data</span> <small class="dictionary-total-count"></small></h4>
                     <p class="text-warning">
                         The database contains extra data which is not associated with any installed dictionary.
                         Purging the database can fix this issue.
                     </p>
-                    <pre class="debug dict-counts" hidden></pre>
+                    <pre class="debug dictionary-counts" hidden></pre>
                 </div></template>
             </div>
 
-- 
cgit v1.2.3