dojo.provide("dojox.widget.SpellCheck");
dojo.require("dojox.widget.MenuGenerator");
dojo.requireLocalization("dojox.widget", "SpellCheck");
dojo.declare(
"dojox.widget.SpellCheck",
[dijit._Widget],
{
// summary
// A spellchecker for all king of textarea or richtexteditor
constructor: function(){
this._contextMenu = {};
this._handles = [];
this._nls = dojo.i18n.getLocalization(this.i18nModule, this.i18nBundle, this.lang);
},
//i18nModule: String
//Module for i18n
i18nModule: "dojox.widget",
//i18nBundle: String
//Bundle for i18n
i18nBundle: "SpellCheck",
//aspell: String
//file used (in xhr mode to) for calling aspell
aspell: "spellcheck.php",
//encoding: String
//encoding used in communication, with aspell
encoding: "utf-8",
//language: String
//language to use in aspell
language: "fr",
//leftClickToOpen: Boolean
//false if you want to open contextual menu with right click
leftClickToOpen: true,
//incorrectWordStyle: String
//style applyed on incorrect word
//Why style ? because for class we need to declare some CSS file... to bad...
incorrectWordStyle: "cursor:pointer;border-bottom:1px dotted red;background-color:yellow",
//editor: Object
//Specify wich editor instance use
editor: null,
//innerDocument: Object
//document root in iframe editor widget
innerDocument: null,
//textarea: DomNode
//textarea node for rich editor in simple mode
textarea: null,
//maxSuggest: Integer
//Set the max suggestion available in contextual menu.
//Set to 0 for unlimited contextual menu
maxSuggest: 10,
//contextualMenuCoordsCorrection: Object
//properties are:
//l:some px to add to left menu coords
//t:some px to add to top menu coords
contextualMenuCoordsCorrection: null,
//_activeWord: DomNode
//Store node wich is in correction
_activeWord: null,
//_contextMenu: Object
//Contextual menu
_contextMenu: null,
//_handles: Object
//Store all conexion
_handles: null,
//_useTextarea: Boolean
//true when textarea is used instead of rich editor
_useTextarea: false,
//_divArea: DomNode
//Div put behind the textarea
_divArea: null,
//isInProgress: Boolean
//true when spellchecker is active
isInProgress: false,
//useXYPosition: Boolean
//when true, add coords.x and coords.y to top and left of the textarea
//needed sometime but why ? (maybe in position:relative elements ?)
useXYPosition: false,
//_canStop: Boolean
//true when spellchecker can be stopped
_canStop: false,
_getSpellContent: function(){
// summary: return spell content (including special add for correction)
if(this._useTextarea){
var reg = new RegExp("
","gi");
return this._divArea.innerHTML.replace(reg,"\n");
}else{
return this.getContent();
}
},
_finalizeStop: function(){
// summary: Finalize the stop of spellcheck (if needed)
if(this._useTextarea){
this.textarea.value = this._getSpellContent();
this.textarea.style.visibility = "visible";
dojo._destroyElement(this._divArea);
this._divArea = null;
}
},
_initializeStart: function(){
// summary: Initialize the start of spellcheck (if needed)
if(this._useTextarea){
this._divArea = dojo.doc.createElement("div");
this._divArea.className = this.textarea.className + " spellChecker";
this._divArea.style.position = "absolute";
var coords = dojo.coords(this.textarea);
//FIXME : why sometime we need x and y sum ?
if(this.useXYPosition){
coords.l += coords.x;
coords.t += coords.y;
}
dojo.marginBox(this._divArea,coords);
this.textarea.style.visibility = "hidden";
dojo.body().appendChild(this._divArea);
}
},
stop: function(){
// summary: Stop the spell checker
if(!this.isInProgress){
return false;
}
if(!this._canStop){
return false;
}
this._canStop = false;
this._destroySpellItem();
//we use a temporary div because IE does not allow replaceChild in iframe document
var tmp = dojo.doc.createElement("div");
tmp.innerHTML = this._getSpellContent();
//remove all spell node
var rm = this._query("spellCheck",tmp);
rm.forEach(function(obj){
var fragment = dojo.doc.createTextNode(obj.innerHTML);
obj.parentNode.replaceChild(fragment, obj);
fragment = null;
},this);
rm = null;
this._setContent(tmp.innerHTML);
tmp = null;
this._finalizeStop();
this.isInProgress = false;
return true;
},
start: function(/*Boolean*/useTextarea){
// summary: Start the spell checker
if(this.isInProgress){
return false;
}
this.isInProgress = true;
this._useTextarea = useTextarea || false;
this._initializeStart();
var content = this._getContent();
dojo.xhrPost({
url : this.aspell,
encoding : this.encoding,
handleAs : "json",
content : {
words : content,
lang : this.language
},
handle : dojo.hitch(this,this._addSpelling)
});
return true;
},
_getContent: function(){
// summary: Internal method, choose between getContent in richtext and getContent in textarea
var value = "";
if(this._useTextarea){
value = this.textarea.value.replace(/\n|\r/g,"
");
}else{
value = this.getContent().replace(/\n|\r/g,"");
}
return value;
},
_setContent: function(/*String*/content){
// summary: Internal method, choose between setContent in richtext and setContent in textarea
if(this._useTextarea){
this._divArea.innerHTML = content;
}else{
this.setContent(content);
}
},
_addSpelling: function(data){
// summary: add all element used to show the correction
var content = this._getContent();
var decalage = 0;
var span = '${mot}';
var nbMenu = 0;
if(!this._useTextarea){
var editorCoords = dojo.coords(this.editor.domNode);
if(this.contextualMenuCoordsCorrection != null && typeof this.contextualMenuCoordsCorrection.l != "undefined"){
editorCoords.x += this.contextualMenuCoordsCorrection.l;
}
if(this.contextualMenuCoordsCorrection != null && typeof this.contextualMenuCoordsCorrection.t != "undefined"){
editorCoords.y += this.contextualMenuCoordsCorrection.t;
}
}
for(var i in data.response){
var off = decalage + data.response[i]['offset'] + 1;
var mot = data.response[i]['mot'];
var len = data.response[i]['length'];
var sp = dojo.string.substitute(span, { mot: mot });
var tmp = [content.substr(0,off),
sp,
content.substr(off + len)].join("");
content = tmp;
decalage += sp.length - len;
if(typeof this._contextMenu[mot] == "undefined"){
var menu = [];
var countSuggest = 0;
for(var j in data.response[i]['suggestion']){
menu.push({
id : this.id + "menu" + nbMenu,
onClick : dojo.hitch(this,this._onItemClick),
label : data.response[i]['suggestion'][j]
});
nbMenu++;
countSuggest++;
if(this.maxSuggest > 0 && this.maxSuggest <= countSuggest){
break;
}
}
menu.push({
id : this.id + "separator" + nbMenu,
label : "-"
});
menu.push({
id : this.id + "ignore" + nbMenu,
label : this._nls.ignore,
onClick : dojo.hitch(this,this.ignore)
});
menu.push({
id : this.id + "ignoreall" + nbMenu,
label : this._nls.ignoreAll,
onClick : dojo.hitch(this,this.ignoreAll)
});
this._contextMenu[mot] = new dojox.widget.MenuGenerator({
menuClass : "dojox.widget.SpellCheckContextMenu",
id : this.id + "popmenu" + mot,
menuEntries : menu,
leftClickToOpen : this.leftClickToOpen
});
this._contextMenu[mot].startup();
if(!this._useTextarea){
this._contextMenu[mot].getMenu().coordsPadding = {
x : editorCoords.x,
y : editorCoords.y
};
}
}
}
this._setContent(content);
for(var i in this._contextMenu){
var nl = this._query(i);
nl.forEach(function(obj){
this._contextMenu[i].bindDomNode(obj);
this._handles.push(dojo.connect(obj,"onmousedown",this,"_memorizeNode"));
},this);
nl = null;
}
this._canStop = true;
},
_query: function(/*string*/className,/*Optional, DomNode*/root,/*String,optional*/tag){
// summary: We cannot use dojo.query in firefox 3 (security restriction with iframe)
// so we do our query method
tag = tag || "span";
var nodes = new dojo.NodeList();
if(typeof root == "undefined" || root == null){
if(this._useTextarea){
root = this._divArea;
}else{
root = this.innerDocument;
}
}
var nl = root.getElementsByTagName(tag);
if(nl.length > 0){
for(var j in nl){
if(typeof nl[j] == "undefined" || typeof nl[j].nodeType == "undefined" || 1 != nl[j].nodeType){
continue;
}
var cls = nl[j].className.split(" ");
if(dojo.indexOf(cls,className) != -1){
nodes.push(nl[j]);
}
}
}
nl = null;
try{
return nodes;
}finally{
nodes = null;
}
},
_memorizeNode: function(e){
// summary: Store current word
e = dojo.fixEvent(e);
this._activeWord = e.target;
},
_onItemClick: function(e){
// summary: Change mispelled word by choosen
e = dojo.fixEvent(e);
//find the td with content, not td clicked
var father = e.target.parentNode;
var target = this._query("dijitMenuItemLabel",father,"td")[0];
this._activeWord.innerHTML = target.innerHTML;
this._unBind(this._activeWord);
father = null;
target = null;
},
ignore: function(e){
// summary: ignore a word
e = dojo.fixEvent(e);
this._unBind(this._activeWord);
},
ignoreAll: function(e){
// summary: ignore all word
e = dojo.fixEvent(e);
var rm = this._query(this._activeWord.getAttribute("word"));
this._unBindAll(rm);
},
_unBindAll: function(nodes){
// summary: remove events on all incorrect word
nodes.forEach(function(obj){
this._unBind(obj);
},this);
},
_unBind: function(node){
// summary: remove events on incorrect word
if(dojo.isIE){
node.style.cssText = "";
}else{
node.setAttribute("style","");
}
this._contextMenu[node.getAttribute("word")].unBindDomNode(node);
},
_destroySpellItem: function(){
// summary: destroy all item added by spellchecker
for(var i in this._handles){
dojo.disconnect(this._handles[i]);
}
for(var i in this._contextMenu){
if(typeof i != "string" || i == ""){
continue;
}
var rm = this._query(i);
this._unBindAll(rm);
this._contextMenu[i].destroy();
}
this._contextMenu = {};
},
destroy: function(){
// summary: destroy spellchecker
this._destroySpellItem();
this._activeWord = null;
this._divArea = null;
this._contextMenu = null;
this._handles = null;
this.inherited("destroy",arguments);
},
/**************************************************************************************
* Overwrited method
**************************************************************************************/
setContent: function(value){
// summary: Overwrited method, refer to the internal method of editor to set content
},
getContent: function(){
// summary: Overwrited method, refer to the internal method of editor to get content
}
});
dojo.declare(
"dojox.widget.SpellCheckContextMenu",
[dijit.Menu],
{
// summary
// Contextual menu for spellchecker
//coordsPadding: Object
//Used to positionning context menu
coordsPadding: null,
_openMyself: function(/*Event*/ e){
// summary:
// Internal function for opening myself when the user
// does a right-click or something similar
e = dojo.fixEvent(e);
this.opener = e.target;
if(this.leftClickToOpen && e.button > 0){
return;
}
dojo.stopEvent(e);
// Get coordinates.
// if we are opening the menu with the mouse or on safari open
// the menu at the mouse cursor
// (Safari does not have a keyboard command to open the context menu
// and we don't currently have a reliable way to determine
// _contextMenuWithMouse on Safari)
var x,y;
if(dojo.isSafari || this._contextMenuWithMouse){
x = e.pageX;
y = e.pageY;
}else{
// otherwise open near e.target
var coords = dojo.coords(e.target, true);
x = coords.x + 10;
y = coords.y + 10;
}
if(this.coordsPadding != null && typeof this.coordsPadding.x != "undefined"){
x += this.coordsPadding.x;
}
if(this.coordsPadding != null && typeof this.coordsPadding.y != "undefined"){
y += this.coordsPadding.y;
}
var self = this;
var savedFocus = dijit.getFocus(this);
function closeAndRestoreFocus(){
// user has clicked on a menu or popup
dijit.focus(savedFocus);
dijit.popup.close(self);
}
dijit.popup.open({
popup : this,
x : x,
y : y,
onExecute : closeAndRestoreFocus,
onCancel : closeAndRestoreFocus,
orient : this.isLeftToRight() ? 'L' : 'R'
});
this.focus();
this._onBlur = function(){
this.inherited('_onBlur', arguments);
// Usually the parent closes the child widget but if this is a context
// menu then there is no parent
dijit.popup.close(this);
// don't try to restore focus; user has clicked another part of the screen
// and set focus there
};
if(dojo.isSafari){
this ._blurHandler = dojo.connect(dojo.doc, "onclick", this, "_onBlur");
}
}
});