forked from mirrors/gecko-dev
		
	Depends on D167518 Differential Revision: https://phabricator.services.mozilla.com/D167519
		
			
				
	
	
		
			626 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			626 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* 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/. */
 | 
						|
 | 
						|
const MAX_UNDO_STACK_DEPTH = 1;
 | 
						|
 | 
						|
export function InlineSpellChecker(aEditor) {
 | 
						|
  this.init(aEditor);
 | 
						|
  this.mAddedWordStack = []; // We init this here to preserve it between init/uninit calls
 | 
						|
}
 | 
						|
 | 
						|
InlineSpellChecker.prototype = {
 | 
						|
  // Call this function to initialize for a given editor
 | 
						|
  init(aEditor) {
 | 
						|
    this.uninit();
 | 
						|
    this.mEditor = aEditor;
 | 
						|
    try {
 | 
						|
      this.mInlineSpellChecker = this.mEditor.getInlineSpellChecker(true);
 | 
						|
      // note: this might have been NULL if there is no chance we can spellcheck
 | 
						|
    } catch (e) {
 | 
						|
      this.mInlineSpellChecker = null;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  initFromRemote(aSpellInfo, aWindowGlobalParent) {
 | 
						|
    if (this.mRemote) {
 | 
						|
      // We shouldn't get here, but let's just recover instead of bricking the
 | 
						|
      // menu by throwing exceptions:
 | 
						|
      console.error(new Error("Unexpected remote spellchecker present!"));
 | 
						|
      try {
 | 
						|
        this.mRemote.uninit();
 | 
						|
      } catch (ex) {
 | 
						|
        console.error(ex);
 | 
						|
      }
 | 
						|
      this.mRemote = null;
 | 
						|
    }
 | 
						|
    this.uninit();
 | 
						|
 | 
						|
    if (!aSpellInfo) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    this.mInlineSpellChecker = this.mRemote = new RemoteSpellChecker(
 | 
						|
      aSpellInfo,
 | 
						|
      aWindowGlobalParent
 | 
						|
    );
 | 
						|
    this.mOverMisspelling = aSpellInfo.overMisspelling;
 | 
						|
    this.mMisspelling = aSpellInfo.misspelling;
 | 
						|
  },
 | 
						|
 | 
						|
  // call this to clear state
 | 
						|
  uninit() {
 | 
						|
    if (this.mRemote) {
 | 
						|
      this.mRemote.uninit();
 | 
						|
      this.mRemote = null;
 | 
						|
    }
 | 
						|
 | 
						|
    this.mEditor = null;
 | 
						|
    this.mInlineSpellChecker = null;
 | 
						|
    this.mOverMisspelling = false;
 | 
						|
    this.mMisspelling = "";
 | 
						|
    this.mMenu = null;
 | 
						|
    this.mSuggestionItems = [];
 | 
						|
    this.mDictionaryMenu = null;
 | 
						|
    this.mDictionaryItems = [];
 | 
						|
    this.mWordNode = null;
 | 
						|
  },
 | 
						|
 | 
						|
  // for each UI event, you must call this function, it will compute the
 | 
						|
  // word the cursor is over
 | 
						|
  initFromEvent(rangeParent, rangeOffset) {
 | 
						|
    this.mOverMisspelling = false;
 | 
						|
 | 
						|
    if (!rangeParent || !this.mInlineSpellChecker) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    var selcon = this.mEditor.selectionController;
 | 
						|
    var spellsel = selcon.getSelection(selcon.SELECTION_SPELLCHECK);
 | 
						|
    if (spellsel.rangeCount == 0) {
 | 
						|
      return;
 | 
						|
    } // easy case - no misspellings
 | 
						|
 | 
						|
    var range = this.mInlineSpellChecker.getMisspelledWord(
 | 
						|
      rangeParent,
 | 
						|
      rangeOffset
 | 
						|
    );
 | 
						|
    if (!range) {
 | 
						|
      return;
 | 
						|
    } // not over a misspelled word
 | 
						|
 | 
						|
    this.mMisspelling = range.toString();
 | 
						|
    this.mOverMisspelling = true;
 | 
						|
    this.mWordNode = rangeParent;
 | 
						|
    this.mWordOffset = rangeOffset;
 | 
						|
  },
 | 
						|
 | 
						|
  // returns false if there should be no spellchecking UI enabled at all, true
 | 
						|
  // means that you can at least give the user the ability to turn it on.
 | 
						|
  get canSpellCheck() {
 | 
						|
    // inline spell checker objects will be created only if there are actual
 | 
						|
    // dictionaries available
 | 
						|
    if (this.mRemote) {
 | 
						|
      return this.mRemote.canSpellCheck;
 | 
						|
    }
 | 
						|
    return this.mInlineSpellChecker != null;
 | 
						|
  },
 | 
						|
 | 
						|
  get initialSpellCheckPending() {
 | 
						|
    if (this.mRemote) {
 | 
						|
      return this.mRemote.spellCheckPending;
 | 
						|
    }
 | 
						|
    return !!(
 | 
						|
      this.mInlineSpellChecker &&
 | 
						|
      !this.mInlineSpellChecker.spellChecker &&
 | 
						|
      this.mInlineSpellChecker.spellCheckPending
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  // Whether spellchecking is enabled in the current box
 | 
						|
  get enabled() {
 | 
						|
    if (this.mRemote) {
 | 
						|
      return this.mRemote.enableRealTimeSpell;
 | 
						|
    }
 | 
						|
    return (
 | 
						|
      this.mInlineSpellChecker && this.mInlineSpellChecker.enableRealTimeSpell
 | 
						|
    );
 | 
						|
  },
 | 
						|
  set enabled(isEnabled) {
 | 
						|
    if (this.mRemote) {
 | 
						|
      this.mRemote.setSpellcheckUserOverride(isEnabled);
 | 
						|
    } else if (this.mInlineSpellChecker) {
 | 
						|
      this.mEditor.setSpellcheckUserOverride(isEnabled);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  // returns true if the given event is over a misspelled word
 | 
						|
  get overMisspelling() {
 | 
						|
    return this.mOverMisspelling;
 | 
						|
  },
 | 
						|
 | 
						|
  // this prepends up to "maxNumber" suggestions at the given menu position
 | 
						|
  // for the word under the cursor. Returns the number of suggestions inserted.
 | 
						|
  addSuggestionsToMenuOnParent(menu, insertBefore, maxNumber) {
 | 
						|
    if (this.mRemote) {
 | 
						|
      // This is used on parent process only.
 | 
						|
      // If you want to add suggestions to context menu, get suggestions then
 | 
						|
      // use addSuggestionsToMenu instead.
 | 
						|
      return 0;
 | 
						|
    }
 | 
						|
    if (!this.mInlineSpellChecker || !this.mOverMisspelling) {
 | 
						|
      return 0;
 | 
						|
    }
 | 
						|
 | 
						|
    let spellchecker = this.mInlineSpellChecker.spellChecker;
 | 
						|
    let spellSuggestions = [];
 | 
						|
 | 
						|
    try {
 | 
						|
      if (!spellchecker.CheckCurrentWord(this.mMisspelling)) {
 | 
						|
        return 0;
 | 
						|
      }
 | 
						|
 | 
						|
      for (let i = 0; i < maxNumber; i++) {
 | 
						|
        let suggestion = spellchecker.GetSuggestedWord();
 | 
						|
        if (!suggestion.length) {
 | 
						|
          // no more data
 | 
						|
          break;
 | 
						|
        }
 | 
						|
        spellSuggestions.push(suggestion);
 | 
						|
      }
 | 
						|
    } catch (e) {
 | 
						|
      return 0;
 | 
						|
    }
 | 
						|
    return this._addSuggestionsToMenu(menu, insertBefore, spellSuggestions);
 | 
						|
  },
 | 
						|
 | 
						|
  addSuggestionsToMenu(menu, insertBefore, spellSuggestions) {
 | 
						|
    if (
 | 
						|
      !this.mRemote &&
 | 
						|
      (!this.mInlineSpellChecker || !this.mOverMisspelling)
 | 
						|
    ) {
 | 
						|
      return 0;
 | 
						|
    } // nothing to do
 | 
						|
 | 
						|
    if (!spellSuggestions?.length) {
 | 
						|
      return 0;
 | 
						|
    }
 | 
						|
 | 
						|
    return this._addSuggestionsToMenu(menu, insertBefore, spellSuggestions);
 | 
						|
  },
 | 
						|
 | 
						|
  _addSuggestionsToMenu(menu, insertBefore, spellSuggestions) {
 | 
						|
    this.mMenu = menu;
 | 
						|
    this.mSuggestionItems = [];
 | 
						|
 | 
						|
    for (let suggestion of spellSuggestions) {
 | 
						|
      var item = menu.ownerDocument.createXULElement("menuitem");
 | 
						|
      this.mSuggestionItems.push(item);
 | 
						|
      item.setAttribute("label", suggestion);
 | 
						|
      item.setAttribute("value", suggestion);
 | 
						|
      item.addEventListener(
 | 
						|
        "command",
 | 
						|
        this.replaceMisspelling.bind(this, suggestion),
 | 
						|
        true
 | 
						|
      );
 | 
						|
      item.setAttribute("class", "spell-suggestion");
 | 
						|
      menu.insertBefore(item, insertBefore);
 | 
						|
    }
 | 
						|
    return spellSuggestions.length;
 | 
						|
  },
 | 
						|
 | 
						|
  // undoes the work of addSuggestionsToMenu for the same menu
 | 
						|
  // (call from popup hiding)
 | 
						|
  clearSuggestionsFromMenu() {
 | 
						|
    for (var i = 0; i < this.mSuggestionItems.length; i++) {
 | 
						|
      this.mMenu.removeChild(this.mSuggestionItems[i]);
 | 
						|
    }
 | 
						|
    this.mSuggestionItems = [];
 | 
						|
  },
 | 
						|
 | 
						|
  sortDictionaryList(list) {
 | 
						|
    var sortedList = [];
 | 
						|
    var names = Services.intl.getLocaleDisplayNames(undefined, list);
 | 
						|
    for (var i = 0; i < list.length; i++) {
 | 
						|
      sortedList.push({ localeCode: list[i], displayName: names[i] });
 | 
						|
    }
 | 
						|
    let comparer = new Services.intl.Collator().compare;
 | 
						|
    sortedList.sort((a, b) => comparer(a.displayName, b.displayName));
 | 
						|
    return sortedList;
 | 
						|
  },
 | 
						|
 | 
						|
  async languageMenuListener(evt) {
 | 
						|
    let curlangs = new Set();
 | 
						|
    if (this.mRemote) {
 | 
						|
      curlangs = new Set(this.mRemote.currentDictionaries);
 | 
						|
    } else if (this.mInlineSpellChecker) {
 | 
						|
      let spellchecker = this.mInlineSpellChecker.spellChecker;
 | 
						|
      try {
 | 
						|
        curlangs = new Set(spellchecker.getCurrentDictionaries());
 | 
						|
      } catch (e) {}
 | 
						|
    }
 | 
						|
 | 
						|
    let localeCodes = new Set(curlangs);
 | 
						|
    let localeCode = evt.target.dataset.localeCode;
 | 
						|
    if (localeCodes.has(localeCode)) {
 | 
						|
      localeCodes.delete(localeCode);
 | 
						|
    } else {
 | 
						|
      localeCodes.add(localeCode);
 | 
						|
    }
 | 
						|
    let dictionaries = Array.from(localeCodes);
 | 
						|
    await this.selectDictionaries(dictionaries);
 | 
						|
    if (this.mRemote) {
 | 
						|
      // Store the new set in case the menu doesn't close.
 | 
						|
      this.mRemote.currentDictionaries = dictionaries;
 | 
						|
    }
 | 
						|
    // Notify change of dictionary, especially for Thunderbird,
 | 
						|
    // which is otherwise not notified any more.
 | 
						|
    let view = this.mDictionaryMenu.ownerGlobal;
 | 
						|
    let spellcheckChangeEvent = new view.CustomEvent("spellcheck-changed", {
 | 
						|
      detail: { dictionaries },
 | 
						|
    });
 | 
						|
    this.mDictionaryMenu.ownerDocument.dispatchEvent(spellcheckChangeEvent);
 | 
						|
  },
 | 
						|
 | 
						|
  // returns the number of dictionary languages. If insertBefore is NULL, this
 | 
						|
  // does an append to the given menu
 | 
						|
  addDictionaryListToMenu(menu, insertBefore) {
 | 
						|
    this.mDictionaryMenu = menu;
 | 
						|
    this.mDictionaryItems = [];
 | 
						|
 | 
						|
    if (!this.enabled) {
 | 
						|
      return 0;
 | 
						|
    }
 | 
						|
 | 
						|
    let list;
 | 
						|
    let curlangs = new Set();
 | 
						|
    if (this.mRemote) {
 | 
						|
      list = this.mRemote.dictionaryList;
 | 
						|
      curlangs = new Set(this.mRemote.currentDictionaries);
 | 
						|
    } else if (this.mInlineSpellChecker) {
 | 
						|
      let spellchecker = this.mInlineSpellChecker.spellChecker;
 | 
						|
      list = spellchecker.GetDictionaryList();
 | 
						|
      try {
 | 
						|
        curlangs = new Set(spellchecker.getCurrentDictionaries());
 | 
						|
      } catch (e) {}
 | 
						|
    }
 | 
						|
 | 
						|
    let sortedList = this.sortDictionaryList(list);
 | 
						|
    this.languageMenuListenerBind = this.languageMenuListener.bind(this);
 | 
						|
    menu.addEventListener("command", this.languageMenuListenerBind, true);
 | 
						|
 | 
						|
    for (let i = 0; i < sortedList.length; i++) {
 | 
						|
      let item = menu.ownerDocument.createXULElement("menuitem");
 | 
						|
 | 
						|
      item.setAttribute(
 | 
						|
        "id",
 | 
						|
        "spell-check-dictionary-" + sortedList[i].localeCode
 | 
						|
      );
 | 
						|
      // XXX: Once Fluent has dynamic references, we could also lazily
 | 
						|
      //      inject regionNames/languageNames FTL and localize using
 | 
						|
      //      `l10n-id` here.
 | 
						|
      item.setAttribute("label", sortedList[i].displayName);
 | 
						|
      item.setAttribute("type", "checkbox");
 | 
						|
      item.setAttribute("selection-type", "multiple");
 | 
						|
      if (sortedList.length > 1) {
 | 
						|
        item.setAttribute("closemenu", "none");
 | 
						|
      }
 | 
						|
      this.mDictionaryItems.push(item);
 | 
						|
      item.dataset.localeCode = sortedList[i].localeCode;
 | 
						|
      if (curlangs.has(sortedList[i].localeCode)) {
 | 
						|
        item.setAttribute("checked", "true");
 | 
						|
      }
 | 
						|
      if (insertBefore) {
 | 
						|
        menu.insertBefore(item, insertBefore);
 | 
						|
      } else {
 | 
						|
        menu.appendChild(item);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return list.length;
 | 
						|
  },
 | 
						|
 | 
						|
  // undoes the work of addDictionaryListToMenu for the menu
 | 
						|
  // (call on popup hiding)
 | 
						|
  clearDictionaryListFromMenu() {
 | 
						|
    this.mDictionaryMenu?.removeEventListener(
 | 
						|
      "command",
 | 
						|
      this.languageMenuListenerBind,
 | 
						|
      true
 | 
						|
    );
 | 
						|
    for (var i = 0; i < this.mDictionaryItems.length; i++) {
 | 
						|
      this.mDictionaryMenu.removeChild(this.mDictionaryItems[i]);
 | 
						|
    }
 | 
						|
    this.mDictionaryItems = [];
 | 
						|
  },
 | 
						|
 | 
						|
  // callback for selecting a dictionary
 | 
						|
  async selectDictionaries(localeCodes) {
 | 
						|
    if (this.mRemote) {
 | 
						|
      this.mRemote.selectDictionaries(localeCodes);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    if (!this.mInlineSpellChecker) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    var spellchecker = this.mInlineSpellChecker.spellChecker;
 | 
						|
    await spellchecker.setCurrentDictionaries(localeCodes);
 | 
						|
    this.mInlineSpellChecker.spellCheckRange(null); // causes recheck
 | 
						|
  },
 | 
						|
 | 
						|
  // callback for selecting a suggested replacement
 | 
						|
  replaceMisspelling(suggestion) {
 | 
						|
    if (this.mRemote) {
 | 
						|
      this.mRemote.replaceMisspelling(suggestion);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    if (!this.mInlineSpellChecker || !this.mOverMisspelling) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    this.mInlineSpellChecker.replaceWord(
 | 
						|
      this.mWordNode,
 | 
						|
      this.mWordOffset,
 | 
						|
      suggestion
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  // callback for enabling or disabling spellchecking
 | 
						|
  toggleEnabled() {
 | 
						|
    if (this.mRemote) {
 | 
						|
      this.mRemote.toggleEnabled();
 | 
						|
    } else {
 | 
						|
      this.mEditor.setSpellcheckUserOverride(
 | 
						|
        !this.mInlineSpellChecker.enableRealTimeSpell
 | 
						|
      );
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  // callback for adding the current misspelling to the user-defined dictionary
 | 
						|
  addToDictionary() {
 | 
						|
    // Prevent the undo stack from growing over the max depth
 | 
						|
    if (this.mAddedWordStack.length == MAX_UNDO_STACK_DEPTH) {
 | 
						|
      this.mAddedWordStack.shift();
 | 
						|
    }
 | 
						|
 | 
						|
    this.mAddedWordStack.push(this.mMisspelling);
 | 
						|
    if (this.mRemote) {
 | 
						|
      this.mRemote.addToDictionary();
 | 
						|
    } else {
 | 
						|
      this.mInlineSpellChecker.addWordToDictionary(this.mMisspelling);
 | 
						|
    }
 | 
						|
  },
 | 
						|
  // callback for removing the last added word to the dictionary LIFO fashion
 | 
						|
  undoAddToDictionary() {
 | 
						|
    if (this.mAddedWordStack.length) {
 | 
						|
      var word = this.mAddedWordStack.pop();
 | 
						|
      if (this.mRemote) {
 | 
						|
        this.mRemote.undoAddToDictionary(word);
 | 
						|
      } else {
 | 
						|
        this.mInlineSpellChecker.removeWordFromDictionary(word);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
  canUndo() {
 | 
						|
    // Return true if we have words on the stack
 | 
						|
    return !!this.mAddedWordStack.length;
 | 
						|
  },
 | 
						|
  ignoreWord() {
 | 
						|
    if (this.mRemote) {
 | 
						|
      this.mRemote.ignoreWord();
 | 
						|
    } else {
 | 
						|
      this.mInlineSpellChecker.ignoreWord(this.mMisspelling);
 | 
						|
    }
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
export var SpellCheckHelper = {
 | 
						|
  // Set when over a non-read-only <textarea> or editable <input>
 | 
						|
  // (that allows text entry of some kind, so not e.g. <input type=checkbox>)
 | 
						|
  EDITABLE: 0x1,
 | 
						|
 | 
						|
  // Set when over an <input> element of any type.
 | 
						|
  INPUT: 0x2,
 | 
						|
 | 
						|
  // Set when over any <textarea>.
 | 
						|
  TEXTAREA: 0x4,
 | 
						|
 | 
						|
  // Set when over any text-entry <input>.
 | 
						|
  TEXTINPUT: 0x8,
 | 
						|
 | 
						|
  // Set when over an <input> that can be used as a keyword field.
 | 
						|
  KEYWORD: 0x10,
 | 
						|
 | 
						|
  // Set when over an element that otherwise would not be considered
 | 
						|
  // "editable" but is because content editable is enabled for the document.
 | 
						|
  CONTENTEDITABLE: 0x20,
 | 
						|
 | 
						|
  // Set when over an <input type="number"> or other non-text field.
 | 
						|
  NUMERIC: 0x40,
 | 
						|
 | 
						|
  // Set when over an <input type="password"> field.
 | 
						|
  PASSWORD: 0x80,
 | 
						|
 | 
						|
  // Set when spellcheckable. Replaces `EDITABLE`/`CONTENTEDITABLE` combination
 | 
						|
  // specifically for spellcheck.
 | 
						|
  SPELLCHECKABLE: 0x100,
 | 
						|
 | 
						|
  isTargetAKeywordField(aNode, window) {
 | 
						|
    if (!window.HTMLInputElement.isInstance(aNode)) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    var form = aNode.form;
 | 
						|
    if (!form || aNode.type == "password") {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    var method = form.method.toUpperCase();
 | 
						|
 | 
						|
    // These are the following types of forms we can create keywords for:
 | 
						|
    //
 | 
						|
    // method   encoding type       can create keyword
 | 
						|
    // GET      *                                 YES
 | 
						|
    //          *                                 YES
 | 
						|
    // POST                                       YES
 | 
						|
    // POST     application/x-www-form-urlencoded YES
 | 
						|
    // POST     text/plain                        NO (a little tricky to do)
 | 
						|
    // POST     multipart/form-data               NO
 | 
						|
    // POST     everything else                   YES
 | 
						|
    return (
 | 
						|
      method == "GET" ||
 | 
						|
      method == "" ||
 | 
						|
      (form.enctype != "text/plain" && form.enctype != "multipart/form-data")
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  // Returns the computed style attribute for the given element.
 | 
						|
  getComputedStyle(aElem, aProp) {
 | 
						|
    return aElem.ownerGlobal.getComputedStyle(aElem).getPropertyValue(aProp);
 | 
						|
  },
 | 
						|
 | 
						|
  isEditable(element, window) {
 | 
						|
    var flags = 0;
 | 
						|
    if (window.HTMLInputElement.isInstance(element)) {
 | 
						|
      flags |= this.INPUT;
 | 
						|
      if (element.mozIsTextField(false) || element.type == "number") {
 | 
						|
        flags |= this.TEXTINPUT;
 | 
						|
        if (!element.readOnly) {
 | 
						|
          flags |= this.EDITABLE;
 | 
						|
        }
 | 
						|
 | 
						|
        if (element.type == "number") {
 | 
						|
          flags |= this.NUMERIC;
 | 
						|
        }
 | 
						|
 | 
						|
        // Allow spellchecking UI on all text and search inputs.
 | 
						|
        if (
 | 
						|
          !element.readOnly &&
 | 
						|
          (element.type == "text" || element.type == "search")
 | 
						|
        ) {
 | 
						|
          flags |= this.SPELLCHECKABLE;
 | 
						|
        }
 | 
						|
        if (this.isTargetAKeywordField(element, window)) {
 | 
						|
          flags |= this.KEYWORD;
 | 
						|
        }
 | 
						|
        if (element.type == "password") {
 | 
						|
          flags |= this.PASSWORD;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    } else if (window.HTMLTextAreaElement.isInstance(element)) {
 | 
						|
      flags |= this.TEXTINPUT | this.TEXTAREA;
 | 
						|
      if (!element.readOnly) {
 | 
						|
        flags |= this.SPELLCHECKABLE | this.EDITABLE;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (!(flags & this.SPELLCHECKABLE)) {
 | 
						|
      var win = element.ownerGlobal;
 | 
						|
      if (win) {
 | 
						|
        var isSpellcheckable = false;
 | 
						|
        try {
 | 
						|
          var editingSession = win.docShell.editingSession;
 | 
						|
          if (
 | 
						|
            editingSession.windowIsEditable(win) &&
 | 
						|
            this.getComputedStyle(element, "-moz-user-modify") == "read-write"
 | 
						|
          ) {
 | 
						|
            isSpellcheckable = true;
 | 
						|
          }
 | 
						|
        } catch (ex) {
 | 
						|
          // If someone built with composer disabled, we can't get an editing session.
 | 
						|
        }
 | 
						|
 | 
						|
        if (isSpellcheckable) {
 | 
						|
          flags |= this.CONTENTEDITABLE | this.SPELLCHECKABLE;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return flags;
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
function RemoteSpellChecker(aSpellInfo, aWindowGlobalParent) {
 | 
						|
  this._spellInfo = aSpellInfo;
 | 
						|
  this._suggestionGenerator = null;
 | 
						|
  this._actor = aWindowGlobalParent.getActor("InlineSpellChecker");
 | 
						|
  this._actor.registerDestructionObserver(this);
 | 
						|
}
 | 
						|
 | 
						|
RemoteSpellChecker.prototype = {
 | 
						|
  get canSpellCheck() {
 | 
						|
    return this._spellInfo.canSpellCheck;
 | 
						|
  },
 | 
						|
  get spellCheckPending() {
 | 
						|
    return this._spellInfo.initialSpellCheckPending;
 | 
						|
  },
 | 
						|
  get overMisspelling() {
 | 
						|
    return this._spellInfo.overMisspelling;
 | 
						|
  },
 | 
						|
  get enableRealTimeSpell() {
 | 
						|
    return this._spellInfo.enableRealTimeSpell;
 | 
						|
  },
 | 
						|
  get suggestions() {
 | 
						|
    return this._spellInfo.spellSuggestions;
 | 
						|
  },
 | 
						|
 | 
						|
  get currentDictionaries() {
 | 
						|
    return this._spellInfo.currentDictionaries;
 | 
						|
  },
 | 
						|
  set currentDictionaries(dicts) {
 | 
						|
    this._spellInfo.currentDictionaries = dicts;
 | 
						|
  },
 | 
						|
  get dictionaryList() {
 | 
						|
    return this._spellInfo.dictionaryList.slice();
 | 
						|
  },
 | 
						|
 | 
						|
  selectDictionaries(localeCodes) {
 | 
						|
    this._actor.selectDictionaries({ localeCodes });
 | 
						|
  },
 | 
						|
 | 
						|
  replaceMisspelling(suggestion) {
 | 
						|
    this._actor.replaceMisspelling({ suggestion });
 | 
						|
  },
 | 
						|
 | 
						|
  toggleEnabled() {
 | 
						|
    this._actor.toggleEnabled();
 | 
						|
  },
 | 
						|
  addToDictionary() {
 | 
						|
    // This is really ugly. There is an nsISpellChecker somewhere in the
 | 
						|
    // parent that corresponds to our current element's spell checker in the
 | 
						|
    // child, but it's hard to access it. However, we know that
 | 
						|
    // addToDictionary adds the word to the singleton personal dictionary, so
 | 
						|
    // we just do that here.
 | 
						|
    // NB: We also rely on the fact that we only ever pass an empty string in
 | 
						|
    // as the "lang".
 | 
						|
 | 
						|
    let dictionary = Cc[
 | 
						|
      "@mozilla.org/spellchecker/personaldictionary;1"
 | 
						|
    ].getService(Ci.mozIPersonalDictionary);
 | 
						|
    dictionary.addWord(this._spellInfo.misspelling);
 | 
						|
    this._actor.recheckSpelling();
 | 
						|
  },
 | 
						|
  undoAddToDictionary(word) {
 | 
						|
    let dictionary = Cc[
 | 
						|
      "@mozilla.org/spellchecker/personaldictionary;1"
 | 
						|
    ].getService(Ci.mozIPersonalDictionary);
 | 
						|
    dictionary.removeWord(word);
 | 
						|
    this._actor.recheckSpelling();
 | 
						|
  },
 | 
						|
  ignoreWord() {
 | 
						|
    let dictionary = Cc[
 | 
						|
      "@mozilla.org/spellchecker/personaldictionary;1"
 | 
						|
    ].getService(Ci.mozIPersonalDictionary);
 | 
						|
    dictionary.ignoreWord(this._spellInfo.misspelling);
 | 
						|
    this._actor.recheckSpelling();
 | 
						|
  },
 | 
						|
  uninit() {
 | 
						|
    if (this._actor) {
 | 
						|
      this._actor.uninit();
 | 
						|
      this._actor.unregisterDestructionObserver(this);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  actorDestroyed() {
 | 
						|
    // The actor lets us know if it gets destroyed, so we don't
 | 
						|
    // later try to call `.uninit()` on it.
 | 
						|
    this._actor = null;
 | 
						|
  },
 | 
						|
};
 |