Bug 302050 inline spellchecking for text boxes r=bryner, sr=beng

This commit is contained in:
brettw%gmail.com 2005-12-05 18:18:11 +00:00
Родитель 97c9c6513a
Коммит 14b72727d1
12 изменённых файлов: 479 добавлений и 7 удалений

Просмотреть файл

@ -435,5 +435,9 @@ pref("bidi.browser.ui", false);
// 2 and other values, nothing
pref("browser.backspace_action", 0);
// this will automatically enable inline spellchecking (if it is available) for
// multi-line text entry controls <textarea>s in HTML
pref("layout.textarea.spellcheckDefault", true);
pref("view_source.editor.path", "");
pref("view_source.editor.external", false);

Просмотреть файл

@ -37,7 +37,30 @@
<popup id="contentAreaContextMenu"
onpopupshowing="if (event.target != this) return true; gContextMenu = new nsContextMenu( this ); return gContextMenu.shouldDisplay;"
onpopuphiding="if (event.target == this) gContextMenu = null;">
onpopuphiding="if (event.target == this) { gContextMenu.hiding(); gContextMenu = null; }">
<menuitem id="spell-no-suggestions"
disabled="true"
label="&spellNoSuggestions.label;"/>
<menuseparator id="spell-suggestions-separator"/>
<menuitem id="spell-add-to-dictionary"
label="&spellAddToDictionary.label;"
accesskey="&spellAddToDictionary.accesskey;"
oncommand="InlineSpellCheckerUI.addToDictionary();"/>
<menuitem id="spell-check-enabled"
label="&spellEnable.label;"
accesskey="&spellEnable.accesskey;"
oncommand="InlineSpellCheckerUI.toggleEnabled();"/>
<menu id="spell-dictionaries"
label="&spellDictionaries.label;"
accesskey="&spellDictionaries.accesskey;">
<menupopup id="spell-dictionaries-menu">
<menuseparator id="spell-language-separator"/>
<menuitem id="spell-add-dictionaries"
label="&spellAddDictionaries.label;"
accesskey="&spellAddDictionaries.accesskey;"/>
</menupopup>
</menu>
<menuseparator id="spell-separator"/>
<menuitem id="context-openlink"
label="&openLinkCmd.label;"
accesskey="&openLinkCmd.accesskey;"

Просмотреть файл

@ -8,6 +8,10 @@ toolbar[printpreview="true"] {
-moz-binding: url("chrome://global/content/printPreviewBindings.xml#printpreviewtoolbar");
}
menuitem.spell-suggestion {
font-weight:bold;
}
#noPreviewAvailable
{
background-color: white !important;

Просмотреть файл

@ -4075,7 +4075,7 @@ nsContextMenu.prototype = {
this.menu = popup;
// Get contextual info.
this.setTarget( document.popupNode );
this.setTarget( document.popupNode, document.popupEvent );
this.isTextSelected = this.isTextSelection();
this.isContentSelected = this.isContentSelection();
@ -4088,6 +4088,7 @@ nsContextMenu.prototype = {
this.initNavigationItems();
this.initViewItems();
this.initMiscItems();
this.initSpellingItems();
this.initSaveItems();
this.initClipboardItems();
this.initMetadataItems();
@ -4200,6 +4201,35 @@ nsContextMenu.prototype = {
// Only show the block image item if the image can be blocked
this.showItem( "context-blockimage", this.onImage && hostLabel);
},
initSpellingItems : function () {
var canSpell = InlineSpellCheckerUI.canSpellCheck;
var onMisspelling = InlineSpellCheckerUI.overMisspelling;
this.showItem("spell-check-enabled", canSpell);
this.showItem("spell-separator", canSpell);
if (canSpell)
document.getElementById("spell-check-enabled").setAttribute("checked",
InlineSpellCheckerUI.enabled);
this.showItem("spell-add-to-dictionary", onMisspelling);
// suggestion list
this.showItem("spell-suggestions-separator", onMisspelling);
if (onMisspelling) {
var menu = document.getElementById("contentAreaContextMenu");
var suggestionsSeparator = document.getElementById("spell-suggestions-separator");
var numsug = InlineSpellCheckerUI.addSuggestionsToMenu(menu, suggestionsSeparator, 5);
this.showItem("spell-no-suggestions", numsug == 0);
} else {
this.showItem("spell-no-suggestions", false);
}
// dictionary list
this.showItem("spell-dictionaries", InlineSpellCheckerUI.enabled);
if (canSpell) {
var dictMenu = document.getElementById("spell-dictionaries-menu");
var dictSep = document.getElementById("spell-language-separator");
InlineSpellCheckerUI.addDictionaryListToMenu(dictMenu, dictSep);
}
},
initClipboardItems : function () {
// Copy depends on whether there is selected text.
@ -4243,8 +4273,13 @@ nsContextMenu.prototype = {
// Show if user clicked on something which has metadata.
this.showItem( "context-metadata", this.onMetaDataItem );
},
// called when the menu is going away
hiding : function() {
InlineSpellCheckerUI.clearSuggestionsFromMenu();
InlineSpellCheckerUI.clearDictionaryListFromMenu();
},
// Set various context menu attributes based on the state of the world.
setTarget : function ( node ) {
setTarget : function ( node, event ) {
const xulNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
if ( node.namespaceURI == xulNS ) {
this.shouldDisplay = false;
@ -4267,6 +4302,7 @@ nsContextMenu.prototype = {
this.inFrame = false;
this.hasBGImage = false;
this.bgImageURL = "";
InlineSpellCheckerUI.uninit();
// Remember the node that was clicked.
this.target = node;
@ -4291,9 +4327,18 @@ nsContextMenu.prototype = {
this.onStandaloneImage = true;
} else if ( this.target instanceof HTMLInputElement ) {
this.onTextInput = this.isTargetATextBox(this.target);
// allow spellchecking UI on all writable text boxes except passwords
if (this.onTextInput && ! this.target.readOnly && this.target.type != "password") {
InlineSpellCheckerUI.init(this.target);
InlineSpellCheckerUI.initFromEvent(event);
}
this.onKeywordField = this.isTargetAKeywordField(this.target);
} else if ( this.target instanceof HTMLTextAreaElement ) {
this.onTextInput = true;
if (! this.target.readOnly) {
InlineSpellCheckerUI.init(this.target);
InlineSpellCheckerUI.initFromEvent(event);
}
} else if ( this.target instanceof HTMLHtmlElement ) {
// pages with multiple <body>s are lame. we'll teach them a lesson.
var bodyElt = this.target.ownerDocument.getElementsByTagName("body")[0];

Просмотреть файл

@ -46,4 +46,5 @@
<script type="application/x-javascript" src="chrome://browser/content/bookmarks/bookmarksMenu.js"/>
<script type="application/x-javascript" src="chrome://global/content/viewZoomOverlay.js"/>
<script type="application/x-javascript" src="chrome://browser/content/browser.js"/>
<script type="application/x-javascript" src="chrome://global/content/inlineSpellCheckUI.js"/>
<script type="application/x-javascript" src="chrome://global/content/viewSourceUtils.js"/>

Просмотреть файл

@ -357,3 +357,13 @@
<!ENTITY findAgainCmd.accesskey "g">
<!ENTITY findAgainCmd.commandkey "g">
<!ENTITY findAgainCmd.commandkey2 "VK_F3">
<!ENTITY spellAddToDictionary.label "Add to dictionary">
<!ENTITY spellAddToDictionary.accesskey "t">
<!ENTITY spellEnable.label "Spell check this field">
<!ENTITY spellEnable.accesskey "S">
<!ENTITY spellNoSuggestions.label "(No spelling suggestions)">
<!ENTITY spellDictionaries.label "Languages">
<!ENTITY spellDictionaries.accesskey "l">
<!ENTITY spellAddDictionaries.label "Add dictionaries...">
<!ENTITY spellAddDictionaries.accesskey "A">

Просмотреть файл

@ -0,0 +1,243 @@
# -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
# ***** 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 <brettw@gmail.com>
#
# 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 InlineSpellCheckerUI = {
// Call this function to initialize for a given edit element
init: function(inputElt)
{
this.uninit();
this.mEditor = inputElt.QueryInterface(Components.interfaces.nsIDOMNSEditableElement).editor;
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.mInlineSpellChecker = null;
this.mOverMisspelling = false;
this.mMisspelling = "";
this.mMenu = null;
this.mSpellSuggestions = []; // text of words
this.mSuggestionItems = []; // menuitem nodes
this.mDictionaryMenu = null;
this.mDictionaryNames = [];
this.mDictionaryItems = [];
},
// for each UI event, you must call this function, it will compute the
// word the cursor is over
initFromEvent: function(event)
{
this.mOverMisspelling = false;
this.mEvent = event;
if (! this.mInlineSpellChecker || ! (event instanceof UIEvent))
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.getMispelledWord(event.rangeParent,
event.rangeOffset);
if (! range)
return; // not over a misspelled word
this.mMisspelling = range.toString();
this.mOverMisspelling = true;
this.mWordNode = event.rangeParent;
this.mWordOffset = event.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.mInlineSpellChecker.enableRealTimeSpell = 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;
if (! spellchecker.CheckCurrentWord(this.mMisspelling))
return 0; // word seems not misspelled after all (?)
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 = document.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 (! 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 = spellchecker.GetCurrentDictionary();
for (var i = 0; i < list.length; i ++) {
this.mDictionaryNames.push(list[i]);
var item = document.createElement("menuitem");
item.setAttribute("label", list[i]);
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;
this.mInlineSpellChecker.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.mInlineSpellChecker.enableRealTimeSpell =
! this.mInlineSpellChecker.enableRealTimeSpell;
},
// callback for adding the current misspelling to the user-defined dictionary
addToDictionary: function()
{
this.mInlineSpellChecker.addWordToDictionary(this.mMisspelling);
}
};

Просмотреть файл

@ -26,6 +26,7 @@ toolkit.jar:
*+ content/global/fontpackage.js (fontpackage.js)
*+ content/global/fontpackage.xul (fontpackage.xul)
*+ content/global/globalOverlay.js (globalOverlay.js)
*+ content/global/inlineSpellCheckUI.js (inlineSpellCheckUI.js)
+ content/global/mozilla.xhtml (mozilla.xhtml)
*+ content/global/nsDragAndDrop.js (nsDragAndDrop.js)
+ content/global/nsTransferable.js (nsTransferable.js)

Просмотреть файл

@ -83,6 +83,23 @@
<property name="selectionEnd" onset="this.inputField.selectionEnd = val; return val;"
onget="return this.inputField.selectionEnd;"/>
<!-- use this property "spellCheckerUI" instead of "InlineSpellCheckUI",
because this object does the lazy creation and initialization
you need -->
<property name="spellCheckerUI" readonly="true">
<getter><![CDATA[
if (! this.InlineSpellCheckerUI) {
var loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"].
getService(Components.interfaces.mozIJSSubScriptLoader);
loader.loadSubScript("chrome://global/content/inlineSpellCheckUI.js", this);
// watch out - that could have failed
if (this.InlineSpellCheckerUI)
this.InlineSpellCheckerUI.init(this.inputField);
}
return this.InlineSpellCheckerUI;
]]></getter>
</property>
<method name="setSelectionRange">
<parameter name="aSelectionStart"/>
<parameter name="aSelectionEnd"/>
@ -91,6 +108,33 @@
</body>
</method>
<constructor><![CDATA[
var str = this.boxObject.getProperty("value");
if (str) {
this.inputField.value=str;
this.boxObject.removeProperty("value");
}
str = this.getAttribute("spellcheck");
if (str == "true") {
// the problem is that we can't initialize the spell checker in the
// constructor because not everything is initialized and the editor
// will fail to create the inline spell checker object.
setTimeout(this._delayedInitSpellCheck, 0, this)
}
]]></constructor>
<destructor action="if (this.inputField.value) this.boxObject.setProperty('value', this.inputField.value);"/>
<method name="_delayedInitSpellCheck">
<parameter name="me"/>
<body><![CDATA[
// called by the constructor to turn on spell checking
var spellui = me.spellCheckerUI;
if (spellui) {
spellui.enabled = true;
}
]]></body>
</method>
<constructor>
<![CDATA[
var str = this.boxObject.getProperty('value');
@ -216,8 +260,20 @@
<xul:menupopup anonid="input-box-contextmenu"
onpopupshowing="if (document.commandDispatcher.focusedElement != this.parentNode.firstChild)
this.parentNode.firstChild.focus();
this.parentNode.doPopupItemEnabling(this);"
oncommand="this.parentNode.doCommand(event.originalTarget.getAttribute('cmd'));event.preventBubble();">
this.parentNode._doPopupItemEnabling(this);"
onpopuphiding="this.parentNode._doPopupItemDisabling(this);"
oncommand="var cmd = event.originalTarget.getAttribute('cmd'); if(cmd) { this.parentNode.doCommand(cmd); event.preventBubble(); }">
<xul:menuitem label="&spellNoSuggestions.label;" anonid="spell-no-suggestions" disabled="true"/>
<xul:menuseparator anonid="spell-suggestions-separator"/>
<xul:menuitem label="&spellAddToDictionary.label;" accesskey="&spellAddToDictionary.accesskey;" anonid="spell-add-to-dictionary" oncommand="this.parentNode.parentNode.spellui.addToDictionary();"/>
<xul:menuitem label="&spellEnable.label;" accesskey="&spellEnable.accesskey;" anonid="spell-check-enabled" oncommand="this.parentNode.parentNode.spellui.toggleEnabled();"/>
<xul:menu label="&spellDictionaries.label;" accesskey="&spellDictionaries.accesskey;" anonid="spell-dictionaries">
<xul:menupopup anonid="spell-dictionaries-menu"
onpopupshowing="event.stopPropagation();"
onpopuphiding="event.stopPropagation();">
</xul:menupopup>
</xul:menu>
<xul:menuseparator anonid="spell-check-separator"/>
<xul:menuitem label="&undoCmd.label;" accesskey="&undoCmd.accesskey;" cmd="cmd_undo"/>
<xul:menuseparator/>
<xul:menuitem label="&cutCmd.label;" accesskey="&cutCmd.accesskey;" cmd="cmd_cut"/>
@ -230,7 +286,7 @@
</content>
<implementation>
<method name="doPopupItemEnabling">
<method name="_doPopupItemEnabling">
<parameter name="popupNode"/>
<body>
<![CDATA[
@ -246,9 +302,80 @@
children[i].setAttribute("disabled", "true");
}
}
// -- spell checking --
var textboxElt = popupNode;
do {
if (textboxElt.localName == "textbox")
break;
textboxElt = textboxElt.parentNode;
} while (textboxElt);
if (! textboxElt || textboxElt.localName != "textbox") {
// can't find it, give up
this._setNoSpellCheckingAllowed();
return;
}
var spellui = textboxElt.spellCheckerUI;
this.spellui = spellui;
if (! spellui.canSpellCheck)
{
this._setNoSpellCheckingAllowed();
return;
}
spellui.initFromEvent(document.popupEvent);
var enabled = spellui.enabled;
document.getAnonymousElementByAttribute(this, "anonid",
"spell-check-enabled").setAttribute("checked", enabled);
var overMisspelling = spellui.overMisspelling;
this._setMenuItemVisibility("spell-add-to-dictionary", overMisspelling);
this._setMenuItemVisibility("spell-suggestions-separator", overMisspelling);
// suggestion list
var spellSeparator = document.getAnonymousElementByAttribute(this,
"anonid", "spell-suggestions-separator");
var numsug = spellui.addSuggestionsToMenu(popupNode, spellSeparator, 5);
this._setMenuItemVisibility("spell-no-suggestions", overMisspelling && numsug == 0);
// dictionary list
var dictmenu = document.getAnonymousElementByAttribute(this, "anonid",
"spell-dictionaries-menu");
var numdicts = spellui.addDictionaryListToMenu(dictmenu, null);
this._setMenuItemVisibility("spell-dictionaries", enabled && numdicts > 1);
]]>
</body>
</method>
<method name="_doPopupItemDisabling">
<parameter name="popupNode"/>
<body><![CDATA[
if (this.spellui) {
this.spellui.clearSuggestionsFromMenu();
this.spellui.clearDictionaryListFromMenu();
}
]]></body>
</method>
<method name="_setMenuItemVisibility">
<parameter name="anonid"/>
<parameter name="visible"/>
<body><![CDATA[
document.getAnonymousElementByAttribute(this, "anonid", anonid).
hidden = ! visible;
]]></body>
</method>
<method name="_setNoSpellCheckingAllowed">
<body><![CDATA[
this._setMenuItemVisibility("spell-no-suggestions", false);
this._setMenuItemVisibility("spell-check-enabled", false);
this._setMenuItemVisibility("spell-check-separator", false);
this._setMenuItemVisibility("spell-add-to-dictionary", false);
this._setMenuItemVisibility("spell-suggestions-separator", false);
this._setMenuItemVisibility("spell-dictionaries", false);
]]></body>
</method>
<method name="doCommand">
<parameter name="command"/>

Просмотреть файл

@ -9,4 +9,11 @@
<!ENTITY selectAllCmd.label "Select All">
<!ENTITY selectAllCmd.accesskey "a">
<!ENTITY deleteCmd.label "Delete">
<!ENTITY deleteCmd.accesskey "d">
<!ENTITY deleteCmd.accesskey "d">
<!ENTITY spellAddToDictionary.label "Add to dictionary">
<!ENTITY spellAddToDictionary.accesskey "t">
<!ENTITY spellEnable.label "Spell check this field">
<!ENTITY spellEnable.accesskey "S">
<!ENTITY spellNoSuggestions.label "(No spelling suggestions)">
<!ENTITY spellDictionaries.label "Languages">
<!ENTITY spellDictionaries.accesskey "l">

Просмотреть файл

@ -81,6 +81,9 @@ menubar > menu[open] {
color: -moz-menuhovertext;
background-color: -moz-menuhover;
}
menuitem.spell-suggestion {
font-weight:bold;
}
/* ::::: menu/menuitems in menulist popups ::::: */

Просмотреть файл

@ -72,6 +72,10 @@ menuitem[_moz-menuactive="true"][disabled="true"] {
color: GrayText;
}
menuitem.spell-suggestion {
font-weight:bold;
}
/* ..... internal content .... */
.menu-accel,