source: Dev/trunk/src/client/dojox/editor/plugins/SpellCheck.js

Last change on this file was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 40.9 KB
Line 
1define([
2        "dojo",
3        "dijit",
4        "dojo/io/script",
5        "dijit/popup",
6        "dijit/_Widget",
7        "dijit/_Templated",
8        "dijit/_editor/_Plugin",
9        "dijit/form/TextBox",
10        "dijit/form/DropDownButton",
11        "dijit/TooltipDialog",
12        "dijit/form/MultiSelect",
13        "dijit/Menu",
14        "dojo/i18n!dojox/editor/plugins/nls/SpellCheck"
15], function(dojo, dijit, script, popup, _Widget, _Templated, _Plugin){
16
17dojo.experimental("dojox.editor.plugins.SpellCheck");
18
19var SpellCheckControl = dojo.declare("dojox.editor.plugins._spellCheckControl", [_Widget, _Templated], {
20        // summary:
21        //              The widget that is used for the UI of the batch spelling check
22
23        widgetsInTemplate: true,
24
25        templateString:
26                "<table role='presentation' class='dijitEditorSpellCheckTable'>" +
27                        "<tr><td colspan='3' class='alignBottom'><label for='${textId}' id='${textId}_label'>${unfound}</label>" +
28                                "<div class='dijitEditorSpellCheckBusyIcon' id='${id}_progressIcon'></div></td></tr>" +
29                        "<tr>" +
30                                "<td class='dijitEditorSpellCheckBox'><input dojoType='dijit.form.TextBox' required='false' intermediateChanges='true' " +
31                                        "class='dijitEditorSpellCheckBox' dojoAttachPoint='unfoundTextBox' id='${textId}'/></td>" +
32                                "<td><button dojoType='dijit.form.Button' class='blockButton' dojoAttachPoint='skipButton'>${skip}</button></td>" +
33                                "<td><button dojoType='dijit.form.Button' class='blockButton' dojoAttachPoint='skipAllButton'>${skipAll}</button></td>" +
34                        "</tr>" +
35                        "<tr>" +
36                                "<td class='alignBottom'><label for='${selectId}'>${suggestions}</td></label>" +
37                                "<td colspan='2'><button dojoType='dijit.form.Button' class='blockButton' dojoAttachPoint='toDicButton'>${toDic}</button></td>" +
38                        "</tr>" +
39                        "<tr>" +
40                                "<td>" +
41                                        "<select dojoType='dijit.form.MultiSelect' id='${selectId}' " +
42                                                "class='dijitEditorSpellCheckBox listHeight' dojoAttachPoint='suggestionSelect'></select>" +
43                                "</td>" +
44                                "<td colspan='2'>" +
45                                        "<button dojoType='dijit.form.Button' class='blockButton' dojoAttachPoint='replaceButton'>${replace}</button>" +
46                                        "<div class='topMargin'><button dojoType='dijit.form.Button' class='blockButton' " +
47                                                "dojoAttachPoint='replaceAllButton'>${replaceAll}</button><div>" +
48                                "</td>" +
49                        "</tr>" +
50                        "<tr>" +
51                                "<td><div class='topMargin'><button dojoType='dijit.form.Button' dojoAttachPoint='cancelButton'>${cancel}</button></div></td>" +
52                                "<td></td>" +
53                                "<td></td>" +
54                        "</tr>" +
55                "</table>",
56
57        /*************************************************************************/
58        /**                      Framework Methods                              **/
59        /*************************************************************************/
60        constructor: function(){
61                // Indicate if the textbox ignores the text change event of the textbox
62                this.ignoreChange = false;
63                // Indicate if the text of the textbox is changed or not
64                this.isChanged = false;
65                // Indicate if the dialog is open or not
66                this.isOpen = false;
67                // Indicate if the dialog can be closed
68                this.closable = true;
69        },
70
71        postMixInProperties: function(){
72                this.id = dijit.getUniqueId(this.declaredClass.replace(/\./g,"_"));
73                this.textId = this.id + "_textBox";
74                this.selectId = this.id + "_select";
75        },
76
77        postCreate: function(){
78                var select = this.suggestionSelect;
79
80                // Customize multi-select to single select
81                dojo.removeAttr(select.domNode, "multiple");
82                select.addItems = function(/*Array*/ items){
83                        // summary:
84                        //              Add items to the select widget
85                        // items:
86                        //              An array of items be added to the select
87                        // tags:
88                        //              public
89                        var _this = this;
90                        var o = null;
91                        if(items && items.length > 0){
92                                dojo.forEach(items, function(item, i){
93                                        o = dojo.create("option", {innerHTML: item, value: item}, _this.domNode);
94                                        if(i == 0){
95                                                o.selected = true;
96                                        }
97                                });
98                        }
99                };
100                select.removeItems = function(){
101                        // summary:
102                        //              Remove all the items within the select widget
103                        // tags:
104                        //              public
105                        dojo.empty(this.domNode);
106                };
107
108                select.deselectAll = function(){
109                        // summary:
110                        //              De-select all the selected items
111                        // tags:
112                        //              public
113                        this.containerNode.selectedIndex = -1;
114                };
115
116                // Connect up all the controls with their event handler
117                this.connect(this, "onKeyPress", "_cancel");
118                this.connect(this.unfoundTextBox, "onKeyPress", "_enter");
119                this.connect(this.unfoundTextBox, "onChange", "_unfoundTextBoxChange");
120                this.connect(this.suggestionSelect, "onKeyPress", "_enter");
121                this.connect(this.skipButton, "onClick", "onSkip");
122                this.connect(this.skipAllButton, "onClick", "onSkipAll");
123                this.connect(this.toDicButton, "onClick", "onAddToDic");
124                this.connect(this.replaceButton, "onClick", "onReplace");
125                this.connect(this.replaceAllButton, "onClick", "onReplaceAll");
126                this.connect(this.cancelButton, "onClick", "onCancel");
127        },
128
129        /*************************************************************************/
130        /**                      Public Methods                                 **/
131        /*************************************************************************/
132
133        onSkip: function(){
134                // Stub for the click event of the skip button.
135        },
136
137        onSkipAll: function(){
138                // Stub for the click event of the skipAll button.
139        },
140
141        onAddToDic: function(){
142                // Stub for the click event of the toDic button.
143        },
144
145        onReplace: function(){
146                // Stub for the click event of the replace button.
147        },
148
149        onReplaceAll: function(){
150                // Stub for the click event of the replaceAll button.
151        },
152
153        onCancel: function(){
154                // Stub for the click event of the cancel button.
155        },
156
157        onEnter: function(){
158                // Stub for the enter event of the unFound textbox.
159        },
160
161        focus: function(){
162                // summary:
163                //              Set the focus of the control
164                // tags:
165                //              public
166                this.unfoundTextBox.focus();
167        },
168
169        /*************************************************************************/
170        /**                      Private Methods                                **/
171        /*************************************************************************/
172
173        _cancel: function(/*Event*/ evt){
174                // summary:
175                //              Handle the cancel event
176                // evt:
177                //              The event object
178                // tags:
179                //              private
180                if(evt.keyCode == dojo.keys.ESCAPE){
181                        this.onCancel();
182                        dojo.stopEvent(evt);
183                }
184        },
185
186        _enter: function(/*Event*/ evt){
187                // summary:
188                //              Handle the enter event
189                // evt:
190                //              The event object
191                // tags:
192                //              private
193                if(evt.keyCode == dojo.keys.ENTER){
194                        this.onEnter();
195                        dojo.stopEvent(evt);
196                }
197        },
198
199        _unfoundTextBoxChange: function(){
200                // summary:
201                //              Indicate that the Not Found textbox is changed or not
202                // tags:
203                //              private
204                var id = this.textId + "_label";
205                if(!this.ignoreChange){
206                        dojo.byId(id).innerHTML = this["replaceWith"];
207                        this.isChanged = true;
208                        this.suggestionSelect.deselectAll();
209                }else{
210                        dojo.byId(id).innerHTML = this["unfound"];
211                }
212        },
213
214        _setUnfoundWordAttr: function(/*String*/ value){
215                // summary:
216                //              Set the value of the Not Found textbox
217                // value:
218                //              The value of the Not Found textbox
219                // tags:
220                //              private
221                value = value || "";
222                this.unfoundTextBox.set("value", value);
223        },
224
225        _getUnfoundWordAttr: function(){
226                // summary:
227                //              Get the value of the Not Found textbox
228                // tags:
229                //              private
230                return this.unfoundTextBox.get("value");
231        },
232
233        _setSuggestionListAttr: function(/*Array*/ values){
234                // summary:
235                //              Set the items of the suggestion list
236                // values:
237                //              The list of the suggestion items
238                // tags:
239                //              private
240                var select = this.suggestionSelect;
241                values = values || [];
242                select.removeItems();
243                select.addItems(values);
244        },
245
246        _getSelectedWordAttr: function(){
247                // summary:
248                //              Get the suggested word.
249                //              If the select box is selected, the value is the selected item's value,
250                //              else the value the the textbox's value
251                // tags:
252                //              private
253                var selected = this.suggestionSelect.getSelected();
254                if(selected && selected.length > 0){
255                        return selected[0].value;
256                }else{
257                        return this.unfoundTextBox.get("value");
258                }
259        },
260
261        _setDisabledAttr: function(/*Boolean*/ disabled){
262                // summary:
263                //              Enable/disable the control
264                // tags:
265                //              private
266                this.skipButton.set("disabled", disabled);
267                this.skipAllButton.set("disabled", disabled);
268                this.toDicButton.set("disabled", disabled);
269                this.replaceButton.set("disabled", disabled);
270                this.replaceAllButton.set("disabled", disabled);
271        },
272
273        _setInProgressAttr: function(/*Boolean*/ show){
274                // summary:
275                //              Set the visibility of the progress icon
276                // tags:
277                //              private
278                var id = this.id + "_progressIcon";
279                dojo.toggleClass(id, "hidden", !show);
280        }
281});
282
283var SpellCheckScriptMultiPart = dojo.declare("dojox.editor.plugins._SpellCheckScriptMultiPart", null, {
284        // summary:
285        //              It is a base network service component. It transfers text to a remote service port
286        //              with cross domain ability enabled. It can split text into specified pieces and send
287        //              them out one by one so that it can handle the case when the service has a limitation of
288        //              the capability.
289        //              The encoding is UTF-8.
290
291        // ACTION [public const] String
292        //              Actions for the server-side piece to take
293        ACTION_QUERY: "query",
294        ACTION_UPDATE: "update",
295
296        // callbackHandle [public] String
297        //              The callback name of JSONP
298        callbackHandle: "callback",
299
300        // maxBufferLength [public] Number
301        //              The max number of characters that send to the service at one time.
302        maxBufferLength: 100,
303
304        // delimiter [public] String
305        //              A token that is used to identify the end of a word (a complete unit). It prevents the service from
306        //              cutting a single word into two parts. For example:
307        // |            "Dojo toolkit is a ajax framework. It helps the developers buid their web applications."
308        //              Without the delimiter, the sentence might be split into the follow pieces which is absolutely
309        //              not the result we want.
310        // |            "Dojo toolkit is a ajax fram", "ework It helps the developers bu", "id their web applications"
311        //              Having " " as the delimiter, we get the following correct pieces.
312        // |            "Dojo toolkit is a ajax framework", " It helps the developers buid", " their web applications"
313        delimiter: " ",
314
315        // label [public] String
316        //              The leading label of the JSON response. The service will return the result like this:
317        // |    {response: [
318        // |            {
319        // |                    text: "teest",
320        // |                    suggestion: ["test","treat"]
321        // |            }
322        // |    ]}
323        label: "response",
324
325        // _timeout: [private] Number
326        //              Set JSONP timeout period
327        _timeout: 30000,
328        SEC: 1000,
329
330        constructor: function(){
331                // The URL of the target service
332                this.serviceEndPoint = "";
333                // The queue that holds all the xhr request
334                this._queue = [];
335                // Indicate if the component is still working. For example, waiting for collecting all
336                // the responses from the service
337                this.isWorking = false;
338                // The extra command passed to the service
339                this.exArgs = null;
340                // The counter that indicate if all the responses are collected to
341                // assemble the final result.
342                this._counter = 0;
343        },
344
345        send: function(/*String*/ content, /*String?*/ action){
346                // summary:
347                //              Send the content to the service port with the specified action
348                // content:
349                //              The text to be sent
350                // action:
351                //              The action the service should take. Current support actions are
352                //              ACTION_QUERY and ACTION_UPDATE
353                // tags:
354                //              public
355                var _this = this,
356                        dt = this.delimiter,
357                        mbl = this.maxBufferLength,
358                        label = this.label,
359                        serviceEndPoint = this.serviceEndPoint,
360                        callbackParamName = this.callbackHandle,
361                        comms = this.exArgs,
362                        timeout = this._timeout,
363                        l = 0, r = 0;
364
365                // Temporary list that holds the result returns from the service, which will be
366                // assembled into a completed one.
367                if(!this._result) {
368                        this._result = [];
369                }
370
371                action = action || this.ACTION_QUERY;
372
373                var batchSend = function(){
374                        var plan = [];
375                        var plannedSize = 0;
376                        if(content && content.length > 0){
377                                _this.isWorking = true;
378                                var len = content.length;
379                                do{
380                                        l = r + 1;
381                                        if((r += mbl) > len){
382                                                r = len;
383                                        }else{
384                                                // If there is no delimiter (emplty string), leave the right boundary where it is.
385                                                // Else extend the right boundary to the first occurance of the delimiter if
386                                                // it doesn't meet the end of the content.
387                                                while(dt && content.charAt(r) != dt && r <= len){
388                                                        r++;
389                                                }
390                                        }
391                                        // Record the information of the text slices
392                                        plan.push({l: l, r: r});
393                                        plannedSize++;
394                                }while(r < len);
395
396                                dojo.forEach(plan, function(item, index){
397                                        var jsonpArgs = {
398                                                url: serviceEndPoint,
399                                                action: action,
400                                                timeout: timeout,
401                                                callbackParamName: callbackParamName,
402                                                handle: function(response, ioArgs){
403                                                        if(++_this._counter <= this.size && !(response instanceof Error) &&
404                                                                response[label] && dojo.isArray(response[label])){
405                                                                // Collect the results
406                                                                var offset = this.offset;
407                                                                dojo.forEach(response[label], function(item){
408                                                                        item.offset += offset;
409                                                                });
410                                                                // Put the packages in order
411                                                                _this._result[this.number]= response[label];
412                                                        }
413                                                        if(_this._counter == this.size){
414                                                                _this._finalizeCollection(this.action);
415                                                                _this.isWorking = false;
416                                                                if(_this._queue.length > 0){
417                                                                        // Call the next request waiting in queue
418                                                                        (_this._queue.shift())();
419                                                                }
420                                                        }
421                                                }
422                                        };
423                                        jsonpArgs.content = comms ? dojo.mixin(comms, {action: action, content: content.substring(item.l - 1, item.r)}):
424                                                                                                        {action: action, content: content.substring(item.l - 1, item.r)};
425                                        jsonpArgs.size = plannedSize;
426                                        jsonpArgs.number = index; // The index of the current package
427                                        jsonpArgs.offset = item.l - 1;
428                                        dojo.io.script.get(jsonpArgs);
429                                });
430                        }
431                };
432
433                if(!_this.isWorking){
434                        batchSend();
435                }else{
436                        _this._queue.push(batchSend);
437                }
438        },
439
440        _finalizeCollection: function(action){
441                // summary:
442                //              Assemble the responses into one result.
443                // action:
444                //              The action token
445                // tags:
446                //              private
447                var result = this._result,
448                        len = result.length;
449                // Turn the result into a one-dimensional array
450                for(var i = 0; i < len; i++){
451                        var temp = result.shift();
452                        result = result.concat(temp);
453                }
454                if(action == this.ACTION_QUERY){
455                        this.onLoad(result);
456                }
457                this._counter = 0;
458                this._result = [];
459        },
460
461        onLoad: function(/*String*/ data){
462                // Stub method for a sucessful call
463        },
464
465        setWaitingTime: function(/*Number*/ seconds){
466                this._timeout = seconds * this.SEC;
467        }
468});
469
470var SpellCheck = dojo.declare("dojox.editor.plugins.SpellCheck", [_Plugin], {
471        // summary:
472        //              This plugin provides a spelling check capability for the editor.
473
474        // url: [public] String
475        //              The url of the spelling check service
476        url: "",
477
478        // bufferLength: [public] Number
479        //              The max length of each XHR request. It is used to divide the large
480        //              text into pieces so that the server-side piece can hold.
481        bufferLength: 100,
482
483        // interactive: [public] Boolean
484        //              Indicate if the interactive spelling check is enabled
485        interactive: false,
486
487        // timeout: [public] Number
488        //              The minutes to waiting for the response. The default value is 30 seconds.
489        timeout: 30,
490
491        // button: [protected] dijit/form/DropDownButton
492        //              The button displayed on the editor's toolbar
493        button: null,
494
495        // _editor: [private] dijit/Editor
496        //              The reference to the editor the plug-in belongs to.
497        _editor: null,
498
499        // exArgs: [private] Object
500        //              The object that holds all the parametes passed into the constructor
501        exArgs: null,
502
503        // _cursorSpan: [private] String
504        //              The span that holds the current position of the cursor
505        _cursorSpan:
506                "<span class=\"cursorPlaceHolder\"></span>",
507
508        // _cursorSelector: [private] String
509        //              The CSS selector of the cursor span
510        _cursorSelector:
511                "cursorPlaceHolder",
512
513        // _incorrectWordsSpan: [private] String
514        //              The wrapper that marks the incorrect words
515        _incorrectWordsSpan:
516                "<span class='incorrectWordPlaceHolder'>${text}</span>",
517
518        // _ignoredIncorrectStyle: [private] Object
519        //              The style of the ignored incorrect words
520        _ignoredIncorrectStyle:
521                {"cursor": "inherit", "borderBottom": "none", "backgroundColor": "transparent"},
522
523        // _normalIncorrectStyle: [private] Object
524        //              The style of the marked incorrect words.
525        _normalIncorrectStyle:
526                {"cursor": "pointer", "borderBottom": "1px dotted red", "backgroundColor": "yellow"},
527
528        // _highlightedIncorrectStyle: [private] Object
529        //              The style of the highlighted incorrect words
530        _highlightedIncorrectStyle:
531                {"borderBottom": "1px dotted red", "backgroundColor": "#b3b3ff"},
532
533        // _selector: [private] String
534        //              An empty CSS class that identifies the incorrect words
535        _selector: "incorrectWordPlaceHolder",
536
537        // _maxItemNumber: [private] Number
538        //              The max number of the suggestion list items
539        _maxItemNumber: 3,
540
541        /*************************************************************************/
542        /**                      Framework Methods                              **/
543        /*************************************************************************/
544
545        constructor: function(){
546                // A list that holds all the spans that contains the incorrect words
547                // It is used to select/replace the specified word.
548                this._spanList = [];
549                // The cache that stores all the words. It looks like the following
550                // {
551                //       "word": [],
552                //       "wrd": ["word", "world"]
553                // }
554                this._cache = {};
555                // Indicate if this plugin is enabled or not
556                this._enabled = true;
557                // The index of the _spanList
558                this._iterator = 0;
559        },
560
561        setEditor: function(/*dijit.Editor*/ editor){
562                this._editor = editor;
563                this._initButton();
564                this._setNetwork();
565                this._connectUp();
566        },
567
568        /*************************************************************************/
569        /**                      Private Methods                                **/
570        /*************************************************************************/
571
572        _initButton: function(){
573                // summary:
574                //              Initialize the button displayed on the editor's toolbar
575                // tags:
576                //              private
577                var _this = this,
578                        strings = (this._strings = dojo.i18n.getLocalization("dojox.editor.plugins", "SpellCheck")),
579                        dialogPane = (this._dialog = new dijit.TooltipDialog());
580
581                dialogPane.set("content", (this._dialogContent = new SpellCheckControl({
582                        unfound: strings["unfound"],
583                        skip: strings["skip"],
584                        skipAll: strings["skipAll"],
585                        toDic: strings["toDic"],
586                        suggestions: strings["suggestions"],
587                        replaceWith: strings["replaceWith"],
588                        replace: strings["replace"],
589                        replaceAll: strings["replaceAll"],
590                        cancel: strings["cancel"]
591                })));
592
593                this.button = new dijit.form.DropDownButton({
594                        label: strings["widgetLabel"],
595                        showLabel: false,
596                        iconClass: "dijitEditorSpellCheckIcon",
597                        dropDown: dialogPane,
598                        id: dijit.getUniqueId(this.declaredClass.replace(/\./g,"_")) + "_dialogPane",
599                        closeDropDown: function(focus){
600                                // Determine if the dialog can be closed
601                                if(_this._dialogContent.closable){
602                                        _this._dialogContent.isOpen = false;
603                                        if(dojo.isIE){
604                                                var pos = _this._iterator,
605                                                        list = _this._spanList;
606                                                if(pos < list.length && pos >=0 ){
607                                                        dojo.style(list[pos], _this._normalIncorrectStyle);
608                                                }
609                                        }
610                                        if(this._opened){
611                                                popup.close(this.dropDown);
612                                                if(focus){ this.focus(); }
613                                                this._opened = false;
614                                                this.state = "";
615                                        }
616                                }
617                        }
618                });
619                _this._dialogContent.isOpen = false;
620
621                dialogPane.domNode.setAttribute("aria-label", this._strings["widgetLabel"]);
622        },
623
624        _setNetwork: function(){
625                // summary:
626                //              Set up the underlying network service
627                // tags:
628                //              private
629                var comms = this.exArgs;
630
631                if(!this._service){
632                        var service = (this._service = new SpellCheckScriptMultiPart());
633                        service.serviceEndPoint = this.url;
634                        service.maxBufferLength = this.bufferLength;
635                        service.setWaitingTime(this.timeout);
636                        // Pass the other arguments directly to the service
637                        if(comms){
638                                delete comms.name;
639                                delete comms.url;
640                                delete comms.interactive;
641                                delete comms.timeout;
642                                service.exArgs = comms;
643                        }
644                }
645        },
646
647        _connectUp: function(){
648                // summary:
649                //              Connect up all the events with their event handlers
650                // tags:
651                //              private
652                var editor = this._editor,
653                        cont = this._dialogContent;
654
655                this.connect(this.button, "set", "_disabled");
656                this.connect(this._service, "onLoad", "_loadData");
657                this.connect(this._dialog, "onOpen", "_openDialog");
658                this.connect(editor, "onKeyPress", "_keyPress");
659                this.connect(editor, "onLoad", "_submitContent");
660                this.connect(cont, "onSkip", "_skip");
661                this.connect(cont, "onSkipAll", "_skipAll");
662                this.connect(cont, "onAddToDic", "_add");
663                this.connect(cont, "onReplace", "_replace");
664                this.connect(cont, "onReplaceAll", "_replaceAll");
665                this.connect(cont, "onCancel", "_cancel");
666                this.connect(cont, "onEnter", "_enter");
667
668                editor.contentPostFilters.push(this._spellCheckFilter); // Register the filter
669                dojo.publish(dijit._scopeName + ".Editor.plugin.SpellCheck.getParser", [this]); // Get the language parser
670                if(!this.parser){
671                        console.error("Can not get the word parser!");
672                }
673        },
674
675        /*************************************************************************/
676        /**                      Event Handlers                                 **/
677        /*************************************************************************/
678
679        _disabled: function(name, disabled){
680                // summary:
681                //              When the plugin is disabled (the button is disabled), reset all to their initial status.
682                //              If the interactive mode is on, check the content once it is enabled.
683                // name:
684                //              Command name
685                // disabled:
686                //              Command argument
687                // tags:
688                //              private
689                if(name == "disabled"){
690                        if(disabled){
691                                this._iterator = 0;
692                                this._spanList = [];
693                        }else if(this.interactive && !disabled && this._service){
694                                this._submitContent(true);
695                        }
696                        this._enabled = !disabled;
697                }
698        },
699
700        _keyPress: function(evt){
701                // summary:
702                //              The handler of the onKeyPress event of the editor
703                // tags:
704                //              private
705                if(this.interactive){
706                        var v = 118, V = 86,
707                                cc = evt.charCode;
708                        if(!evt.altKey && cc == dojo.keys.SPACE){
709                                this._submitContent();
710                        }else if((evt.ctrlKey && (cc == v || cc == V)) || (!evt.ctrlKey && evt.charCode)){
711                                this._submitContent(true);
712                        }
713                }
714        },
715
716        _loadData: function(/*Array*/ data){
717                // summary:
718                //              Apply the query result to the content
719                // data:
720                //              The result of the query
721                // tags:
722                //              private
723                var cache = this._cache,
724                        html = this._editor.get("value"),
725                        cont = this._dialogContent;
726
727                this._iterator = 0;
728
729                // Update the local cache
730                dojo.forEach(data, function(d){
731                        cache[d.text] = d.suggestion;
732                        cache[d.text].correct = false;
733                });
734
735                if(this._enabled){
736                        // Mark incorrect words
737                        cont.closable = false;
738                        this._markIncorrectWords(html, cache);
739                        cont.closable = true;
740
741                        if(this._dialogContent.isOpen){
742                                this._iterator = -1;
743                                this._skip();
744                        }
745                }
746        },
747
748        _openDialog: function(){
749                // summary:
750                //              The handler of the onOpen event
751                var cont = this._dialogContent;
752
753                // Clear dialog content and disable it first
754                cont.ignoreChange = true;
755                cont.set("unfoundWord", "");
756                cont.set("suggestionList", null);
757                cont.set("disabled", true);
758                cont.set("inProgress", true);
759
760                cont.isOpen = true; // Indicate that the dialog is open
761                cont.closable = false;
762
763                this._submitContent();
764
765                cont.closable = true;
766        },
767
768        _skip: function(/*Event?*/ evt, /*Boolean?*/ noUpdate){
769                // summary:
770                //              Ignore this word and move to the next unignored one.
771                // evt:
772                //              The event object
773                // noUpdate:
774                //              Indicate whether to update the status of the span list or not
775                // tags:
776                //              private
777                var cont = this._dialogContent,
778                        list = this._spanList || [],
779                        len = list.length,
780                        iter = this._iterator;
781
782                cont.closable = false;
783                cont.isChanged = false;
784                cont.ignoreChange = true;
785
786                // Skip the current word
787                if(!noUpdate && iter >= 0 && iter < len){
788                        this._skipWord(iter);
789                }
790
791                // Move to the next
792                while(++iter < len && list[iter].edited == true){ /* do nothing */}
793                if(iter < len){
794                        this._iterator = iter;
795                        this._populateDialog(iter);
796                        this._selectWord(iter);
797                }else{
798                        // Reaches the end of the list
799                        this._iterator = -1;
800                        cont.set("unfoundWord", this._strings["msg"]);
801                        cont.set("suggestionList", null);
802                        cont.set("disabled", true);
803                        cont.set("inProgress", false);
804                }
805
806                setTimeout(function(){
807                        // When moving the focus out of the iframe in WebKit browsers, we
808                        // need to focus something else first. So the textbox
809                        // can be focused correctly.
810                        if(dojo.isWebKit) { cont.skipButton.focus(); }
811                        cont.focus();
812                        cont.ignoreChange = false;
813                        cont.closable = true;
814                }, 0);
815        },
816
817        _skipAll: function(){
818                // summary:
819                //              Ignore all the same words
820                // tags:
821                //              private
822                this._dialogContent.closable = false;
823                this._skipWordAll(this._iterator);
824                this._skip();
825        },
826
827        _add: function(){
828                // summary:
829                //              Add the unrecognized word into the dictionary
830                // tags:
831                //              private
832                var cont = this._dialogContent;
833
834                cont.closable = false;
835                cont.isOpen = true;
836                this._addWord(this._iterator, cont.get("unfoundWord"));
837                this._skip();
838        },
839
840        _replace: function(){
841                // summary:
842                //              Replace the incorrect word with the selected one,
843                //              or the one the user types in the textbox
844                // tags:
845                //              private
846                var cont = this._dialogContent,
847                        iter = this._iterator,
848                        targetWord = cont.get("selectedWord");
849
850                cont.closable = false;
851                this._replaceWord(iter, targetWord);
852                this._skip(null, true);
853        },
854
855        _replaceAll: function(){
856                // summary:
857                //              Replace all the words with the same text
858                // tags:
859                //              private
860                var cont = this._dialogContent,
861                        list = this._spanList,
862                        len = list.length,
863                        word = list[this._iterator].innerHTML.toLowerCase(),
864                        targetWord = cont.get("selectedWord");
865
866                cont.closable = false;
867                for(var iter = 0; iter < len; iter++){
868                        // If this word is not ignored and is the same as the source word,
869                        // replace it.
870                        if(list[iter].innerHTML.toLowerCase() == word){
871                                this._replaceWord(iter, targetWord);
872                        }
873                }
874
875                this._skip(null, true);
876        },
877
878        _cancel: function(){
879                // summary:
880                //              Cancel this check action
881                // tags:
882                //              private
883                this._dialogContent.closable = true;
884                this._editor.focus();
885        },
886
887        _enter: function(){
888                // summary:
889                //              Handle the ENTER event
890                // tags:
891                //              private
892                if(this._dialogContent.isChanged){
893                        this._replace();
894                }else{
895                        this._skip();
896                }
897        },
898
899        /*************************************************************************/
900        /**                              Utils                                  **/
901        /*************************************************************************/
902
903        _query: function(/*String*/ html){
904                // summary:
905                //              Send the query text to the service. The query text is a string of words
906                //              separated by space.
907                // html:
908                //              The html value of the editor
909                // tags:
910                //              private
911                var service = this._service,
912                        cache = this._cache,
913                        words = this.parser.parseIntoWords(this._html2Text(html)) || [];
914                var content = [];
915                dojo.forEach(words, function(word){
916                        word = word.toLowerCase();
917                        if(!cache[word]){
918                                // New word that need to be send to the server side for check
919                                cache[word] = [];
920                                cache[word].correct = true;
921                                content.push(word);
922                        }
923                });
924                if(content.length > 0){
925                        service.send(content.join(" "));
926                }else if(!service.isWorking){
927                        this._loadData([]);
928                }
929        },
930
931        _html2Text: function(html){
932                // summary:
933                //              Substitute the tag with white charactors so that the server
934                //              can easily process the text. For example:
935                // |    "<a src="sample.html">Hello, world!</a>" ==>
936                // |    "                     Hello, world!    "
937                // html:
938                //              The html code
939                // tags:
940                //              private
941                var text = [],
942                        isTag = false,
943                        len = html ? html.length : 0;
944
945                for(var i = 0; i < len; i++){
946                        if(html.charAt(i) == "<"){ isTag = true; }
947                        if(isTag == true){
948                                text.push(" ");
949                        }else{
950                                text.push(html.charAt(i));
951                        }
952                        if(html.charAt(i) == ">"){ isTag = false; }
953
954                }
955                return text.join("");
956        },
957
958        _getBookmark: function(/*String*/ eValue){
959                // summary:
960                //              Get the cursor position. It is the index of the characters
961                //              where the cursor is.
962                // eValue:
963                //              The html value of the editor
964                // tags:
965                //              private
966                var ed = this._editor,
967                        cp = this._cursorSpan;
968                ed.execCommand("inserthtml", cp);
969                var nv = ed.get("value"),
970                        index = nv.indexOf(cp),
971                        i = -1;
972                while(++i < index && eValue.charAt(i) == nv.charAt(i)){ /* do nothing */}
973                return i;
974        },
975
976        _moveToBookmark: function(){
977                // summary:
978                //              Move to the position when the cursor was.
979                // tags:
980                //              private
981                var ed = this._editor,
982                        cps = dojo.query("." + this._cursorSelector, ed.document),
983                        cursorSpan = cps && cps[0];
984                // Find the cursor place holder
985                if(cursorSpan){
986                        ed._sCall("selectElement", [cursorSpan]);
987                        ed._sCall("collapse", [true]);
988                        var parent = cursorSpan.parentNode;
989                        if(parent){ parent.removeChild(cursorSpan); }
990                }
991        },
992
993        _submitContent: function(/*Boolean?*/ delay){
994                // summary:
995                //              Functions to submit the content of the editor
996                // delay:
997                //              Indicate if the action is taken immediately or not
998                // tags:
999                //              private
1000                if(delay){
1001                        var _this = this,
1002                                interval = 3000;
1003                        if(this._delayHandler){
1004                                clearTimeout(this._delayHandler);
1005                                this._delayHandler = null;
1006                        }
1007                        setTimeout(function(){ _this._query(_this._editor.get("value")); }, interval);
1008                }else{
1009                        this._query(this._editor.get("value"));
1010                }
1011        },
1012
1013        _populateDialog: function(index){
1014                // summary:
1015                //              Populate the content of the dailog
1016                // index:
1017                //              The idex of the span list
1018                // tags:
1019                //              private
1020                var list = this._spanList,
1021                        cache = this._cache,
1022                        cont = this._dialogContent;
1023
1024                cont.set("disabled", false);
1025                if(index < list.length && list.length > 0){
1026                        var word = list[index].innerHTML;
1027                        cont.set("unfoundWord", word);
1028                        cont.set("suggestionList", cache[word.toLowerCase()]);
1029                        cont.set("inProgress", false);
1030                }
1031        },
1032
1033        _markIncorrectWords: function(/*String*/ html, /*Object*/ cache){
1034                // summary:
1035                //              Mark the incorrect words and set up menus if available
1036                // html:
1037                //              The html value of the editor
1038                // cache:
1039                //              The local word cache
1040                // tags:
1041                //              private
1042                var _this = this,
1043                        parser = this.parser,
1044                        editor = this._editor,
1045                        spanString = this._incorrectWordsSpan,
1046                        nstyle = this._normalIncorrectStyle,
1047                        selector = this._selector,
1048                        words = parser.parseIntoWords(this._html2Text(html).toLowerCase()),
1049                        indices = parser.getIndices(),
1050                        bookmark = this._cursorSpan,
1051                        bmpos = this._getBookmark(html),
1052                        spanOffset = "<span class='incorrectWordPlaceHolder'>".length,
1053                        bmMarked = false,
1054                        cArray = html.split(""),
1055                        spanList = null;
1056
1057                // Mark the incorrect words and cursor position
1058                for(var i = words.length - 1; i >= 0; i--){
1059                        var word = words[i];
1060                        if(cache[word] && !cache[word].correct){
1061                                var offset = indices[i],
1062                                        len = words[i].length,
1063                                        end = offset + len;
1064                                if(end <= bmpos && !bmMarked){
1065                                        cArray.splice(bmpos, 0, bookmark);
1066                                        bmMarked = true;
1067                                }
1068                                cArray.splice(offset, len, dojo.string.substitute(spanString, {text: html.substring(offset, end)}));
1069                                if(offset < bmpos && bmpos < end && !bmMarked){
1070                                        var tmp = cArray[offset].split("");
1071                                        tmp.splice(spanOffset + bmpos - offset, 0, bookmark);
1072                                        cArray[offset] = tmp.join("");
1073                                        bmMarked = true;
1074                                }
1075                        }
1076                }
1077                if(!bmMarked){
1078                        cArray.splice(bmpos, 0, bookmark);
1079                        bmMarked = true;
1080                }
1081
1082                editor.set("value", cArray.join(""));
1083                editor._cursorToStart = false; // HACK! But really necessary here.
1084
1085                this._moveToBookmark();
1086
1087                // Get the incorrect words <span>
1088                spanList = this._spanList = dojo.query("." + this._selector, editor.document);
1089                spanList.forEach(function(span, i){ span.id = selector + i; });
1090
1091                // Set them to the incorrect word style
1092                if(!this.interactive){ delete nstyle.cursor; }
1093                spanList.style(nstyle);
1094
1095                if(this.interactive){
1096                        // Build the context menu
1097                        if(_this._contextMenu){
1098                                _this._contextMenu.uninitialize();
1099                                _this._contextMenu = null;
1100                        }
1101                        _this._contextMenu = new dijit.Menu({
1102                                targetNodeIds: [editor.iframe],
1103
1104                                bindDomNode: function(/*String|DomNode*/ node){
1105                                        // summary:
1106                                        //              Attach menu to given node
1107                                        node = dojo.byId(node);
1108
1109                                        var cn; // Connect node
1110
1111                                        // Support context menus on iframes.   Rather than binding to the iframe itself we need
1112                                        // to bind to the <body> node inside the iframe.
1113                                        var iframe, win;
1114                                        if(node.tagName.toLowerCase() == "iframe"){
1115                                                iframe = node;
1116                                                win = this._iframeContentWindow(iframe);
1117                                                cn = dojo.body(editor.document)
1118                                        }else{
1119
1120                                                // To capture these events at the top level, attach to <html>, not <body>.
1121                                                // Otherwise right-click context menu just doesn't work.
1122                                                cn = (node == dojo.body() ? dojo.doc.documentElement : node);
1123                                        }
1124
1125
1126                                        // "binding" is the object to track our connection to the node (ie, the parameter to bindDomNode())
1127                                        var binding = {
1128                                                node: node,
1129                                                iframe: iframe
1130                                        };
1131
1132                                        // Save info about binding in _bindings[], and make node itself record index(+1) into
1133                                        // _bindings[] array.   Prefix w/_dijitMenu to avoid setting an attribute that may
1134                                        // start with a number, which fails on FF/safari.
1135                                        dojo.attr(node, "_dijitMenu" + this.id, this._bindings.push(binding));
1136
1137                                        // Setup the connections to monitor click etc., unless we are connecting to an iframe which hasn't finished
1138                                        // loading yet, in which case we need to wait for the onload event first, and then connect
1139                                        // On linux Shift-F10 produces the oncontextmenu event, but on Windows it doesn't, so
1140                                        // we need to monitor keyboard events in addition to the oncontextmenu event.
1141                                        var doConnects = dojo.hitch(this, function(cn){
1142                                                return [
1143                                                        // TODO: when leftClickToOpen is true then shouldn't space/enter key trigger the menu,
1144                                                        // rather than shift-F10?
1145                                                        dojo.connect(cn, this.leftClickToOpen ? "onclick" : "oncontextmenu", this, function(evt){
1146                                                                var target = evt.target,
1147                                                                        strings = _this._strings;
1148                                                                // Schedule context menu to be opened unless it's already been scheduled from onkeydown handler
1149                                                                if(dojo.hasClass(target, selector) && !target.edited){ // Click on the incorrect word
1150                                                                        dojo.stopEvent(evt);
1151
1152                                                                        // Build the on-demand menu items
1153                                                                        var maxNumber = _this._maxItemNumber,
1154                                                                                id = target.id,
1155                                                                                index = id.substring(selector.length),
1156                                                                                suggestions = cache[target.innerHTML.toLowerCase()],
1157                                                                                slen = suggestions.length;
1158
1159                                                                        // Add the suggested words menu items
1160                                                                        this.destroyDescendants();
1161                                                                        if(slen == 0){
1162                                                                                this.addChild(new dijit.MenuItem({
1163                                                                                        label: strings["iMsg"],
1164                                                                                        disabled: true
1165                                                                                }));
1166                                                                        }else{
1167                                                                                for(var i = 0 ; i < maxNumber && i < slen; i++){
1168                                                                                        this.addChild(new dijit.MenuItem({
1169                                                                                                label: suggestions[i],
1170                                                                                                onClick: (function(){
1171                                                                                                        var idx = index, txt = suggestions[i];
1172                                                                                                        return function(){
1173                                                                                                                _this._replaceWord(idx, txt);
1174                                                                                                                editor.focus();
1175                                                                                                        };
1176                                                                                                })()
1177                                                                                        }));
1178                                                                                }
1179                                                                        }
1180
1181                                                                        //Add the other action menu items
1182                                                                        this.addChild(new dijit.MenuSeparator());
1183                                                                        this.addChild(new dijit.MenuItem({
1184                                                                                label: strings["iSkip"],
1185                                                                                onClick: function(){
1186                                                                                        _this._skipWord(index);
1187                                                                                        editor.focus();
1188                                                                                }
1189                                                                        }));
1190                                                                        this.addChild(new dijit.MenuItem({
1191                                                                                label: strings["iSkipAll"],
1192                                                                                onClick: function(){
1193                                                                                        _this._skipWordAll(index);
1194                                                                                        editor.focus();
1195                                                                                }
1196                                                                        }));
1197                                                                        this.addChild(new dijit.MenuSeparator());
1198                                                                        this.addChild(new dijit.MenuItem({
1199                                                                                label: strings["toDic"],
1200                                                                                onClick: function(){
1201                                                                                        _this._addWord(index);
1202                                                                                        editor.focus();
1203                                                                                }
1204                                                                        }));
1205
1206                                                                        this._scheduleOpen(target, iframe, {x: evt.pageX, y: evt.pageY});
1207                                                                }
1208                                                        }),
1209                                                        dojo.connect(cn, "onkeydown", this, function(evt){
1210                                                                if(evt.shiftKey && evt.keyCode == dojo.keys.F10){
1211                                                                        dojo.stopEvent(evt);
1212                                                                        this._scheduleOpen(evt.target, iframe); // no coords - open near target node
1213                                                                }
1214                                                        })
1215                                                ];
1216                                        });
1217                                        binding.connects = cn ? doConnects(cn) : [];
1218
1219                                        if(iframe){
1220                                                // Setup handler to [re]bind to the iframe when the contents are initially loaded,
1221                                                // and every time the contents change.
1222                                                // Need to do this b/c we are actually binding to the iframe's <body> node.
1223                                                // Note: can't use dojo.connect(), see #9609.
1224
1225                                                binding.onloadHandler = dojo.hitch(this, function(){
1226                                                        // want to remove old connections, but IE throws exceptions when trying to
1227                                                        // access the <body> node because it's already gone, or at least in a state of limbo
1228
1229                                                        var win = this._iframeContentWindow(iframe),
1230                                                                cn = dojo.body(editor.document);
1231                                                        binding.connects = doConnects(cn);
1232                                                });
1233                                                if(iframe.addEventListener){
1234                                                        iframe.addEventListener("load", binding.onloadHandler, false);
1235                                                }else{
1236                                                        iframe.attachEvent("onload", binding.onloadHandler);
1237                                                }
1238                                        }
1239                                }
1240                        });
1241                }
1242        },
1243
1244        _selectWord: function(index){
1245                // summary:
1246                //              Select the incorrect word. Move to it and highlight it
1247                // index:
1248                //              The index of the span list
1249                // tags:
1250                //              private
1251                var ed = this._editor,
1252                        list = this._spanList;
1253
1254                if(index < list.length && list.length > 0){
1255                        ed._sCall("selectElement", [list[index]]);
1256                        ed._sCall("collapse", [true]);
1257                        this._findText(list[index].innerHTML, false, false);
1258                        if(dojo.isIE){
1259                                // Because the selection in the iframe will be lost when the outer window get the
1260                                // focus, we need to mimic the highlight ourselves.
1261                                dojo.style(list[index], this._highlightedIncorrectStyle);
1262                        }
1263                }
1264        },
1265
1266        _replaceWord: function(index, text){
1267                // summary:
1268                //              Replace the word at the given index with the text
1269                // index:
1270                //              The index of the span list
1271                // text:
1272                //              The text to be replaced with
1273                // tags:
1274                //              private
1275                var list = this._spanList;
1276
1277                list[index].innerHTML = text;
1278                dojo.style(list[index], this._ignoredIncorrectStyle);
1279                list[index].edited = true;
1280        },
1281
1282        _skipWord: function(index){
1283                // summary:
1284                //              Skip the word at the index
1285                // index:
1286                //              The index of the span list
1287                // tags:
1288                //              private
1289                var list = this._spanList;
1290
1291                dojo.style(list[index], this._ignoredIncorrectStyle);
1292                this._cache[list[index].innerHTML.toLowerCase()].correct = true;
1293                list[index].edited = true;
1294        },
1295
1296        _skipWordAll: function(index, /*String?*/word){
1297                // summary:
1298                //              Skip the all the word that have the same text as the word at the index
1299                //              or the given word
1300                // index:
1301                //              The index of the span list
1302                // word:
1303                //              If this argument is given, skip all the words that have the same text
1304                //              as the word
1305                // tags:
1306                //              private
1307                var list = this._spanList,
1308                        len = list.length;
1309                word = word || list[index].innerHTML.toLowerCase();
1310
1311                for(var i = 0; i < len; i++){
1312                        if(!list[i].edited && list[i].innerHTML.toLowerCase() == word){
1313                                this._skipWord(i);
1314                        }
1315                }
1316        },
1317
1318        _addWord: function(index, /*String?*/word){
1319                // summary:
1320                //              Add the word at the index to the dictionary
1321                // index:
1322                //              The index of the span list
1323                // word:
1324                //              If this argument is given, add the word to the dictionary and
1325                //              skip all the words like it
1326                // tags:
1327                //              private
1328                var service = this._service;
1329                service.send(word || this._spanList[index].innerHTML.toLowerCase(), service.ACTION_UPDATE);
1330                this._skipWordAll(index, word);
1331        },
1332
1333        _findText: function(/*String*/ txt, /*Boolean*/ caseSensitive, /*Boolean*/ backwards){
1334                // summary:
1335                //              This function invokes a find with specific options
1336                // txt: String
1337                //              The text to locate in the document.
1338                // caseSensitive: Boolean
1339                //              Whether or ot to search case-sensitively.
1340                // backwards: Boolean
1341                //              Whether or not to search backwards in the document.
1342                // tags:
1343                //              private.
1344                // returns:
1345                //              Boolean indicating if the content was found or not.
1346                var ed = this._editor,
1347                        win = ed.window,
1348                        found = false;
1349                if(txt){
1350                        if(win.find){
1351                                found = win.find(txt, caseSensitive, backwards, false, false, false, false);
1352                        }else{
1353                                var doc = ed.document;
1354                                if(doc.selection){
1355                                        /* IE */
1356                                        // Focus to restore position/selection,
1357                                        // then shift to search from current position.
1358                                        this._editor.focus();
1359                                        var txtRg = doc.body.createTextRange();
1360                                        var curPos = doc.selection?doc.selection.createRange():null;
1361                                        if(curPos){
1362                                                if(backwards){
1363                                                        txtRg.setEndPoint("EndToStart", curPos);
1364                                                }else{
1365                                                        txtRg.setEndPoint("StartToEnd", curPos);
1366                                                }
1367                                        }
1368                                        var flags = caseSensitive?4:0;
1369                                        if(backwards){
1370                                                flags = flags | 1;
1371                                        }
1372                                        //flags = flags |
1373                                        found = txtRg.findText(txt,txtRg.text.length,flags);
1374                                        if(found){
1375                                                txtRg.select();
1376                                        }
1377                                }
1378                        }
1379                }
1380                return found;
1381        },
1382
1383        _spellCheckFilter: function(/*String*/ value){
1384                // summary:
1385                //              Filter out the incorrect word style so that the value of the edtior
1386                //              won't include the spans that wrap around the incorrect words
1387                // value:
1388                //              The html value of the editor
1389                // tags:
1390                //              private
1391                var regText = /<span class=["']incorrectWordPlaceHolder["'].*?>(.*?)<\/span>/g;
1392                return value.replace(regText, "$1");
1393        }
1394});
1395
1396// For monkey patching
1397SpellCheck._SpellCheckControl = SpellCheckControl;
1398SpellCheck._SpellCheckScriptMultiPart = SpellCheckScriptMultiPart;
1399
1400// Register this plugin.
1401dojo.subscribe(dijit._scopeName + ".Editor.getPlugin",null,function(o){
1402        if(o.plugin){ return; }
1403        var name = o.args.name.toLowerCase();
1404        if(name ===  "spellcheck"){
1405                o.plugin = new SpellCheck({
1406                        url: ("url" in o.args) ? o.args.url : "",
1407                        interactive: ("interactive" in o.args) ? o.args.interactive : false,
1408                        bufferLength: ("bufferLength" in o.args) ? o.args.bufferLength: 100,
1409                        timeout: ("timeout" in o.args) ? o.args.timeout : 30,
1410                        exArgs: o.args
1411                });
1412        }
1413});
1414
1415return SpellCheck;
1416
1417});
Note: See TracBrowser for help on using the repository browser.