mirror of
https://github.com/zotero/zotero.git
synced 2026-10-05 02:43:38 +00:00
commit
9327819895
15 changed files with 891 additions and 9 deletions
193
chrome/content/zotero/dictionaryManager.js
Normal file
193
chrome/content/zotero/dictionaryManager.js
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***** 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();
|
||||
}
|
||||
});
|
||||
38
chrome/content/zotero/dictionaryManager.xul
Normal file
38
chrome/content/zotero/dictionaryManager.xul
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0"?>
|
||||
|
||||
<?xml-stylesheet href="chrome://global/skin/"?>
|
||||
<?xml-stylesheet href="chrome://zotero-platform/content/zotero-react-client.css"?>
|
||||
|
||||
<!DOCTYPE window SYSTEM "chrome://zotero/locale/zotero.dtd">
|
||||
|
||||
<window
|
||||
id="dictionary-manager"
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
xmlns:html="http://www.w3.org/1999/xhtml"
|
||||
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
onload="Zotero_Dictionary_Manager.init()"
|
||||
windowtype="zotero:dictionaries"
|
||||
width="445"
|
||||
height="400">
|
||||
|
||||
<script src="chrome://global/content/globalOverlay.js"/>
|
||||
<script src="include.js"/>
|
||||
<script src="commonDialog.js"/>
|
||||
<script src="dictionaryManager.js"/>
|
||||
|
||||
<vbox id="DictionaryManager" flex="1">
|
||||
<richlistbox id="dictionaries" flex="1" tabindex="0"/>
|
||||
|
||||
<html:div id="status"> </html:div>
|
||||
|
||||
<hbox class="dialog-button-box">
|
||||
<button dlgtype="cancel" label="&zotero.general.cancel;" oncommand="window.close()"/>
|
||||
<button dlgtype="accept" default="true" label="&zotero.general.ok;"
|
||||
oncommand="Zotero_Dictionary_Manager.handleAccept()"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
|
||||
<keyset>
|
||||
<key id="key_close" key="W" modifiers="accel" oncommand="window.close()"/>
|
||||
</keyset>
|
||||
</window>
|
||||
289
chrome/content/zotero/xpcom/dictionaries.js
Normal file
289
chrome/content/zotero/xpcom/dictionaries.js
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
/*
|
||||
***** 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***** 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;
|
||||
}
|
||||
});
|
||||
|
||||
// Note: Doesn't include bundled en-US
|
||||
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 _loadDirectory(dir);
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
finally {
|
||||
iterator.close();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get available dictionaries from server
|
||||
*
|
||||
* @return {Promise<Object>}
|
||||
*/
|
||||
this.fetchDictionariesList = async function () {
|
||||
let url = this.baseURL + 'dictionaries.json';
|
||||
let req = await Zotero.HTTP.request('GET', url, { responseType: 'json' });
|
||||
return req.response;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (id == '@unitedstatesenglishdictionary') {
|
||||
throw new Error("en-US dictionary is bundled");
|
||||
}
|
||||
await this.remove(id);
|
||||
Zotero.debug("Installing dictionaries from " + 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 _loadDirectory(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 dictionaries by extension id
|
||||
*
|
||||
* @param {String} id
|
||||
* @return {Promise}
|
||||
*/
|
||||
this.remove = async function (id) {
|
||||
Zotero.debug("Removing dictionaries from " + id);
|
||||
var dictionary = _dictionaries.find(x => x.id === id);
|
||||
if (!dictionary) {
|
||||
return;
|
||||
}
|
||||
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');
|
||||
Zotero.debug(`Removing ${locale} dictionary`);
|
||||
_spellChecker.removeDictionary(locale, Zotero.File.pathToFile(affPath));
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
}
|
||||
await OS.File.removeDir(dictionary.dir);
|
||||
// Technically there can be more than one dictionary provided by the same extension id,
|
||||
// so remove all that match
|
||||
_dictionaries = _dictionaries.filter(x => x.id != id);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object[]} [dictionaries] - Dictionary list from fetchDictionariesList(); fetched
|
||||
* automatically if not provided
|
||||
* @return {Object[]} - Array of objects with 'old' and 'new'
|
||||
*/
|
||||
this.getAvailableUpdates = async function (dictionaries) {
|
||||
var updates = [];
|
||||
let availableDictionaries = dictionaries || await this.fetchDictionariesList();
|
||||
for (let dictionary of _dictionaries) {
|
||||
let availableDictionary = availableDictionaries.find((x) => {
|
||||
return x.id === dictionary.id || x.locale == dictionary.locale;
|
||||
});
|
||||
if (!availableDictionary) continue;
|
||||
// If same id, check if version is higher
|
||||
if (availableDictionary.id == dictionary.id) {
|
||||
if (Services.vc.compare(dictionary.version, availableDictionary.version) < 0) {
|
||||
updates.push({ old: dictionary, new: availableDictionary });
|
||||
}
|
||||
}
|
||||
// If different id for same locale, always offer as an update
|
||||
else {
|
||||
updates.push({ old: dictionary, new: availableDictionary });
|
||||
}
|
||||
}
|
||||
if (updates.length) {
|
||||
Zotero.debug("Available dictionary updates:");
|
||||
Zotero.debug(updates);
|
||||
}
|
||||
else {
|
||||
Zotero.debug("No dictionary updates found");
|
||||
}
|
||||
return updates;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the best display name for a dictionary
|
||||
*
|
||||
* For known locales, this will be the native name in the target locale. If a native name isn't
|
||||
* available and inlineSpellChecker is provided, an English name will be provided if available.
|
||||
*
|
||||
* @param {String} locale
|
||||
* @param {InlineSpellChecker} [inlineSpellChecker] - An instance of InlineSpellChecker from
|
||||
* InlineSpellChecker.jsm
|
||||
* @return {String} - The best available name, or the locale code if unavailable
|
||||
*/
|
||||
this.getBestDictionaryName = function (locale, inlineSpellChecker) {
|
||||
var name = Zotero.Locale.availableLocales[locale];
|
||||
if (!name) {
|
||||
for (let key in Zotero.Locale.availableLocales) {
|
||||
if (key.split('-')[0] === locale) {
|
||||
name = Zotero.Locale.availableLocales[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!name && inlineSpellChecker) {
|
||||
name = inlineSpellChecker.getDictionaryDisplayName(locale)
|
||||
}
|
||||
return name || name;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update dictionaries
|
||||
*
|
||||
* @return {Promise<Integer>} - Number of updated dictionaries
|
||||
*/
|
||||
this.update = async function () {
|
||||
var updates = await Zotero.Dictionaries.getAvailableUpdates();
|
||||
var updated = 0;
|
||||
for (let update of updates) {
|
||||
try {
|
||||
await this.remove(update.old.id);
|
||||
await this.install(update.new.id);
|
||||
updated++;
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
};
|
||||
|
||||
/**
|
||||
* Load dictionary from specified dir
|
||||
*
|
||||
* @param {String} dir
|
||||
* @return {Promise}
|
||||
*/
|
||||
async function _loadDirectory(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');
|
||||
Zotero.debug(`Adding ${locale} dictionary`);
|
||||
_spellChecker.addDictionary(locale, Zotero.File.pathToFile(affPath));
|
||||
_dictionaries.push({ id, locale, version, dir });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -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)) {
|
||||
|
|
@ -644,7 +661,9 @@ class EditorInstance {
|
|||
}
|
||||
case 'openContextMenu': {
|
||||
let { x, y, pos, itemGroups } = message;
|
||||
this._openPopup(x, y, pos, itemGroups);
|
||||
// If `contenteditable` area wasn't focused before, the spell checker
|
||||
// might not be fully initialized on right-click
|
||||
setTimeout(() => this._openPopup(x, y, pos, itemGroups), 50);
|
||||
return;
|
||||
}
|
||||
case 'return': {
|
||||
|
|
@ -788,9 +807,87 @@ class EditorInstance {
|
|||
}
|
||||
|
||||
appendItems(this._popup, itemGroups);
|
||||
|
||||
// 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', Zotero.getString('spellCheck.checkSpelling'));
|
||||
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', Zotero.getString('general.languages'));
|
||||
this._popup.append(menu);
|
||||
// Languages menu popup
|
||||
var menupopup = this._popup.ownerDocument.createElement('menupopup');
|
||||
menu.append(menupopup);
|
||||
|
||||
spellChecker.addDictionaryListToMenu(menupopup, null);
|
||||
|
||||
// The menu is prepopulated with names from InlineSpellChecker::getDictionaryDisplayName(),
|
||||
// which will be in English, so swap in native locale names where we have them
|
||||
for (var menuitem of menupopup.children) {
|
||||
// 'spell-check-dictionary-en-US'
|
||||
let locale = menuitem.id.slice(23);
|
||||
let label = Zotero.Dictionaries.getBestDictionaryName(locale);
|
||||
if (label && label != locale) {
|
||||
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', Zotero.getString('spellCheck.addRemoveDictionaries'));
|
||||
menuitem.addEventListener('command', () => {
|
||||
Services.ww.openWindow(null, "chrome://zotero/content/dictionaryManager.xul",
|
||||
"dictionary-manager", "chrome,centerscreen", {});
|
||||
|
||||
});
|
||||
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 +958,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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
Zotero.Locale = {
|
||||
/**
|
||||
* Keep this up to date with chrome.manifest
|
||||
* Keep this up to date with chrome.manifest and zotero-build/dictionaries/build-dictionaries
|
||||
*
|
||||
* Names from https://addons.mozilla.org/en-US/firefox/language-tools/
|
||||
*/
|
||||
availableLocales: Object.freeze({
|
||||
'ar': 'عربي',
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ general.checkForUpdates = Check for Updates
|
|||
general.actionCannotBeUndone = This action cannot be undone.
|
||||
general.install = Install
|
||||
general.updateAvailable = Update Available
|
||||
general.downloading = Downloading %S…
|
||||
general.downloading.quoted = Downloading “%S”…
|
||||
general.noUpdatesFound = No Updates Found
|
||||
general.isUpToDate = %S is up to date.
|
||||
general.upgrade = Upgrade
|
||||
|
|
@ -79,6 +81,7 @@ general.nMegabytes = %S MB
|
|||
general.item = Item
|
||||
general.pdf = PDF
|
||||
general.back = Back
|
||||
general.languages = Languages
|
||||
|
||||
general.yellow = Yellow
|
||||
general.red = Red
|
||||
|
|
@ -1342,3 +1345,9 @@ pdfReader.nextPage = Next Page
|
|||
pdfReader.previousPage = Previous Page
|
||||
pdfReader.page = Page
|
||||
pdfReader.readOnly = Read-only
|
||||
|
||||
spellCheck.checkSpelling = Check Spelling
|
||||
spellCheck.addRemoveDictionaries = Add/Remove Dictionaries…
|
||||
spellCheck.dictionaryManager.title = Add/Remove Dictionaries
|
||||
spellCheck.dictionaryManager.updateAvailable = %S (update available)
|
||||
spellCheck.dictionaryManager.error.unableToInstall = Unable to install “%S”:
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ const xpcomFilesLocal = [
|
|||
'data/searches',
|
||||
'data/tags',
|
||||
'db',
|
||||
'dictionaries',
|
||||
'duplicates',
|
||||
'editorInstance',
|
||||
'feedReader',
|
||||
|
|
|
|||
|
|
@ -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]'){
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
@import "components/autosuggest";
|
||||
@import "components/button";
|
||||
@import "components/createParent";
|
||||
@import "components/dictionaryManager";
|
||||
@import "components/editable";
|
||||
@import "components/exportOptions";
|
||||
@import "components/icons";
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ $space-thumb: 42px;
|
|||
// Typography
|
||||
// --------------------------------------------------
|
||||
|
||||
$font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
$font-family-sans-serif: Lucida Grande, Lucida Sans Unicode, Lucida Sans, Geneva, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
$font-family-base: $font-family-sans-serif;
|
||||
|
||||
$font-size-base: 13px;
|
||||
|
|
|
|||
27
scss/components/_dictionaryManager.scss
Normal file
27
scss/components/_dictionaryManager.scss
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
#DictionaryManager {
|
||||
padding: 15px;
|
||||
|
||||
richlistitem {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
input {
|
||||
// Select rows, not checkboxes
|
||||
-moz-user-focus: ignore;
|
||||
}
|
||||
|
||||
#status {
|
||||
margin: 9px 0 0 5px;
|
||||
font-size: 10px;
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
.dialog-button-box {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
193
test/tests/dictionariesTest.js
Normal file
193
test/tests/dictionariesTest.js
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
"use strict";
|
||||
|
||||
var sandbox = sinon.createSandbox();
|
||||
|
||||
describe("Dictionaries", function () {
|
||||
var win;
|
||||
var enUKXPIOld, frFRv1XPI, unKNXPI, enUKXPINew, frFRv2XPI;
|
||||
|
||||
async function makeFakeDictionary({ id, locale, version }) {
|
||||
var dir = await getTempDirectory();
|
||||
var extDir = OS.Path.join(dir, 'sub');
|
||||
var dictDir = OS.Path.join(extDir, 'dictionaries');
|
||||
await OS.File.makeDir(dictDir, { from: dir });
|
||||
var manifest = {
|
||||
dictionaries: {
|
||||
[locale]: `dictionaries/${locale}.dic`,
|
||||
},
|
||||
version,
|
||||
applications: {
|
||||
gecko: {
|
||||
id
|
||||
}
|
||||
},
|
||||
name,
|
||||
manifest_version: 2
|
||||
};
|
||||
await Zotero.File.putContentsAsync(
|
||||
OS.Path.join(extDir, 'manifest.json'),
|
||||
JSON.stringify(manifest)
|
||||
);
|
||||
await Zotero.File.putContentsAsync(
|
||||
OS.Path.join(dictDir, locale + '.dic'),
|
||||
"1\n0/nm"
|
||||
);
|
||||
var path = OS.Path.join(dir, id + '.xpi');
|
||||
await Zotero.File.zipDirectory(extDir, path);
|
||||
return path;
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
// Make fake installed dictionaries
|
||||
enUKXPIOld = await makeFakeDictionary({
|
||||
id: '@fake-en-UK-dictionary',
|
||||
locale: 'en-UK',
|
||||
version: 5,
|
||||
name: "Fake English UK Dictionary"
|
||||
});
|
||||
frFRv1XPI = await makeFakeDictionary({
|
||||
id: '@fake-fr-FR-dictionary',
|
||||
locale: 'fr-FR',
|
||||
version: 1,
|
||||
name: "Fake French Dictionary"
|
||||
});
|
||||
unKNXPI = await makeFakeDictionary({
|
||||
id: '@fake-unknown-dictionary',
|
||||
locale: 'xx-UN',
|
||||
version: 5,
|
||||
name: "Fake Unknown Dictionary"
|
||||
});
|
||||
// Make fake updated dictionaries
|
||||
enUKXPINew = await makeFakeDictionary({
|
||||
id: '@another-fake-en-UK-dictionary',
|
||||
locale: 'en-UK',
|
||||
version: 1,
|
||||
name: "Another Fake English UK Dictionary"
|
||||
});
|
||||
frFRv2XPI = await makeFakeDictionary({
|
||||
id: '@fake-fr-FR-dictionary',
|
||||
locale: 'fr-FR',
|
||||
version: 2,
|
||||
name: "Fake French Dictionary"
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
for (let id of Zotero.Dictionaries.dictionaries.map(x => x.id)) {
|
||||
await Zotero.Dictionaries.remove(id);
|
||||
}
|
||||
|
||||
sandbox.stub(Zotero.File, 'download').callsFake(async (url, downloadPath) => {
|
||||
if (url.includes('en-UK')) {
|
||||
return OS.File.copy(enUKXPIOld, downloadPath);
|
||||
}
|
||||
if (url.includes('fr-FR')) {
|
||||
return OS.File.copy(frFRv1XPI, downloadPath);
|
||||
}
|
||||
if (url.includes('xx-UN')) {
|
||||
return OS.File.copy(unKNXPI, downloadPath);
|
||||
}
|
||||
throw new Error("Unexpected URL " + url);
|
||||
});
|
||||
await Zotero.Dictionaries.install('@fake-en-UK-dictionary');
|
||||
await Zotero.Dictionaries.install('@fake-fr-FR-dictionary');
|
||||
await Zotero.Dictionaries.install('@fake-xx-UN-dictionary');
|
||||
sandbox.restore();
|
||||
|
||||
// Create metadata response for available dictionaries
|
||||
sandbox.stub(Zotero.Dictionaries, 'fetchDictionariesList')
|
||||
.resolves([
|
||||
{
|
||||
id: '@another-fake-en-UK-dictionary',
|
||||
locale: 'en-UK',
|
||||
name: "English (UK)",
|
||||
version: 1
|
||||
},
|
||||
{
|
||||
id: '@fake-fr-FR-dictionary',
|
||||
locale: 'fr-FR',
|
||||
name: "Français",
|
||||
version: 2
|
||||
}
|
||||
]);
|
||||
|
||||
sandbox.stub(Zotero.File, 'download').callsFake(async (url, downloadPath) => {
|
||||
if (url.includes('en-UK')) {
|
||||
return OS.File.copy(enUKXPINew, downloadPath);
|
||||
}
|
||||
if (url.includes('fr-FR')) {
|
||||
return OS.File.copy(frFRv2XPI, downloadPath);
|
||||
}
|
||||
throw new Error("Unexpected URL " + url);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("Zotero.Dictionaries", function () {
|
||||
describe("#update()", function () {
|
||||
it("should update outdated dictionary and replace an installed dictionary with a new one with a different id", async function () {
|
||||
var numDictionaries = Zotero.Dictionaries.dictionaries.length;
|
||||
function updated() {
|
||||
return !!(
|
||||
!Zotero.Dictionaries.dictionaries.find(x => x.id == '@fake-en-UK-dictionary')
|
||||
&& Zotero.Dictionaries.dictionaries.find(x => x.id == '@another-fake-en-UK-dictionary')
|
||||
// Version update happens too
|
||||
&& !Zotero.Dictionaries.dictionaries.find(x => x.id == '@fake-fr-FR-dictionary' && x.version == 1)
|
||||
&& Zotero.Dictionaries.dictionaries.find(x => x.id == '@fake-fr-FR-dictionary' && x.version == 2)
|
||||
);
|
||||
}
|
||||
assert.isFalse(updated());
|
||||
await Zotero.Dictionaries.update();
|
||||
assert.isTrue(updated());
|
||||
assert.lengthOf(Zotero.Dictionaries.dictionaries, numDictionaries);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Dictionary Manager", function () {
|
||||
beforeEach(async function () {
|
||||
win = Services.ww.openWindow(
|
||||
null,
|
||||
'chrome://zotero/content/dictionaryManager.xul',
|
||||
'dictionary-manager',
|
||||
'chrome,centerscreen',
|
||||
{}
|
||||
);
|
||||
while (!win.document.querySelectorAll('input[type="checkbox"]').length) {
|
||||
await Zotero.Promise.delay(50);
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
win.close();
|
||||
});
|
||||
|
||||
it("should show unknown dictionary as installed", async function () {
|
||||
var elems = win.document.querySelectorAll('input[type="checkbox"]');
|
||||
var names = [...elems].map(elem => elem.dataset.dictName);
|
||||
assert.sameMembers(names, ['English (UK)', 'Français', 'xx (UN)']);
|
||||
});
|
||||
|
||||
it("should update outdated dictionary and replace an installed dictionary with a new one with a different id", async function () {
|
||||
var numDictionaries = Zotero.Dictionaries.dictionaries.length;
|
||||
function updated() {
|
||||
return !!(
|
||||
!Zotero.Dictionaries.dictionaries.find(x => x.id == '@fake-en-UK-dictionary')
|
||||
&& Zotero.Dictionaries.dictionaries.find(x => x.id == '@another-fake-en-UK-dictionary')
|
||||
// Version update happens too
|
||||
&& !Zotero.Dictionaries.dictionaries.find(x => x.id == '@fake-fr-FR-dictionary' && x.version == 1)
|
||||
&& Zotero.Dictionaries.dictionaries.find(x => x.id == '@fake-fr-FR-dictionary' && x.version == 2)
|
||||
);
|
||||
}
|
||||
assert.isFalse(updated());
|
||||
win.document.querySelector('button[dlgtype="accept"]').click();
|
||||
while (!updated()) {
|
||||
await Zotero.Promise.delay(50);
|
||||
}
|
||||
assert.lengthOf(Zotero.Dictionaries.dictionaries, numDictionaries);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue