/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global EditContextMenu, MozXULElement */ const { InlineSpellChecker } = ChromeUtils.importESModule( "resource://gre/modules/InlineSpellChecker.sys.mjs" ); /** * Spellchecking for the subject field, contributed to the text context menu * the field shares with the window's other inputs. The suggestions and * add-to-dictionary items sit above the standard items and the enable toggle * and dictionary list below, the layout the browser context menu uses too. */ let spellChecker = null; let clearListenerAdded = false; /** * Resolves the word the menu was opened over. Both item sets call this, so * neither depends on the other having run first. * * @param {Element} input * @param {MouseEvent} event * @returns {?InlineSpellChecker} * Null while there's nothing to spellcheck, which includes the input not * having an editor yet. */ function initSpellChecker(input, event) { if (!spellChecker && input.editor) { spellChecker = new InlineSpellChecker(input.editor); } if (!spellChecker?.canSpellCheck) { return null; } spellChecker.initFromEvent(event.rangeParent, event.rangeOffset); return spellChecker; } /** * The suggestions and the dictionary list are built into the menu on every * open, so they have to come back out on every close. The menu is shared, and * a stale suggestion left in it would show up for another input. * * @param {Element} popup */ function ensureClearedOnHiding(popup) { if (clearListenerAdded) { return; } clearListenerAdded = true; popup.addEventListener("popuphiding", () => { spellChecker?.clearSuggestionsFromMenu(); spellChecker?.clearDictionaryListFromMenu(); }); } const matches = input => input == document.getElementById("msgSubject"); EditContextMenu.addItems({ matches, before: "edit-contextmenu-undo", createItems() { return MozXULElement.parseXULToFragment(` `); }, onShowing(input, items, event) { const [noSuggestions, addToDictionary, undoAddToDictionary, separator] = items; const checker = initSpellChecker(input, event); if (!checker) { for (const item of items) { item.hidden = true; } return; } ensureClearedOnHiding(noSuggestions.parentNode); // Reassigned on every open so they act on the current word. addToDictionary.oncommand = () => checker.addToDictionary(); undoAddToDictionary.oncommand = () => checker.undoAddToDictionary(); const overMisspelling = checker.overMisspelling; const canUndo = checker.canUndo(); addToDictionary.hidden = !overMisspelling; undoAddToDictionary.hidden = !canUndo; separator.hidden = !overMisspelling && !canUndo; const suggestions = checker.addSuggestionsToMenuOnParent( noSuggestions.parentNode, noSuggestions, 5 ); noSuggestions.hidden = !overMisspelling || !!suggestions; }, }); EditContextMenu.addItems({ matches, after: "edit-contextmenu-select-all", createItems() { return MozXULElement.parseXULToFragment(` `); }, onShowing(input, items, event) { const [separator, enabled, dictionaries] = items; const checker = initSpellChecker(input, event); if (!checker) { for (const item of items) { item.hidden = true; } return; } enabled.oncommand = () => checker.toggleEnabled(); enabled.toggleAttribute("checked", checker.enabled); separator.hidden = false; enabled.hidden = false; const dictionaryCount = checker.addDictionaryListToMenu( dictionaries.firstElementChild, null ); dictionaries.hidden = !checker.enabled || dictionaryCount <= 1; }, });