1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/modules/InlineSpellChecker.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,323 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +this.EXPORTED_SYMBOLS = [ "InlineSpellChecker" ]; 1.9 +var gLanguageBundle; 1.10 +var gRegionBundle; 1.11 +const MAX_UNDO_STACK_DEPTH = 1; 1.12 + 1.13 +this.InlineSpellChecker = function InlineSpellChecker(aEditor) { 1.14 + this.init(aEditor); 1.15 + this.mAddedWordStack = []; // We init this here to preserve it between init/uninit calls 1.16 +} 1.17 + 1.18 +InlineSpellChecker.prototype = { 1.19 + // Call this function to initialize for a given editor 1.20 + init: function(aEditor) 1.21 + { 1.22 + this.uninit(); 1.23 + this.mEditor = aEditor; 1.24 + try { 1.25 + this.mInlineSpellChecker = this.mEditor.getInlineSpellChecker(true); 1.26 + // note: this might have been NULL if there is no chance we can spellcheck 1.27 + } catch(e) { 1.28 + this.mInlineSpellChecker = null; 1.29 + } 1.30 + }, 1.31 + 1.32 + // call this to clear state 1.33 + uninit: function() 1.34 + { 1.35 + this.mEditor = null; 1.36 + this.mInlineSpellChecker = null; 1.37 + this.mOverMisspelling = false; 1.38 + this.mMisspelling = ""; 1.39 + this.mMenu = null; 1.40 + this.mSpellSuggestions = []; 1.41 + this.mSuggestionItems = []; 1.42 + this.mDictionaryMenu = null; 1.43 + this.mDictionaryNames = []; 1.44 + this.mDictionaryItems = []; 1.45 + this.mWordNode = null; 1.46 + }, 1.47 + 1.48 + // for each UI event, you must call this function, it will compute the 1.49 + // word the cursor is over 1.50 + initFromEvent: function(rangeParent, rangeOffset) 1.51 + { 1.52 + this.mOverMisspelling = false; 1.53 + 1.54 + if (!rangeParent || !this.mInlineSpellChecker) 1.55 + return; 1.56 + 1.57 + var selcon = this.mEditor.selectionController; 1.58 + var spellsel = selcon.getSelection(selcon.SELECTION_SPELLCHECK); 1.59 + if (spellsel.rangeCount == 0) 1.60 + return; // easy case - no misspellings 1.61 + 1.62 + var range = this.mInlineSpellChecker.getMisspelledWord(rangeParent, 1.63 + rangeOffset); 1.64 + if (! range) 1.65 + return; // not over a misspelled word 1.66 + 1.67 + this.mMisspelling = range.toString(); 1.68 + this.mOverMisspelling = true; 1.69 + this.mWordNode = rangeParent; 1.70 + this.mWordOffset = rangeOffset; 1.71 + }, 1.72 + 1.73 + // returns false if there should be no spellchecking UI enabled at all, true 1.74 + // means that you can at least give the user the ability to turn it on. 1.75 + get canSpellCheck() 1.76 + { 1.77 + // inline spell checker objects will be created only if there are actual 1.78 + // dictionaries available 1.79 + return (this.mInlineSpellChecker != null); 1.80 + }, 1.81 + 1.82 + // Whether spellchecking is enabled in the current box 1.83 + get enabled() 1.84 + { 1.85 + return (this.mInlineSpellChecker && 1.86 + this.mInlineSpellChecker.enableRealTimeSpell); 1.87 + }, 1.88 + set enabled(isEnabled) 1.89 + { 1.90 + if (this.mInlineSpellChecker) 1.91 + this.mEditor.setSpellcheckUserOverride(isEnabled); 1.92 + }, 1.93 + 1.94 + // returns true if the given event is over a misspelled word 1.95 + get overMisspelling() 1.96 + { 1.97 + return this.mOverMisspelling; 1.98 + }, 1.99 + 1.100 + // this prepends up to "maxNumber" suggestions at the given menu position 1.101 + // for the word under the cursor. Returns the number of suggestions inserted. 1.102 + addSuggestionsToMenu: function(menu, insertBefore, maxNumber) 1.103 + { 1.104 + if (! this.mInlineSpellChecker || ! this.mOverMisspelling) 1.105 + return 0; // nothing to do 1.106 + 1.107 + var spellchecker = this.mInlineSpellChecker.spellChecker; 1.108 + try { 1.109 + if (! spellchecker.CheckCurrentWord(this.mMisspelling)) 1.110 + return 0; // word seems not misspelled after all (?) 1.111 + } catch(e) { 1.112 + return 0; 1.113 + } 1.114 + 1.115 + this.mMenu = menu; 1.116 + this.mSpellSuggestions = []; 1.117 + this.mSuggestionItems = []; 1.118 + for (var i = 0; i < maxNumber; i ++) { 1.119 + var suggestion = spellchecker.GetSuggestedWord(); 1.120 + if (! suggestion.length) 1.121 + break; 1.122 + this.mSpellSuggestions.push(suggestion); 1.123 + 1.124 + var item = menu.ownerDocument.createElement("menuitem"); 1.125 + this.mSuggestionItems.push(item); 1.126 + item.setAttribute("label", suggestion); 1.127 + item.setAttribute("value", suggestion); 1.128 + // this function thing is necessary to generate a callback with the 1.129 + // correct binding of "val" (the index in this loop). 1.130 + var callback = function(me, val) { return function(evt) { me.replaceMisspelling(val); } }; 1.131 + item.addEventListener("command", callback(this, i), true); 1.132 + item.setAttribute("class", "spell-suggestion"); 1.133 + menu.insertBefore(item, insertBefore); 1.134 + } 1.135 + return this.mSpellSuggestions.length; 1.136 + }, 1.137 + 1.138 + // undoes the work of addSuggestionsToMenu for the same menu 1.139 + // (call from popup hiding) 1.140 + clearSuggestionsFromMenu: function() 1.141 + { 1.142 + for (var i = 0; i < this.mSuggestionItems.length; i ++) { 1.143 + this.mMenu.removeChild(this.mSuggestionItems[i]); 1.144 + } 1.145 + this.mSuggestionItems = []; 1.146 + }, 1.147 + 1.148 + // returns the number of dictionary languages. If insertBefore is NULL, this 1.149 + // does an append to the given menu 1.150 + addDictionaryListToMenu: function(menu, insertBefore) 1.151 + { 1.152 + this.mDictionaryMenu = menu; 1.153 + this.mDictionaryNames = []; 1.154 + this.mDictionaryItems = []; 1.155 + 1.156 + if (! this.mInlineSpellChecker || ! this.enabled) 1.157 + return 0; 1.158 + var spellchecker = this.mInlineSpellChecker.spellChecker; 1.159 + var o1 = {}, o2 = {}; 1.160 + spellchecker.GetDictionaryList(o1, o2); 1.161 + var list = o1.value; 1.162 + var listcount = o2.value; 1.163 + var curlang = ""; 1.164 + try { 1.165 + curlang = spellchecker.GetCurrentDictionary(); 1.166 + } catch(e) {} 1.167 + 1.168 + var sortedList = []; 1.169 + for (var i = 0; i < list.length; i ++) { 1.170 + sortedList.push({"id": list[i], 1.171 + "label": this.getDictionaryDisplayName(list[i])}); 1.172 + } 1.173 + sortedList.sort(function(a, b) { 1.174 + if (a.label < b.label) 1.175 + return -1; 1.176 + if (a.label > b.label) 1.177 + return 1; 1.178 + return 0; 1.179 + }); 1.180 + 1.181 + for (var i = 0; i < sortedList.length; i ++) { 1.182 + this.mDictionaryNames.push(sortedList[i].id); 1.183 + var item = menu.ownerDocument.createElement("menuitem"); 1.184 + item.setAttribute("id", "spell-check-dictionary-" + sortedList[i].id); 1.185 + item.setAttribute("label", sortedList[i].label); 1.186 + item.setAttribute("type", "radio"); 1.187 + this.mDictionaryItems.push(item); 1.188 + if (curlang == sortedList[i].id) { 1.189 + item.setAttribute("checked", "true"); 1.190 + } else { 1.191 + var callback = function(me, val) { return function(evt) { me.selectDictionary(val); } }; 1.192 + item.addEventListener("command", callback(this, i), true); 1.193 + } 1.194 + if (insertBefore) 1.195 + menu.insertBefore(item, insertBefore); 1.196 + else 1.197 + menu.appendChild(item); 1.198 + } 1.199 + return list.length; 1.200 + }, 1.201 + 1.202 + // Formats a valid BCP 47 language tag based on available localized names. 1.203 + getDictionaryDisplayName: function(dictionaryName) { 1.204 + try { 1.205 + // Get the display name for this dictionary. 1.206 + let languageTagMatch = /^([a-z]{2,3}|[a-z]{4}|[a-z]{5,8})(?:[-_]([a-z]{4}))?(?:[-_]([A-Z]{2}|[0-9]{3}))?((?:[-_](?:[a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*)(?:[-_][a-wy-z0-9](?:[-_][a-z0-9]{2,8})+)*(?:[-_]x(?:[-_][a-z0-9]{1,8})+)?$/i; 1.207 + var [languageTag, languageSubtag, scriptSubtag, regionSubtag, variantSubtags] = dictionaryName.match(languageTagMatch); 1.208 + } catch(e) { 1.209 + // If we weren't given a valid language tag, just use the raw dictionary name. 1.210 + return dictionaryName; 1.211 + } 1.212 + 1.213 + if (!gLanguageBundle) { 1.214 + // Create the bundles for language and region names. 1.215 + var bundleService = Components.classes["@mozilla.org/intl/stringbundle;1"] 1.216 + .getService(Components.interfaces.nsIStringBundleService); 1.217 + gLanguageBundle = bundleService.createBundle( 1.218 + "chrome://global/locale/languageNames.properties"); 1.219 + gRegionBundle = bundleService.createBundle( 1.220 + "chrome://global/locale/regionNames.properties"); 1.221 + } 1.222 + 1.223 + var displayName = ""; 1.224 + 1.225 + // Language subtag will normally be 2 or 3 letters, but could be up to 8. 1.226 + try { 1.227 + displayName += gLanguageBundle.GetStringFromName(languageSubtag.toLowerCase()); 1.228 + } catch(e) { 1.229 + displayName += languageSubtag.toLowerCase(); // Fall back to raw language subtag. 1.230 + } 1.231 + 1.232 + // Region subtag will be 2 letters or 3 digits. 1.233 + if (regionSubtag) { 1.234 + displayName += " ("; 1.235 + 1.236 + try { 1.237 + displayName += gRegionBundle.GetStringFromName(regionSubtag.toLowerCase()); 1.238 + } catch(e) { 1.239 + displayName += regionSubtag.toUpperCase(); // Fall back to raw region subtag. 1.240 + } 1.241 + 1.242 + displayName += ")"; 1.243 + } 1.244 + 1.245 + // Script subtag will be 4 letters. 1.246 + if (scriptSubtag) { 1.247 + displayName += " / "; 1.248 + 1.249 + // XXX: See bug 666662 and bug 666731 for full implementation. 1.250 + displayName += scriptSubtag; // Fall back to raw script subtag. 1.251 + } 1.252 + 1.253 + // Each variant subtag will be 4 to 8 chars. 1.254 + if (variantSubtags) 1.255 + // XXX: See bug 666662 and bug 666731 for full implementation. 1.256 + displayName += " (" + variantSubtags.substr(1).split(/[-_]/).join(" / ") + ")"; // Collapse multiple variants. 1.257 + 1.258 + return displayName; 1.259 + }, 1.260 + 1.261 + // undoes the work of addDictionaryListToMenu for the menu 1.262 + // (call on popup hiding) 1.263 + clearDictionaryListFromMenu: function() 1.264 + { 1.265 + for (var i = 0; i < this.mDictionaryItems.length; i ++) { 1.266 + this.mDictionaryMenu.removeChild(this.mDictionaryItems[i]); 1.267 + } 1.268 + this.mDictionaryItems = []; 1.269 + }, 1.270 + 1.271 + // callback for selecting a dictionary 1.272 + selectDictionary: function(index) 1.273 + { 1.274 + if (! this.mInlineSpellChecker || index < 0 || index >= this.mDictionaryNames.length) 1.275 + return; 1.276 + var spellchecker = this.mInlineSpellChecker.spellChecker; 1.277 + spellchecker.SetCurrentDictionary(this.mDictionaryNames[index]); 1.278 + this.mInlineSpellChecker.spellCheckRange(null); // causes recheck 1.279 + }, 1.280 + 1.281 + // callback for selecting a suggesteed replacement 1.282 + replaceMisspelling: function(index) 1.283 + { 1.284 + if (! this.mInlineSpellChecker || ! this.mOverMisspelling) 1.285 + return; 1.286 + if (index < 0 || index >= this.mSpellSuggestions.length) 1.287 + return; 1.288 + this.mInlineSpellChecker.replaceWord(this.mWordNode, this.mWordOffset, 1.289 + this.mSpellSuggestions[index]); 1.290 + }, 1.291 + 1.292 + // callback for enabling or disabling spellchecking 1.293 + toggleEnabled: function() 1.294 + { 1.295 + this.mEditor.setSpellcheckUserOverride(!this.mInlineSpellChecker.enableRealTimeSpell); 1.296 + }, 1.297 + 1.298 + // callback for adding the current misspelling to the user-defined dictionary 1.299 + addToDictionary: function() 1.300 + { 1.301 + // Prevent the undo stack from growing over the max depth 1.302 + if (this.mAddedWordStack.length == MAX_UNDO_STACK_DEPTH) 1.303 + this.mAddedWordStack.shift(); 1.304 + 1.305 + this.mAddedWordStack.push(this.mMisspelling); 1.306 + this.mInlineSpellChecker.addWordToDictionary(this.mMisspelling); 1.307 + }, 1.308 + // callback for removing the last added word to the dictionary LIFO fashion 1.309 + undoAddToDictionary: function() 1.310 + { 1.311 + if (this.mAddedWordStack.length > 0) 1.312 + { 1.313 + var word = this.mAddedWordStack.pop(); 1.314 + this.mInlineSpellChecker.removeWordFromDictionary(word); 1.315 + } 1.316 + }, 1.317 + canUndo : function() 1.318 + { 1.319 + // Return true if we have words on the stack 1.320 + return (this.mAddedWordStack.length > 0); 1.321 + }, 1.322 + ignoreWord: function() 1.323 + { 1.324 + this.mInlineSpellChecker.ignoreWord(this.mMisspelling); 1.325 + } 1.326 +};