/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Inline spellcheck code * * The Initial Developer of the Original Code is * Google Inc. * Portions created by the Initial Developer are Copyright (C) 2005 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Brett Wilson * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ var EXPORTED_SYMBOLS = [ "InlineSpellChecker" ]; var gLanguageBundle; var gRegionBundle; const MAX_UNDO_STACK_DEPTH = 1; 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: function(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; } }, // call this to clear state uninit: function() { this.mEditor = null; this.mInlineSpellChecker = null; this.mOverMisspelling = false; this.mMisspelling = ""; this.mMenu = null; this.mSpellSuggestions = []; this.mSuggestionItems = []; this.mDictionaryMenu = null; this.mDictionaryNames = []; this.mDictionaryItems = []; this.mWordNode = null; }, // for each UI event, you must call this function, it will compute the // word the cursor is over initFromEvent: function(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 return (this.mInlineSpellChecker != null); }, // Whether spellchecking is enabled in the current box get enabled() { return (this.mInlineSpellChecker && this.mInlineSpellChecker.enableRealTimeSpell); }, set enabled(isEnabled) { 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. addSuggestionsToMenu: function(menu, insertBefore, maxNumber) { if (! this.mInlineSpellChecker || ! this.mOverMisspelling) return 0; // nothing to do var spellchecker = this.mInlineSpellChecker.spellChecker; try { if (! spellchecker.CheckCurrentWord(this.mMisspelling)) return 0; // word seems not misspelled after all (?) } catch(e) { return 0; } this.mMenu = menu; this.mSpellSuggestions = []; this.mSuggestionItems = []; for (var i = 0; i < maxNumber; i ++) { var suggestion = spellchecker.GetSuggestedWord(); if (! suggestion.length) break; this.mSpellSuggestions.push(suggestion); var item = menu.ownerDocument.createElement("menuitem"); this.mSuggestionItems.push(item); item.setAttribute("label", suggestion); item.setAttribute("value", suggestion); // this function thing is necessary to generate a callback with the // correct binding of "val" (the index in this loop). var callback = function(me, val) { return function(evt) { me.replaceMisspelling(val); } }; item.addEventListener("command", callback(this, i), true); item.setAttribute("class", "spell-suggestion"); menu.insertBefore(item, insertBefore); } return this.mSpellSuggestions.length; }, // undoes the work of addSuggestionsToMenu for the same menu // (call from popup hiding) clearSuggestionsFromMenu: function() { for (var i = 0; i < this.mSuggestionItems.length; i ++) { this.mMenu.removeChild(this.mSuggestionItems[i]); } this.mSuggestionItems = []; }, // returns the number of dictionary languages. If insertBefore is NULL, this // does an append to the given menu addDictionaryListToMenu: function(menu, insertBefore) { this.mDictionaryMenu = menu; this.mDictionaryNames = []; this.mDictionaryItems = []; if (! gLanguageBundle) { // create the bundles for language and region var bundleService = Components.classes["@mozilla.org/intl/stringbundle;1"] .getService(Components.interfaces.nsIStringBundleService); gLanguageBundle = bundleService.createBundle( "chrome://global/locale/languageNames.properties"); gRegionBundle = bundleService.createBundle( "chrome://global/locale/regionNames.properties"); } if (! this.mInlineSpellChecker || ! this.enabled) return 0; var spellchecker = this.mInlineSpellChecker.spellChecker; var o1 = {}, o2 = {}; spellchecker.GetDictionaryList(o1, o2); var list = o1.value; var listcount = o2.value; var curlang = ""; try { curlang = spellchecker.GetCurrentDictionary(); } catch(e) {} var isoStrArray; for (var i = 0; i < list.length; i ++) { // get the display name for this dictionary isoStrArray = list[i].split(/[-_]/); var displayName = ""; if (gLanguageBundle && isoStrArray[0]) { try { displayName = gLanguageBundle.GetStringFromName(isoStrArray[0].toLowerCase()); } catch(e) {} // ignore language bundle errors if (gRegionBundle && isoStrArray[1]) { try { displayName += " / " + gRegionBundle.GetStringFromName(isoStrArray[1].toLowerCase()); } catch(e) {} // ignore region bundle errors if (isoStrArray[2]) displayName += " (" + isoStrArray[2] + ")"; } } // if we didn't get a name, just use the raw dictionary name if (displayName.length == 0) displayName = list[i]; this.mDictionaryNames.push(list[i]); var item = menu.ownerDocument.createElement("menuitem"); item.setAttribute("id", "spell-check-dictionary-" + list[i]); item.setAttribute("label", displayName); item.setAttribute("type", "radio"); this.mDictionaryItems.push(item); if (curlang == list[i]) { item.setAttribute("checked", "true"); } else { var callback = function(me, val) { return function(evt) { me.selectDictionary(val); } }; item.addEventListener("command", callback(this, i), 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: function() { for (var i = 0; i < this.mDictionaryItems.length; i ++) { this.mDictionaryMenu.removeChild(this.mDictionaryItems[i]); } this.mDictionaryItems = []; }, // callback for selecting a dictionary selectDictionary: function(index) { if (! this.mInlineSpellChecker || index < 0 || index >= this.mDictionaryNames.length) return; var spellchecker = this.mInlineSpellChecker.spellChecker; spellchecker.SetCurrentDictionary(this.mDictionaryNames[index]); this.mInlineSpellChecker.spellCheckRange(null); // causes recheck }, // callback for selecting a suggesteed replacement replaceMisspelling: function(index) { if (! this.mInlineSpellChecker || ! this.mOverMisspelling) return; if (index < 0 || index >= this.mSpellSuggestions.length) return; this.mInlineSpellChecker.replaceWord(this.mWordNode, this.mWordOffset, this.mSpellSuggestions[index]); }, // callback for enabling or disabling spellchecking toggleEnabled: function() { this.mEditor.setSpellcheckUserOverride(!this.mInlineSpellChecker.enableRealTimeSpell); }, // callback for adding the current misspelling to the user-defined dictionary addToDictionary: function() { // 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); this.mInlineSpellChecker.addWordToDictionary(this.mMisspelling); }, // callback for removing the last added word to the dictionary LIFO fashion undoAddToDictionary: function() { if (this.mAddedWordStack.length > 0) { var word = this.mAddedWordStack.pop(); this.mInlineSpellChecker.removeWordFromDictionary(word); } }, canUndo : function() { // Return true if we have words on the stack return (this.mAddedWordStack.length > 0); }, ignoreWord: function() { this.mInlineSpellChecker.ignoreWord(this.mMisspelling); } };