From 814cbc0ee3df8385a34db4a0a5b1afaaa9c475c7 Mon Sep 17 00:00:00 2001 From: Martynas Bagdonas Date: Fri, 7 May 2021 10:18:14 +0300 Subject: [PATCH 1/6] Introduce spell checker context menu and dictionaries managing --- chrome/content/zotero/xpcom/dictionaries.js | 248 ++++++++++++++++++ chrome/content/zotero/xpcom/editorInstance.js | 113 +++++++- chrome/content/zotero/xpcom/zotero.js | 1 + components/zotero-service.js | 1 + resource/config.js | 1 + 5 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 chrome/content/zotero/xpcom/dictionaries.js diff --git a/chrome/content/zotero/xpcom/dictionaries.js b/chrome/content/zotero/xpcom/dictionaries.js new file mode 100644 index 0000000000..8b96ae6f0d --- /dev/null +++ b/chrome/content/zotero/xpcom/dictionaries.js @@ -0,0 +1,248 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2021 Corporation for Digital Scholarship + Vienna, Virginia, USA + http://digitalscholar.org/ + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see . + + ***** END LICENSE BLOCK ***** +*/ + +Zotero.Dictionaries = new function () { + let _dictionaries = []; + let _spellChecker = Cc['@mozilla.org/spellchecker/engine;1'] + .getService(Ci.mozISpellCheckingEngine); + _spellChecker.QueryInterface(Ci.mozISpellCheckingEngine); + + Zotero.defineProperty(this, 'baseURL', { + get: () => { + let url = ZOTERO_CONFIG.DICTIONARIES_URL; + if (!url.endsWith('/')) { + url += '/'; + } + return url; + } + }); + + Zotero.defineProperty(this, 'dictionaries', { + get: () => { + return _dictionaries; + } + }); + + /** + * Load all dictionaries + * + * @return {Promise} + */ + this.init = async function () { + let dictionariesDir = OS.Path.join(Zotero.Profile.dir, 'dictionaries'); + if (!(await OS.File.exists(dictionariesDir))) { + return; + } + let iterator = new OS.File.DirectoryIterator(dictionariesDir); + try { + await iterator.forEach(async function (entry) { + if (entry.name.startsWith('.')) { + return; + } + try { + let dir = OS.Path.join(dictionariesDir, entry.name); + await _loadDictionary(dir); + } + catch (e) { + Zotero.logError(e); + } + }); + } + finally { + iterator.close(); + } + }; + + /** + * Get available dictionaries from server + * + * @return {Promise} + */ + this.fetchDictionariesList = async function () { + let url = this.baseURL + 'dictionaries.json'; + let req = await Zotero.HTTP.request('GET', url, { responseType: 'json' }); + return req.response; + }; + + /** + * Install the most popular dictionary for specified locale + * + * @param locale + * @return {Promise} + */ + this.installByLocale = async function (locale) { + let dictionaries = await this.fetchDictionariesList(); + let matched = dictionaries.filter(x => x.locale === locale); + if (!matched.length) { + matched = dictionaries.filter(x => x.locale === locale.split(/[-_]/)[0]); + } + if (!matched.length) { + return false; + } + matched.sort((a, b) => b.users - a.users); + await this.install(matched[0].id); + return true; + }; + + /** + * Remove all dictionaries targeting specific locale + * + * @param locale + * @return {Promise} + */ + this.removeByLocale = async function(locale) { + for (let dictionary of _dictionaries) { + if (dictionary.locales.includes(locale) + || dictionary.locales.some(x => x === locale.split(/[-_]/)[0])) { + await this.remove(dictionary.id); + } + } + }; + + /** + * Install dictionary by extension id, + * e.g., `en-NZ@dictionaries.addons.mozilla.org` + * + * @param {String} id - Dictionary extension id + * @return {Promise} + */ + this.install = async function (id) { + await this.remove(id); + let url = this.baseURL + id + '.xpi'; + let xpiPath = OS.Path.join(Zotero.getTempDirectory().path, id); + let dir = OS.Path.join(Zotero.Profile.dir, 'dictionaries', id); + let zipReader = Components.classes['@mozilla.org/libjar/zip-reader;1'] + .createInstance(Components.interfaces.nsIZipReader); + try { + await Zotero.File.download(url, xpiPath); + + zipReader.open(Zotero.File.pathToFile(xpiPath)); + zipReader.test(null); + + // Create directories + let entries = zipReader.findEntries('*/'); + while (entries.hasMore()) { + let entry = entries.getNext(); + let destPath = OS.Path.join(dir, entry); + await Zotero.File.createDirectoryIfMissingAsync(destPath, { from: Zotero.Profile.dir }); + } + + // Extract files + entries = zipReader.findEntries('*'); + while (entries.hasMore()) { + let entry = entries.getNext(); + if (entry.substr(-1) === '/') { + continue; + } + let destPath = OS.Path.join(dir, entry); + zipReader.extract(entry, Zotero.File.pathToFile(destPath)); + } + + zipReader.close(); + await OS.File.remove(xpiPath); + await _loadDictionary(dir); + } + catch (e) { + if (await OS.File.exists(xpiPath)) { + await OS.File.remove(xpiPath); + } + if (await OS.File.exists(dir)) { + await OS.File.removeDir(dir); + } + throw e; + } + }; + + /** + * Remove dictionary by extension id + * + * @param {String} id + * @return {Promise} + */ + this.remove = async function (id) { + let dictionaryIndex = _dictionaries.findIndex(x => x.id === id); + if (dictionaryIndex !== -1) { + let dictionary = _dictionaries[dictionaryIndex]; + try { + let manifestPath = OS.Path.join(dictionary.dir, 'manifest.json'); + let manifest = await Zotero.File.getContentsAsync(manifestPath); + manifest = JSON.parse(manifest); + for (let locale in manifest.dictionaries) { + let dicPath = manifest.dictionaries[locale]; + let affPath = OS.Path.join(dictionary.dir, dicPath.slice(0, -3) + 'aff'); + _spellChecker.removeDictionary(locale, Zotero.File.pathToFile(affPath)); + } + } + catch (e) { + Zotero.logError(e); + } + await OS.File.removeDir(dictionary.dir); + _dictionaries.splice(dictionaryIndex, 1); + } + }; + + /** + * Update all dictionaries + * + * @return {Promise} + */ + this.update = async function () { + let availableDictionaries = await this.fetchDictionariesList(); + for (let dictionary of _dictionaries) { + let availableDictionary = availableDictionaries.find(x => x.id === dictionary.id); + if (availableDictionary && availableDictionary.version > dictionary.version) { + await this.install(availableDictionary.id); + } + } + }; + + /** + * Load dictionary from specified dir + * + * @param {String} dir + * @return {Promise} + */ + async function _loadDictionary(dir) { + let manifestPath = OS.Path.join(dir, 'manifest.json'); + let manifest = await Zotero.File.getContentsAsync(manifestPath); + manifest = JSON.parse(manifest); + let id; + if (manifest.applications && manifest.applications.gecko) { + id = manifest.applications.gecko.id; + } + else { + id = manifest.browser_specific_settings.gecko.id; + } + let version = manifest.version; + let locales = []; + for (let locale in manifest.dictionaries) { + locales.push(locale); + let dicPath = manifest.dictionaries[locale]; + let affPath = OS.Path.join(dir, dicPath.slice(0, -3) + 'aff'); + _spellChecker.addDictionary(locale, Zotero.File.pathToFile(affPath)); + } + _dictionaries.push({ id, locales, version, dir }); + } +}; diff --git a/chrome/content/zotero/xpcom/editorInstance.js b/chrome/content/zotero/xpcom/editorInstance.js index 96ccd4a14d..8d7560bcd4 100644 --- a/chrome/content/zotero/xpcom/editorInstance.js +++ b/chrome/content/zotero/xpcom/editorInstance.js @@ -23,6 +23,8 @@ ***** END LICENSE BLOCK ***** */ +Components.utils.import("resource://gre/modules/InlineSpellChecker.jsm"); + // Note: TinyMCE is automatically doing some meaningless corrections to // note-editor produced HTML. Which might result to more // conflicts, especially in group libraries @@ -73,7 +75,8 @@ class EditorInstance { }); this._prefObserverIDs = [ Zotero.Prefs.registerObserver('note.fontSize', this._handleFontChange), - Zotero.Prefs.registerObserver('note.fontFamily', this._handleFontChange) + Zotero.Prefs.registerObserver('note.fontFamily', this._handleFontChange), + Zotero.Prefs.registerObserver('layout.spellcheckDefault', this._handleSpellCheckChange, true) ]; // Run Cut/Copy/Paste with chrome privileges @@ -209,6 +212,20 @@ class EditorInstance { _handleFontChange = () => { this._postMessage({ action: 'updateFont', font: this._getFont() }); } + + _handleSpellCheckChange = () => { + try { + let spellChecker = this._getSpellChecker(); + let value = Zotero.Prefs.get('layout.spellcheckDefault', true); + if (!value && spellChecker.enabled + || value && !spellChecker.enabled) { + spellChecker.toggleEnabled(); + } + } + catch (e) { + Zotero.logError(e); + } + } _showInLibrary(ids) { if (!Array.isArray(ids)) { @@ -788,9 +805,93 @@ class EditorInstance { } appendItems(this._popup, itemGroups); + + // TODO: Localize + + // Spell checker + let spellChecker = this._getSpellChecker(); + + // Separator + var separator = this._popup.ownerDocument.createElement('menuseparator'); + this._popup.appendChild(separator); + // Check Spelling + var menuitem = this._popup.ownerDocument.createElement('menuitem'); + menuitem.setAttribute('label', 'Check Spelling'); + menuitem.setAttribute('checked', spellChecker.enabled); + menuitem.addEventListener('command', () => { + // Possible values: 0 - off, 1 - only multi-line, 2 - multi and single line input boxes + Zotero.Prefs.set('layout.spellcheckDefault', spellChecker.enabled ? 0 : 1, true); + }); + this._popup.append(menuitem); + + if (spellChecker.enabled) { + // Languages menu + var menu = this._popup.ownerDocument.createElement('menu'); + menu.setAttribute('label', 'Languages'); + this._popup.append(menu); + // Languages menu popup + var menupopup = this._popup.ownerDocument.createElement('menupopup'); + menu.append(menupopup); + + spellChecker.addDictionaryListToMenu(menupopup, null); + + for (var menuitem of menupopup.children) { + // 'spell-check-dictionary-en-US' + let locale = menuitem.id.slice(23); + let label = Zotero.Locale.availableLocales[locale]; + if (!label) { + for(let key in Zotero.Locale.availableLocales) { + if (key.split('-')[0] === locale) { + label = Zotero.Locale.availableLocales[key]; + } + } + } + + if (label) { + menuitem.setAttribute('label', label); + } + } + + // Separator + var separator = this._popup.ownerDocument.createElement('menuseparator'); + menupopup.appendChild(separator); + // Add Dictionaries + var menuitem = this._popup.ownerDocument.createElement('menuitem'); + menuitem.setAttribute('label', 'Add Dictionaries...'); + menuitem.addEventListener('command', () => { + Zotero.Utilities.Internal.openPreferences('zotero-prefpane-advanced'); + }); + menupopup.append(menuitem); + + let selection = this._iframeWindow.getSelection(); + if (selection) { + spellChecker.initFromEvent( + selection.anchorNode, + selection.anchorOffset + ); + } + + let firstElementChild = this._popup.firstElementChild; + let suggestionCount = spellChecker.addSuggestionsToMenu(this._popup, firstElementChild, 5); + if (suggestionCount) { + let separator = this._popup.ownerDocument.createElement('menuseparator'); + this._popup.insertBefore(separator, firstElementChild); + } + } + this._popup.openPopupAtScreen(x, y, true); } + _getSpellChecker() { + let spellChecker = new InlineSpellChecker(); + let editingSession = this._iframeWindow + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEditingSession); + spellChecker.init(editingSession.getEditorForWindow(this._iframeWindow)); + return spellChecker; + } + async _ensureNoteCreated() { if (!this._item.id) { return this._item.saveTx(); @@ -861,6 +962,16 @@ class EditorInstance { Zotero.crash(true); throw e; } + + // Reset spell checker as ProseMirror DOM modifications are + // often ignored otherwise + try { + let spellChecker = this._getSpellChecker(); + spellChecker.toggleEnabled(); + spellChecker.toggleEnabled(); + } catch(e) { + Zotero.logError(e); + } } /** diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js index 07ff35f031..1eb9b82076 100644 --- a/chrome/content/zotero/xpcom/zotero.js +++ b/chrome/content/zotero/xpcom/zotero.js @@ -725,6 +725,7 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js"); yield Zotero.Relations.init(); yield Zotero.Retractions.init(); yield Zotero.NoteBackups.init(); + yield Zotero.Dictionaries.init(); // Migrate fields from Extra that can be moved to item fields after a schema update yield Zotero.Schema.migrateExtraFields(); diff --git a/components/zotero-service.js b/components/zotero-service.js index f23545071f..b33060c94c 100644 --- a/components/zotero-service.js +++ b/components/zotero-service.js @@ -95,6 +95,7 @@ const xpcomFilesLocal = [ 'data/searches', 'data/tags', 'db', + 'dictionaries', 'duplicates', 'editorInstance', 'feedReader', diff --git a/resource/config.js b/resource/config.js index 0c3bb02272..4a480e3a40 100644 --- a/resource/config.js +++ b/resource/config.js @@ -28,6 +28,7 @@ var ZOTERO_CONFIG = { CREDITS_URL: 'https://www.zotero.org/support/credits_and_acknowledgments', LICENSING_URL: 'https://www.zotero.org/support/licensing', GET_INVOLVED_URL: 'https://www.zotero.org/getinvolved', + DICTIONARIES_URL: 'https://download.zotero.org/dictionaries/', }; if (typeof process === 'object' && process + '' === '[object process]'){ From 1e2ad01f999abb0b2991a4050c812ce73f2f2055 Mon Sep 17 00:00:00 2001 From: Dan Stillman Date: Mon, 21 Jun 2021 04:51:39 -0400 Subject: [PATCH 2/6] Zotero.File.download(): Throw error on non-200 response --- chrome/content/zotero/xpcom/file.js | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/chrome/content/zotero/xpcom/file.js b/chrome/content/zotero/xpcom/file.js index 3a97009911..aef260600d 100644 --- a/chrome/content/zotero/xpcom/file.js +++ b/chrome/content/zotero/xpcom/file.js @@ -448,14 +448,34 @@ Zotero.File = new function(){ this.download = Zotero.Promise.coroutine(function* (uri, path) { - Zotero.debug("Saving " + (uri.spec ? uri.spec : uri) - + " to " + (path.pathQueryRef ? path.pathQueryRef : path)); + var uriStr = uri.spec || uri; + + Zotero.debug(`Saving ${uriStr} to ${path.pathQueryRef || path}`); + + if (Zotero.HTTP.browserIsOffline()) { + let msg = `Download failed: ${Zotero.appName} is currently offline`; + Zotero.debug(msg, 2); + throw new Error(msg); + } var deferred = Zotero.Promise.defer(); NetUtil.asyncFetch(uri, function (is, status, request) { if (!Components.isSuccessCode(status)) { Zotero.logError(status); - deferred.reject(new Error("Download failed with status " + status)); + let msg = Zotero.getString('sync.error.checkConnection'); + switch (status) { + case 2152398878: + // TODO: Localize + msg = "Server not found. Check your internet connection." + break; + } + deferred.reject(new Error(msg)); + return; + } + if (request.responseStatus != 200) { + let msg = `Download failed with response code ${request.responseStatus}`; + Zotero.logError(msg); + deferred.reject(new Error(msg)); return; } deferred.resolve(is); From 7f2296b1fbfaa4669bdabcf9d055425b07987133 Mon Sep 17 00:00:00 2001 From: Dan Stillman Date: Mon, 21 Jun 2021 04:52:29 -0400 Subject: [PATCH 3/6] Tweak a couple error messages for HTTP.BrowserOfflineException --- chrome/content/zotero/xpcom/http.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chrome/content/zotero/xpcom/http.js b/chrome/content/zotero/xpcom/http.js index 2bbff8917f..4a248f931f 100644 --- a/chrome/content/zotero/xpcom/http.js +++ b/chrome/content/zotero/xpcom/http.js @@ -75,7 +75,7 @@ Zotero.HTTP = new function() { * @constructor */ this.BrowserOfflineException = function() { - this.message = "XMLHttpRequest could not complete because the browser is offline"; + this.message = `Request could not be completed because ${Zotero.appName} is offline`; this.stack = new Error().stack; }; this.BrowserOfflineException.prototype = Object.create(Error.prototype); @@ -259,7 +259,7 @@ Zotero.HTTP = new function() { } if (url.startsWith('http') && this.browserIsOffline()) { - Zotero.debug("HTTP " + method + " " + dispURL + " failed: Browser is offline"); + Zotero.debug(`HTTP ${method} ${dispURL} failed: ${Zotero.appName} is offline`); throw new this.BrowserOfflineException(); } From 9a7016ad6461ac1d84eac99bd2e3cd92a4475fb0 Mon Sep 17 00:00:00 2001 From: Dan Stillman Date: Tue, 22 Jun 2021 20:58:16 -0400 Subject: [PATCH 4/6] Spell checker improvements - Add/Remove Dictionaries window - Better account for the (unlikely) possibility that a dictionary could be replaced by another more popular dictionary provided by a different extension id (tested) - Better account for the (very unlikely) possibility that an extension could bundle multiple dictionaries (untested) - Use toolkit version comparator for proper extension version comparisons - Localize strings - Add tests for updating --- chrome/content/zotero/dictionaryManager.js | 193 ++++++++++++++++++ chrome/content/zotero/dictionaryManager.xul | 38 ++++ chrome/content/zotero/xpcom/dictionaries.js | 177 ++++++++++------ chrome/content/zotero/xpcom/editorInstance.js | 26 +-- chrome/content/zotero/xpcom/locale.js | 4 +- chrome/locale/en-US/zotero/zotero.properties | 9 + scss/_zotero-react-client.scss | 1 + scss/components/_dictionaryManager.scss | 27 +++ test/tests/dictionariesTest.js | 193 ++++++++++++++++++ 9 files changed, 583 insertions(+), 85 deletions(-) create mode 100644 chrome/content/zotero/dictionaryManager.js create mode 100644 chrome/content/zotero/dictionaryManager.xul create mode 100644 scss/components/_dictionaryManager.scss create mode 100644 test/tests/dictionariesTest.js diff --git a/chrome/content/zotero/dictionaryManager.js b/chrome/content/zotero/dictionaryManager.js new file mode 100644 index 0000000000..d85630d740 --- /dev/null +++ b/chrome/content/zotero/dictionaryManager.js @@ -0,0 +1,193 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2021 Corporation for Digital Scholarship + Vienna, Virginia, USA + https://digitalscholar.org + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see . + + ***** END LICENSE BLOCK ***** +*/ + +"use strict"; + +// eslint-disable-next-line camelcase, no-unused-vars +var Zotero_Dictionary_Manager = new function () { + const HTML_NS = 'http://www.w3.org/1999/xhtml'; + + var installed; + var updateMap; + + this.init = async function () { + document.title = Zotero.getString('spellCheck.dictionaryManager.title'); + + installed = new Set(Zotero.Dictionaries.dictionaries.map(d => d.id)); + var installedLocales = new Set(Zotero.Dictionaries.dictionaries.map(d => d.locale)); + var availableDictionaries = await Zotero.Dictionaries.fetchDictionariesList(); + var availableUpdates = await Zotero.Dictionaries.getAvailableUpdates(availableDictionaries); + updateMap = new Map(availableUpdates.map(x => [x.old.id, x.new.id])); + + var { InlineSpellChecker } = ChromeUtils.import("resource://gre/modules/InlineSpellChecker.jsm", {}); + var isc = new InlineSpellChecker(); + + // Start with installed dictionaries + var list = []; + for (let d of Zotero.Dictionaries.dictionaries) { + let name = Zotero.Dictionaries.getBestDictionaryName(d.locale, isc); + list.push(Object.assign({}, d, { name })); + } + // Add remote dictionaries not in the list + for (let d of availableDictionaries) { + if (!installed.has(d.id) && !installedLocales.has(d.locale)) { + list.push(d); + } + } + var positionMap = new Map(availableDictionaries.map((d, i) => [d.locale, i + 1])); + list.sort((a, b) => { + // If both locales are in original list, use the original sort order + let posA = positionMap.get(a.locale); + let posB = positionMap.get(b.locale); + if (posA && posB) { + return posA - posB; + } + // Otherwise compare the locale codes + return Zotero.localeCompare(a.locale, b.locale); + }); + + // Build list + var listbox = document.getElementById('dictionaries'); + for (let d of list) { + let name = d.name; + let li = document.createElement('richlistitem'); + let div = document.createElementNS(HTML_NS, 'div'); + + let checkbox = document.createElementNS(HTML_NS, 'input'); + checkbox.type = 'checkbox'; + checkbox.id = d.locale; + // Store properties on element + // .id will be the current id for installed dictionaries and otherwise the remote id + checkbox.dataset.dictId = d.id; + checkbox.dataset.dictLocale = d.locale; + checkbox.dataset.dictName = d.name; + // en-US is always checked and disabled + checkbox.checked = d.locale == 'en-US' || installed.has(d.id); + if (d.locale == 'en-US') { + checkbox.disabled = true; + } + checkbox.setAttribute('tabindex', -1); + + let label = document.createElementNS(HTML_NS, 'label'); + label.setAttribute('for', d.locale); + // Add " (update available)" + if (updateMap.has(d.id)) { + name = Zotero.getString('spellCheck.dictionaryManager.updateAvailable', name); + } + label.textContent = name; + // Don't toggle checkbox for single-click on label + label.onclick = (event) => { + if (event.detail == 1) { + event.preventDefault(); + } + }; + + div.appendChild(checkbox); + div.appendChild(label); + li.appendChild(div); + listbox.appendChild(li); + } + listbox.selectedIndex = 0; + }; + + this.handleAccept = async function () { + // Download selected dictionaries if updated or not currently installed + var elems = document.querySelectorAll('input[type=checkbox]'); + var toRemove = []; + var toDownload = []; + for (let elem of elems) { + if (elem.dataset.dictLocale == 'en-US') { + continue; + } + + let id = elem.dataset.dictId; + if (!elem.checked) { + if (installed.has(id)) { + toRemove.push(id); + } + continue; + } + + if (updateMap.has(id)) { + // If id is changing, delete the old one first + toRemove.push(id); + toDownload.push({ id: updateMap.get(id), name: elem.dataset.dictName }); + } + else if (!installed.has(id)) { + toDownload.push({ id, name: elem.dataset.dictName }); + } + } + if (toRemove.length) { + for (let id of toRemove) { + await Zotero.Dictionaries.remove(id); + } + } + if (toDownload.length) { + for (let { id, name } of toDownload) { + _updateStatus(Zotero.getString('general.downloading.quoted', name)); + try { + await Zotero.Dictionaries.install(id); + } + catch (e) { + Zotero.logError(e); + Zotero.alert( + null, + Zotero.getString('general.error'), + Zotero.getString('spellCheck.dictionaryManager.error.unableToInstall', name) + + "\n\n" + e.message + ); + return; + } + finally { + _updateStatus(); + } + } + } + window.close(); + }; + + function _updateStatus(msg) { + var elem = document.getElementById('status'); + elem.textContent = msg + // Use non-breaking space to maintain height when empty + || '\xA0'; + } +}; + +window.addEventListener('keypress', function (event) { + // Toggle checkbox on spacebar + if (event.key == ' ') { + if (event.target.localName == 'richlistbox') { + let elem = event.target.selectedItem.querySelector('input[type=checkbox]'); + if (!elem.disabled) { + elem.checked = !elem.checked; + } + } + } + + if (event.key == 'Enter') { + document.querySelector('button[dlgtype="accept"]').click(); + } +}); diff --git a/chrome/content/zotero/dictionaryManager.xul b/chrome/content/zotero/dictionaryManager.xul new file mode 100644 index 0000000000..482b3102ad --- /dev/null +++ b/chrome/content/zotero/dictionaryManager.xul @@ -0,0 +1,38 @@ + + + + + + + + + +