source: Dev/trunk/src/client/dijit/_editor/RichText.js

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

Added Dojo 1.9.3 release.

File size: 95.7 KB
Line 
1define([
2        "dojo/_base/array", // array.forEach array.indexOf array.some
3        "dojo/_base/config", // config
4        "dojo/_base/declare", // declare
5        "dojo/_base/Deferred", // Deferred
6        "dojo/dom", // dom.byId
7        "dojo/dom-attr", // domAttr.set or get
8        "dojo/dom-class", // domClass.add domClass.remove
9        "dojo/dom-construct", // domConstruct.create domConstruct.destroy domConstruct.place
10        "dojo/dom-geometry", // domGeometry.position
11        "dojo/dom-style", // domStyle.getComputedStyle domStyle.set
12        "dojo/_base/kernel", // kernel.deprecated
13        "dojo/keys", // keys.BACKSPACE keys.TAB
14        "dojo/_base/lang", // lang.clone lang.hitch lang.isArray lang.isFunction lang.isString lang.trim
15        "dojo/on", // on()
16        "dojo/query", // query
17        "dojo/domReady",
18        "dojo/sniff", // has("ie") has("mozilla") has("opera") has("safari") has("webkit")
19        "dojo/topic", // topic.publish() (publish)
20        "dojo/_base/unload", // unload
21        "dojo/_base/url", // url
22        "dojo/window", // winUtils.get()
23        "../_Widget",
24        "../_CssStateMixin",
25        "../selection",
26        "./range",
27        "./html",
28        "../focus",
29        "../main"    // dijit._scopeName
30], function(array, config, declare, Deferred, dom, domAttr, domClass, domConstruct, domGeometry, domStyle,
31                        kernel, keys, lang, on, query, domReady, has, topic, unload, _Url, winUtils,
32                        _Widget, _CssStateMixin, selectionapi, rangeapi, htmlapi, focus, dijit){
33
34        // module:
35        //              dijit/_editor/RichText
36
37        // If you want to allow for rich text saving with back/forward actions, you must add a text area to your page with
38        // the id==dijit._scopeName + "._editor.RichText.value" (typically "dijit/_editor/RichText.value). For example,
39        // something like this will work:
40        //
41        //      <textarea id="dijit._editor.RichText.value" style="display:none;position:absolute;top:-100px;left:-100px;height:3px;width:3px;overflow:hidden;"></textarea>
42
43        var RichText = declare("dijit._editor.RichText", [_Widget, _CssStateMixin], {
44                // summary:
45                //              dijit/_editor/RichText is the core of dijit.Editor, which provides basic
46                //              WYSIWYG editing features.
47                //
48                // description:
49                //              dijit/_editor/RichText is the core of dijit.Editor, which provides basic
50                //              WYSIWYG editing features. It also encapsulates the differences
51                //              of different js engines for various browsers.  Do not use this widget
52                //              with an HTML &lt;TEXTAREA&gt; tag, since the browser unescapes XML escape characters,
53                //              like &lt;.  This can have unexpected behavior and lead to security issues
54                //              such as scripting attacks.
55                //
56                // tags:
57                //              private
58
59                constructor: function(params /*===== , srcNodeRef =====*/){
60                        // summary:
61                        //              Create the widget.
62                        // params: Object|null
63                        //              Initial settings for any of the widget attributes, except readonly attributes.
64                        // srcNodeRef: DOMNode
65                        //              The widget replaces the specified DOMNode.
66
67                        // contentPreFilters: Function(String)[]
68                        //              Pre content filter function register array.
69                        //              these filters will be executed before the actual
70                        //              editing area gets the html content.
71                        this.contentPreFilters = [];
72
73                        // contentPostFilters: Function(String)[]
74                        //              post content filter function register array.
75                        //              These will be used on the resulting html
76                        //              from contentDomPostFilters. The resulting
77                        //              content is the final html (returned by getValue()).
78                        this.contentPostFilters = [];
79
80                        // contentDomPreFilters: Function(DomNode)[]
81                        //              Pre content dom filter function register array.
82                        //              These filters are applied after the result from
83                        //              contentPreFilters are set to the editing area.
84                        this.contentDomPreFilters = [];
85
86                        // contentDomPostFilters: Function(DomNode)[]
87                        //              Post content dom filter function register array.
88                        //              These filters are executed on the editing area dom.
89                        //              The result from these will be passed to contentPostFilters.
90                        this.contentDomPostFilters = [];
91
92                        // editingAreaStyleSheets: dojo._URL[]
93                        //              array to store all the stylesheets applied to the editing area
94                        this.editingAreaStyleSheets = [];
95
96                        // Make a copy of this.events before we start writing into it, otherwise we
97                        // will modify the prototype which leads to bad things on pages w/multiple editors
98                        this.events = [].concat(this.events);
99
100                        this._keyHandlers = {};
101
102                        if(params && lang.isString(params.value)){
103                                this.value = params.value;
104                        }
105
106                        this.onLoadDeferred = new Deferred();
107                },
108
109                baseClass: "dijitEditor",
110
111                // inheritWidth: Boolean
112                //              whether to inherit the parent's width or simply use 100%
113                inheritWidth: false,
114
115                // focusOnLoad: [deprecated] Boolean
116                //              Focus into this widget when the page is loaded
117                focusOnLoad: false,
118
119                // name: String?
120                //              Specifies the name of a (hidden) `<textarea>` node on the page that's used to save
121                //              the editor content on page leave.   Used to restore editor contents after navigating
122                //              to a new page and then hitting the back button.
123                name: "",
124
125                // styleSheets: [const] String
126                //              semicolon (";") separated list of css files for the editing area
127                styleSheets: "",
128
129                // height: String
130                //              Set height to fix the editor at a specific height, with scrolling.
131                //              By default, this is 300px.  If you want to have the editor always
132                //              resizes to accommodate the content, use AlwaysShowToolbar plugin
133                //              and set height="".  If this editor is used within a layout widget,
134                //              set height="100%".
135                height: "300px",
136
137                // minHeight: String
138                //              The minimum height that the editor should have.
139                minHeight: "1em",
140
141                // isClosed: [private] Boolean
142                isClosed: true,
143
144                // isLoaded: [private] Boolean
145                isLoaded: false,
146
147                // _SEPARATOR: [private] String
148                //              Used to concat contents from multiple editors into a single string,
149                //              so they can be saved into a single `<textarea>` node.  See "name" attribute.
150                _SEPARATOR: "@@**%%__RICHTEXTBOUNDRY__%%**@@",
151
152                // _NAME_CONTENT_SEP: [private] String
153                //              USed to separate name from content.  Just a colon isn't safe.
154                _NAME_CONTENT_SEP: "@@**%%:%%**@@",
155
156                // onLoadDeferred: [readonly] dojo/promise/Promise
157                //              Deferred which is fired when the editor finishes loading.
158                //              Call myEditor.onLoadDeferred.then(callback) it to be informed
159                //              when the rich-text area initialization is finalized.
160                onLoadDeferred: null,
161
162                // isTabIndent: Boolean
163                //              Make tab key and shift-tab indent and outdent rather than navigating.
164                //              Caution: sing this makes web pages inaccessible to users unable to use a mouse.
165                isTabIndent: false,
166
167                // disableSpellCheck: [const] Boolean
168                //              When true, disables the browser's native spell checking, if supported.
169                //              Works only in Firefox.
170                disableSpellCheck: false,
171
172                postCreate: function(){
173                        if("textarea" === this.domNode.tagName.toLowerCase()){
174                                console.warn("RichText should not be used with the TEXTAREA tag.  See dijit._editor.RichText docs.");
175                        }
176
177                        // Push in the builtin filters now, making them the first executed, but not over-riding anything
178                        // users passed in.  See: #6062
179                        this.contentPreFilters = [
180                                lang.trim,      // avoid IE10 problem hitting ENTER on last line when there's a trailing \n.
181                                lang.hitch(this, "_preFixUrlAttributes")
182                        ].concat(this.contentPreFilters);
183                        if(has("mozilla")){
184                                this.contentPreFilters = [this._normalizeFontStyle].concat(this.contentPreFilters);
185                                this.contentPostFilters = [this._removeMozBogus].concat(this.contentPostFilters);
186                        }
187                        if(has("webkit")){
188                                // Try to clean up WebKit bogus artifacts.  The inserted classes
189                                // made by WebKit sometimes messes things up.
190                                this.contentPreFilters = [this._removeWebkitBogus].concat(this.contentPreFilters);
191                                this.contentPostFilters = [this._removeWebkitBogus].concat(this.contentPostFilters);
192                        }
193                        if(has("ie") || has("trident")){
194                                // IE generates <strong> and <em> but we want to normalize to <b> and <i>
195                                // Still happens in IE11!
196                                this.contentPostFilters = [this._normalizeFontStyle].concat(this.contentPostFilters);
197                                this.contentDomPostFilters = [lang.hitch(this, "_stripBreakerNodes")].concat(this.contentDomPostFilters);
198                        }
199                        this.contentDomPostFilters = [lang.hitch(this, "_stripTrailingEmptyNodes")].concat(this.contentDomPostFilters);
200                        this.inherited(arguments);
201
202                        topic.publish(dijit._scopeName + "._editor.RichText::init", this);
203                },
204
205                startup: function(){
206                        this.inherited(arguments);
207
208                        // Don't call open() until startup() because we need to be attached to the DOM, and also if we are the
209                        // child of a StackContainer, let StackContainer._setupChild() do DOM manipulations before iframe is
210                        // created, to avoid duplicate onload call.
211                        this.open();
212                        this.setupDefaultShortcuts();
213                },
214
215                setupDefaultShortcuts: function(){
216                        // summary:
217                        //              Add some default key handlers
218                        // description:
219                        //              Overwrite this to setup your own handlers. The default
220                        //              implementation does not use Editor commands, but directly
221                        //              executes the builtin commands within the underlying browser
222                        //              support.
223                        // tags:
224                        //              protected
225                        var exec = lang.hitch(this, function(cmd, arg){
226                                return function(){
227                                        return !this.execCommand(cmd, arg);
228                                };
229                        });
230
231                        var ctrlKeyHandlers = {
232                                b: exec("bold"),
233                                i: exec("italic"),
234                                u: exec("underline"),
235                                a: exec("selectall"),
236                                s: function(){
237                                        this.save(true);
238                                },
239                                m: function(){
240                                        this.isTabIndent = !this.isTabIndent;
241                                },
242
243                                "1": exec("formatblock", "h1"),
244                                "2": exec("formatblock", "h2"),
245                                "3": exec("formatblock", "h3"),
246                                "4": exec("formatblock", "h4"),
247
248                                "\\": exec("insertunorderedlist")
249                        };
250
251                        if(!has("ie")){
252                                ctrlKeyHandlers.Z = exec("redo"); //FIXME: undo?
253                        }
254
255                        var key;
256                        for(key in ctrlKeyHandlers){
257                                this.addKeyHandler(key, true, false, ctrlKeyHandlers[key]);
258                        }
259                },
260
261                // events: [private] String[]
262                //               events which should be connected to the underlying editing area
263                events: ["onKeyDown", "onKeyUp"], // onClick handled specially
264
265                // captureEvents: [deprecated] String[]
266                //               Events which should be connected to the underlying editing
267                //               area, events in this array will be addListener with
268                //               capture=true.
269                // TODO: looking at the code I don't see any distinction between events and captureEvents,
270                // so get rid of this for 2.0 if not sooner
271                captureEvents: [],
272
273                _editorCommandsLocalized: false,
274                _localizeEditorCommands: function(){
275                        // summary:
276                        //              When IE is running in a non-English locale, the API actually changes,
277                        //              so that we have to say (for example) danraku instead of p (for paragraph).
278                        //              Handle that here.
279                        // tags:
280                        //              private
281                        if(RichText._editorCommandsLocalized){
282                                // Use the already generate cache of mappings.
283                                this._local2NativeFormatNames = RichText._local2NativeFormatNames;
284                                this._native2LocalFormatNames = RichText._native2LocalFormatNames;
285                                return;
286                        }
287                        RichText._editorCommandsLocalized = true;
288                        RichText._local2NativeFormatNames = {};
289                        RichText._native2LocalFormatNames = {};
290                        this._local2NativeFormatNames = RichText._local2NativeFormatNames;
291                        this._native2LocalFormatNames = RichText._native2LocalFormatNames;
292                        //in IE, names for blockformat is locale dependent, so we cache the values here
293
294                        //put p after div, so if IE returns Normal, we show it as paragraph
295                        //We can distinguish p and div if IE returns Normal, however, in order to detect that,
296                        //we have to call this.document.selection.createRange().parentElement() or such, which
297                        //could slow things down. Leave it as it is for now
298                        var formats = ['div', 'p', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'ul', 'address'];
299                        var localhtml = "", format, i = 0;
300                        while((format = formats[i++])){
301                                //append a <br> after each element to separate the elements more reliably
302                                if(format.charAt(1) !== 'l'){
303                                        localhtml += "<" + format + "><span>content</span></" + format + "><br/>";
304                                }else{
305                                        localhtml += "<" + format + "><li>content</li></" + format + "><br/>";
306                                }
307                        }
308                        // queryCommandValue returns empty if we hide editNode, so move it out of screen temporary
309                        // Also, IE9 does weird stuff unless we do it inside the editor iframe.
310                        var style = { position: "absolute", top: "0px", zIndex: 10, opacity: 0.01 };
311                        var div = domConstruct.create('div', {style: style, innerHTML: localhtml});
312                        this.ownerDocumentBody.appendChild(div);
313
314                        // IE9 has a timing issue with doing this right after setting
315                        // the inner HTML, so put a delay in.
316                        var inject = lang.hitch(this, function(){
317                                var node = div.firstChild;
318                                while(node){
319                                        try{
320                                                this.selection.selectElement(node.firstChild);
321                                                var nativename = node.tagName.toLowerCase();
322                                                this._local2NativeFormatNames[nativename] = document.queryCommandValue("formatblock");
323                                                this._native2LocalFormatNames[this._local2NativeFormatNames[nativename]] = nativename;
324                                                node = node.nextSibling.nextSibling;
325                                                //console.log("Mapped: ", nativename, " to: ", this._local2NativeFormatNames[nativename]);
326                                        }catch(e){ /*Sqelch the occasional IE9 error */
327                                        }
328                                }
329                                domConstruct.destroy(div);
330                        });
331                        this.defer(inject);
332                },
333
334                open: function(/*DomNode?*/ element){
335                        // summary:
336                        //              Transforms the node referenced in this.domNode into a rich text editing
337                        //              node.
338                        // description:
339                        //              Sets up the editing area asynchronously. This will result in
340                        //              the creation and replacement with an iframe.
341                        // tags:
342                        //              private
343
344                        if(!this.onLoadDeferred || this.onLoadDeferred.fired >= 0){
345                                this.onLoadDeferred = new Deferred();
346                        }
347
348                        if(!this.isClosed){
349                                this.close();
350                        }
351                        topic.publish(dijit._scopeName + "._editor.RichText::open", this);
352
353                        if(arguments.length === 1 && element.nodeName){ // else unchanged
354                                this.domNode = element;
355                        }
356
357                        var dn = this.domNode;
358
359                        // "html" will hold the innerHTML of the srcNodeRef and will be used to
360                        // initialize the editor.
361                        var html;
362
363                        if(lang.isString(this.value)){
364                                // Allow setting the editor content programmatically instead of
365                                // relying on the initial content being contained within the target
366                                // domNode.
367                                html = this.value;
368                                dn.innerHTML = "";
369                        }else if(dn.nodeName && dn.nodeName.toLowerCase() == "textarea"){
370                                // if we were created from a textarea, then we need to create a
371                                // new editing harness node.
372                                var ta = (this.textarea = dn);
373                                this.name = ta.name;
374                                html = ta.value;
375                                dn = this.domNode = this.ownerDocument.createElement("div");
376                                dn.setAttribute('widgetId', this.id);
377                                ta.removeAttribute('widgetId');
378                                dn.cssText = ta.cssText;
379                                dn.className += " " + ta.className;
380                                domConstruct.place(dn, ta, "before");
381                                var tmpFunc = lang.hitch(this, function(){
382                                        //some browsers refuse to submit display=none textarea, so
383                                        //move the textarea off screen instead
384                                        domStyle.set(ta, {
385                                                display: "block",
386                                                position: "absolute",
387                                                top: "-1000px"
388                                        });
389
390                                        if(has("ie")){ //nasty IE bug: abnormal formatting if overflow is not hidden
391                                                var s = ta.style;
392                                                this.__overflow = s.overflow;
393                                                s.overflow = "hidden";
394                                        }
395                                });
396                                if(has("ie")){
397                                        this.defer(tmpFunc, 10);
398                                }else{
399                                        tmpFunc();
400                                }
401
402                                if(ta.form){
403                                        var resetValue = ta.value;
404                                        this.reset = function(){
405                                                var current = this.getValue();
406                                                if(current !== resetValue){
407                                                        this.replaceValue(resetValue);
408                                                }
409                                        };
410                                        on(ta.form, "submit", lang.hitch(this, function(){
411                                                // Copy value to the <textarea> so it gets submitted along with form.
412                                                // FIXME: should we be calling close() here instead?
413                                                domAttr.set(ta, 'disabled', this.disabled); // don't submit the value if disabled
414                                                ta.value = this.getValue();
415                                        }));
416                                }
417                        }else{
418                                html = htmlapi.getChildrenHtml(dn);
419                                dn.innerHTML = "";
420                        }
421
422                        this.value = html;
423
424                        // If we're a list item we have to put in a blank line to force the
425                        // bullet to nicely align at the top of text
426                        if(dn.nodeName && dn.nodeName === "LI"){
427                                dn.innerHTML = " <br>";
428                        }
429
430                        // Construct the editor div structure.
431                        this.header = dn.ownerDocument.createElement("div");
432                        dn.appendChild(this.header);
433                        this.editingArea = dn.ownerDocument.createElement("div");
434                        dn.appendChild(this.editingArea);
435                        this.footer = dn.ownerDocument.createElement("div");
436                        dn.appendChild(this.footer);
437
438                        if(!this.name){
439                                this.name = this.id + "_AUTOGEN";
440                        }
441
442                        // User has pressed back/forward button so we lost the text in the editor, but it's saved
443                        // in a hidden <textarea> (which contains the data for all the editors on this page),
444                        // so get editor value from there
445                        if(this.name !== "" && (!config["useXDomain"] || config["allowXdRichTextSave"])){
446                                var saveTextarea = dom.byId(dijit._scopeName + "._editor.RichText.value");
447                                if(saveTextarea && saveTextarea.value !== ""){
448                                        var datas = saveTextarea.value.split(this._SEPARATOR), i = 0, dat;
449                                        while((dat = datas[i++])){
450                                                var data = dat.split(this._NAME_CONTENT_SEP);
451                                                if(data[0] === this.name){
452                                                        html = data[1];
453                                                        datas = datas.splice(i, 1);
454                                                        saveTextarea.value = datas.join(this._SEPARATOR);
455                                                        break;
456                                                }
457                                        }
458                                }
459
460                                if(!RichText._globalSaveHandler){
461                                        RichText._globalSaveHandler = {};
462                                        unload.addOnUnload(function(){
463                                                var id;
464                                                for(id in RichText._globalSaveHandler){
465                                                        var f = RichText._globalSaveHandler[id];
466                                                        if(lang.isFunction(f)){
467                                                                f();
468                                                        }
469                                                }
470                                        });
471                                }
472                                RichText._globalSaveHandler[this.id] = lang.hitch(this, "_saveContent");
473                        }
474
475                        this.isClosed = false;
476
477                        var ifr = (this.editorObject = this.iframe = this.ownerDocument.createElement('iframe'));
478                        ifr.id = this.id + "_iframe";
479                        ifr.style.border = "none";
480                        ifr.style.width = "100%";
481                        if(this._layoutMode){
482                                // iframe should be 100% height, thus getting it's height from surrounding
483                                // <div> (which has the correct height set by Editor)
484                                ifr.style.height = "100%";
485                        }else{
486                                if(has("ie") >= 7){
487                                        if(this.height){
488                                                ifr.style.height = this.height;
489                                        }
490                                        if(this.minHeight){
491                                                ifr.style.minHeight = this.minHeight;
492                                        }
493                                }else{
494                                        ifr.style.height = this.height ? this.height : this.minHeight;
495                                }
496                        }
497                        ifr.frameBorder = 0;
498                        ifr._loadFunc = lang.hitch(this, function(w){
499                                this.window = w;
500                                this.document = w.document;
501
502                                // instantiate class to access selected text in editor's iframe
503                                this.selection = new selectionapi.SelectionManager(w);
504
505                                if(has("ie")){
506                                        this._localizeEditorCommands();
507                                }
508
509                                // Do final setup and set initial contents of editor
510                                this.onLoad(html);
511                        });
512
513                        // Attach iframe to document, and set the initial (blank) content.
514                        var src = this._getIframeDocTxt().replace(/\\/g, "\\\\").replace(/'/g, "\\'"),
515                                s;
516
517                        // IE10 and earlier will throw an "Access is denied" error when attempting to access the parent frame if
518                        // document.domain has been set, unless the child frame also has the same document.domain set. The child frame
519                        // can only set document.domain while the document is being constructed using open/write/close; attempting to
520                        // set it later results in a different "This method can't be used in this context" error. See #17529
521                        if (has("ie") < 11) {
522                                s = 'javascript:document.open();try{parent.window;}catch(e){document.domain="' + document.domain + '";}' +
523                                        'document.write(\'' + src + '\');document.close()';
524                        }
525                        else {
526                                s = "javascript: '" + src + "'";
527                        }
528
529                        if(has("ie") == 9){
530                                // On IE9, attach to document before setting the content, to avoid problem w/iframe running in
531                                // wrong security context, see #16633.
532                                this.editingArea.appendChild(ifr);
533                                ifr.src = s;
534                        }else{
535                                // For other browsers, set src first, especially for IE6/7 where attaching first gives a warning on
536                                // https:// about "this page contains secure and insecure items, do you want to view both?"
537                                ifr.setAttribute('src', s);
538                                this.editingArea.appendChild(ifr);
539                        }
540
541                        // TODO: this is a guess at the default line-height, kinda works
542                        if(dn.nodeName === "LI"){
543                                dn.lastChild.style.marginTop = "-1.2em";
544                        }
545
546                        domClass.add(this.domNode, this.baseClass);
547                },
548
549                //static cache variables shared among all instance of this class
550                _local2NativeFormatNames: {},
551                _native2LocalFormatNames: {},
552
553                _getIframeDocTxt: function(){
554                        // summary:
555                        //              Generates the boilerplate text of the document inside the iframe (ie, `<html><head>...</head><body/></html>`).
556                        //              Editor content (if not blank) should be added afterwards.
557                        // tags:
558                        //              private
559                        var _cs = domStyle.getComputedStyle(this.domNode);
560
561                        // The contents inside of <body>.  The real contents are set later via a call to setValue().
562                        // In auto-expand mode, need a wrapper div for AlwaysShowToolbar plugin to correctly
563                        // expand/contract the editor as the content changes.
564                        var html = "<div id='dijitEditorBody'></div>";
565
566                        var font = [ _cs.fontWeight, _cs.fontSize, _cs.fontFamily ].join(" ");
567
568                        // line height is tricky - applying a units value will mess things up.
569                        // if we can't get a non-units value, bail out.
570                        var lineHeight = _cs.lineHeight;
571                        if(lineHeight.indexOf("px") >= 0){
572                                lineHeight = parseFloat(lineHeight) / parseFloat(_cs.fontSize);
573                                // console.debug(lineHeight);
574                        }else if(lineHeight.indexOf("em") >= 0){
575                                lineHeight = parseFloat(lineHeight);
576                        }else{
577                                // If we can't get a non-units value, just default
578                                // it to the CSS spec default of 'normal'.  Seems to
579                                // work better, esp on IE, than '1.0'
580                                lineHeight = "normal";
581                        }
582                        var userStyle = "";
583                        var self = this;
584                        this.style.replace(/(^|;)\s*(line-|font-?)[^;]+/ig, function(match){
585                                match = match.replace(/^;/ig, "") + ';';
586                                var s = match.split(":")[0];
587                                if(s){
588                                        s = lang.trim(s);
589                                        s = s.toLowerCase();
590                                        var i;
591                                        var sC = "";
592                                        for(i = 0; i < s.length; i++){
593                                                var c = s.charAt(i);
594                                                switch(c){
595                                                        case "-":
596                                                                i++;
597                                                                c = s.charAt(i).toUpperCase();
598                                                        default:
599                                                                sC += c;
600                                                }
601                                        }
602                                        domStyle.set(self.domNode, sC, "");
603                                }
604                                userStyle += match + ';';
605                        });
606
607
608                        // need to find any associated label element, aria-label, or aria-labelledby and update iframe document title
609                        var label = query('label[for="' + this.id + '"]');
610                        var title = "";
611                        if(label.length){
612                                title = label[0].innerHTML;
613                        }else if(this["aria-label"]){
614                                title = this["aria-label"];
615                        }else if(this["aria-labelledby"]){
616                                title = dom.byId(this["aria-labelledby"]).innerHTML;
617                        }
618
619                        // Now that we have the title, also set it as the title attribute on the iframe
620                        this.iframe.setAttribute("title", title);
621
622                        return [
623                                "<!DOCTYPE html>",
624                                this.isLeftToRight() ? "<html lang='" + this.lang + "'>\n<head>\n" : "<html dir='rtl' lang='" + this.lang + "'>\n<head>\n",
625                                title ? "<title>" + title + "</title>" : "",
626                                "<meta http-equiv='Content-Type' content='text/html'>\n",
627                                "<style>\n",
628                                "\tbody,html {\n",
629                                "\t\tbackground:transparent;\n",
630                                "\t\tpadding: 1px 0 0 0;\n",
631                                "\t\tmargin: -1px 0 0 0;\n", // remove extraneous vertical scrollbar on safari and firefox
632                                "\t}\n",
633                                "\tbody,html,#dijitEditorBody { outline: none; }",
634
635                                // Set <body> to expand to full size of editor, so clicking anywhere will work.
636                                // Except in auto-expand mode, in which case the editor expands to the size of <body>.
637                                // Also determine how scrollers should be applied.  In autoexpand mode (height = "") no scrollers on y at all.
638                                // But in fixed height mode we want both x/y scrollers.
639                                // Scrollers go on <body> since it's been set to height: 100%.
640                                "html { height: 100%; width: 100%; overflow: hidden; }\n",      // scroll bar is on #dijitEditorBody, shouldn't be on <html>
641                                this.height ? "\tbody,#dijitEditorBody { height: 100%; width: 100%; overflow: auto; }\n" :
642                                        "\tbody,#dijitEditorBody { min-height: " + this.minHeight + "; width: 100%; overflow-x: auto; overflow-y: hidden; }\n",
643
644                                // TODO: left positioning will cause contents to disappear out of view
645                                //         if it gets too wide for the visible area
646                                "\tbody{\n",
647                                "\t\ttop:0px;\n",
648                                "\t\tleft:0px;\n",
649                                "\t\tright:0px;\n",
650                                "\t\tfont:", font, ";\n",
651                                ((this.height || has("opera")) ? "" : "\t\tposition: fixed;\n"),
652                                "\t\tline-height:", lineHeight, ";\n",
653                                "\t}\n",
654                                "\tp{ margin: 1em 0; }\n",
655
656                                "\tli > ul:-moz-first-node, li > ol:-moz-first-node{ padding-top: 1.2em; }\n",
657                                // Can't set min-height in IE9, it puts layout on li, which puts move/resize handles.
658                                (!has("ie") ? "\tli{ min-height:1.2em; }\n" : ""),
659                                "</style>\n",
660                                this._applyEditingAreaStyleSheets(), "\n",
661                                "</head>\n<body role='main' ",
662
663                                // Onload handler fills in real editor content.
664                                // On IE9, sometimes onload is called twice, and the first time frameElement is null (test_FullScreen.html)
665                                "onload='frameElement && frameElement._loadFunc(window,document)' ",
666                                "style='" + userStyle + "'>", html, "</body>\n</html>"
667                        ].join(""); // String
668                },
669
670                _applyEditingAreaStyleSheets: function(){
671                        // summary:
672                        //              apply the specified css files in styleSheets
673                        // tags:
674                        //              private
675                        var files = [];
676                        if(this.styleSheets){
677                                files = this.styleSheets.split(';');
678                                this.styleSheets = '';
679                        }
680
681                        //empty this.editingAreaStyleSheets here, as it will be filled in addStyleSheet
682                        files = files.concat(this.editingAreaStyleSheets);
683                        this.editingAreaStyleSheets = [];
684
685                        var text = '', i = 0, url, ownerWindow = winUtils.get(this.ownerDocument);
686                        while((url = files[i++])){
687                                var abstring = (new _Url(ownerWindow.location, url)).toString();
688                                this.editingAreaStyleSheets.push(abstring);
689                                text += '<link rel="stylesheet" type="text/css" href="' + abstring + '"/>';
690                        }
691                        return text;
692                },
693
694                addStyleSheet: function(/*dojo/_base/url*/ uri){
695                        // summary:
696                        //              add an external stylesheet for the editing area
697                        // uri:
698                        //              Url of the external css file
699                        var url = uri.toString(), ownerWindow = winUtils.get(this.ownerDocument);
700
701                        //if uri is relative, then convert it to absolute so that it can be resolved correctly in iframe
702                        if(url.charAt(0) === '.' || (url.charAt(0) !== '/' && !uri.host)){
703                                url = (new _Url(ownerWindow.location, url)).toString();
704                        }
705
706                        if(array.indexOf(this.editingAreaStyleSheets, url) > -1){
707//                      console.debug("dijit/_editor/RichText.addStyleSheet(): Style sheet "+url+" is already applied");
708                                return;
709                        }
710
711                        this.editingAreaStyleSheets.push(url);
712                        this.onLoadDeferred.then(lang.hitch(this, function(){
713                                if(this.document.createStyleSheet){ //IE
714                                        this.document.createStyleSheet(url);
715                                }else{ //other browser
716                                        var head = this.document.getElementsByTagName("head")[0];
717                                        var stylesheet = this.document.createElement("link");
718                                        stylesheet.rel = "stylesheet";
719                                        stylesheet.type = "text/css";
720                                        stylesheet.href = url;
721                                        head.appendChild(stylesheet);
722                                }
723                        }));
724                },
725
726                removeStyleSheet: function(/*dojo/_base/url*/ uri){
727                        // summary:
728                        //              remove an external stylesheet for the editing area
729                        var url = uri.toString(), ownerWindow = winUtils.get(this.ownerDocument);
730                        //if uri is relative, then convert it to absolute so that it can be resolved correctly in iframe
731                        if(url.charAt(0) === '.' || (url.charAt(0) !== '/' && !uri.host)){
732                                url = (new _Url(ownerWindow.location, url)).toString();
733                        }
734                        var index = array.indexOf(this.editingAreaStyleSheets, url);
735                        if(index === -1){
736//                      console.debug("dijit/_editor/RichText.removeStyleSheet(): Style sheet "+url+" has not been applied");
737                                return;
738                        }
739                        delete this.editingAreaStyleSheets[index];
740                        query('link[href="' + url + '"]', this.window.document).orphan();
741                },
742
743                // disabled: Boolean
744                //              The editor is disabled; the text cannot be changed.
745                disabled: false,
746
747                _mozSettingProps: {'styleWithCSS': false},
748                _setDisabledAttr: function(/*Boolean*/ value){
749                        value = !!value;
750                        this._set("disabled", value);
751                        if(!this.isLoaded){
752                                return;
753                        } // this method requires init to be complete
754                        var preventIEfocus = has("ie") && (this.isLoaded || !this.focusOnLoad);
755                        if(preventIEfocus){
756                                this.editNode.unselectable = "on";
757                        }
758                        this.editNode.contentEditable = !value;
759                        this.editNode.tabIndex = value ? "-1" : this.tabIndex;
760                        if(preventIEfocus){
761                                this.defer(function(){
762                                        if(this.editNode){        // guard in case widget destroyed before timeout
763                                                this.editNode.unselectable = "off";
764                                        }
765                                });
766                        }
767                        if(has("mozilla") && !value && this._mozSettingProps){
768                                var ps = this._mozSettingProps;
769                                var n;
770                                for(n in ps){
771                                        if(ps.hasOwnProperty(n)){
772                                                try{
773                                                        this.document.execCommand(n, false, ps[n]);
774                                                }catch(e2){
775                                                }
776                                        }
777                                }
778                        }
779                        this._disabledOK = true;
780                },
781
782                /* Event handlers
783                 *****************/
784
785                onLoad: function(/*String*/ html){
786                        // summary:
787                        //              Handler after the iframe finishes loading.
788                        // html: String
789                        //              Editor contents should be set to this value
790                        // tags:
791                        //              protected
792
793                        // TODO: rename this to _onLoad, make empty public onLoad() method, deprecate/make protected onLoadDeferred handler?
794
795                        if(!this.window.__registeredWindow){
796                                this.window.__registeredWindow = true;
797                                this._iframeRegHandle = focus.registerIframe(this.iframe);
798                        }
799
800                        // there's a wrapper div around the content, see _getIframeDocTxt().
801                        this.editNode = this.document.body.firstChild;
802                        var _this = this;
803
804                        // Helper code so IE and FF skip over focusing on the <iframe> and just focus on the inner <div>.
805                        // See #4996 IE wants to focus the BODY tag.
806                        this.beforeIframeNode = domConstruct.place("<div tabIndex=-1></div>", this.iframe, "before");
807                        this.afterIframeNode = domConstruct.place("<div tabIndex=-1></div>", this.iframe, "after");
808                        this.iframe.onfocus = this.document.onfocus = function(){
809                                _this.editNode.focus();
810                        };
811
812                        this.focusNode = this.editNode; // for InlineEditBox
813
814
815                        var events = this.events.concat(this.captureEvents);
816                        var ap = this.iframe ? this.document : this.editNode;
817                        this.own(
818                                array.map(events, function(item){
819                                        var type = item.toLowerCase().replace(/^on/, "");
820                                        on(ap, type, lang.hitch(this, item));
821                                }, this)
822                        );
823
824                        this.own(
825                                // mouseup in the margin does not generate an onclick event
826                                on(ap, "mouseup", lang.hitch(this, "onClick"))
827                        );
828
829                        if(has("ie")){ // IE contentEditable
830                                this.own(on(this.document, "mousedown", lang.hitch(this, "_onIEMouseDown"))); // #4996 fix focus
831
832                                // give the node Layout on IE
833                                // TODO: this may no longer be needed, since we've reverted IE to using an iframe,
834                                // not contentEditable.   Removing it would also probably remove the need for creating
835                                // the extra <div> in _getIframeDocTxt()
836                                this.editNode.style.zoom = 1.0;
837                        }else{
838                                this.own(on(this.document, "mousedown", lang.hitch(this, function(){
839                                        // Clear the moveToStart focus, as mouse
840                                        // down will set cursor point.  Required to properly
841                                        // work with selection/position driven plugins and clicks in
842                                        // the window. refs: #10678
843                                        delete this._cursorToStart;
844                                })));
845                        }
846
847                        if(has("webkit")){
848                                //WebKit sometimes doesn't fire right on selections, so the toolbar
849                                //doesn't update right.  Therefore, help it out a bit with an additional
850                                //listener.  A mouse up will typically indicate a display change, so fire this
851                                //and get the toolbar to adapt.  Reference: #9532
852                                this._webkitListener = this.own(on(this.document, "mouseup", lang.hitch(this, "onDisplayChanged")))[0];
853                                this.own(on(this.document, "mousedown", lang.hitch(this, function(e){
854                                        var t = e.target;
855                                        if(t && (t === this.document.body || t === this.document)){
856                                                // Since WebKit uses the inner DIV, we need to check and set position.
857                                                // See: #12024 as to why the change was made.
858                                                this.defer("placeCursorAtEnd");
859                                        }
860                                })));
861                        }
862
863                        if(has("ie")){
864                                // Try to make sure 'hidden' elements aren't visible in edit mode (like browsers other than IE
865                                // do).  See #9103
866                                try{
867                                        this.document.execCommand('RespectVisibilityInDesign', true, null);
868                                }catch(e){/* squelch */
869                                }
870                        }
871
872                        this.isLoaded = true;
873
874                        this.set('disabled', this.disabled); // initialize content to editable (or not)
875
876                        // Note that setValue() call will only work after isLoaded is set to true (above)
877
878                        // Set up a function to allow delaying the setValue until a callback is fired
879                        // This ensures extensions like dijit.Editor have a way to hold the value set
880                        // until plugins load (and do things like register filters).
881                        var setContent = lang.hitch(this, function(){
882                                this.setValue(html);
883                                if(this.onLoadDeferred){
884                                        this.onLoadDeferred.resolve(true);
885                                }
886                                this.onDisplayChanged();
887                                if(this.focusOnLoad){
888                                        // after the document loads, then set focus after updateInterval expires so that
889                                        // onNormalizedDisplayChanged has run to avoid input caret issues
890                                        domReady(lang.hitch(this, "defer", "focus", this.updateInterval));
891                                }
892                                // Save off the initial content now
893                                this.value = this.getValue(true);
894                        });
895                        if(this.setValueDeferred){
896                                this.setValueDeferred.then(setContent);
897                        }else{
898                                setContent();
899                        }
900                },
901
902                onKeyDown: function(/* Event */ e){
903                        // summary:
904                        //              Handler for keydown event
905                        // tags:
906                        //              protected
907
908                        // Modifier keys should not cause the onKeyPressed event because they do not cause any change to the
909                        // display
910                        if(e.keyCode === keys.SHIFT ||
911                           e.keyCode === keys.ALT ||
912                           e.keyCode === keys.META ||
913                           e.keyCode === keys.CTRL){
914                                return true;
915                        }
916
917                        if(e.keyCode === keys.TAB && this.isTabIndent){
918                                //prevent tab from moving focus out of editor
919                                e.stopPropagation();
920                                e.preventDefault();
921
922                                // FIXME: this is a poor-man's indent/outdent. It would be
923                                // better if it added 4 "&nbsp;" chars in an undoable way.
924                                // Unfortunately pasteHTML does not prove to be undoable
925                                if(this.queryCommandEnabled((e.shiftKey ? "outdent" : "indent"))){
926                                        this.execCommand((e.shiftKey ? "outdent" : "indent"));
927                                }
928                        }
929
930                        // Make tab and shift-tab skip over the <iframe>, going from the nested <div> to the toolbar
931                        // or next element after the editor
932                        if(e.keyCode == keys.TAB && !this.isTabIndent && !e.ctrlKey && !e.altKey){
933                                if(e.shiftKey){
934                                        // focus the <iframe> so the browser will shift-tab away from it instead
935                                        this.beforeIframeNode.focus();
936                                }else{
937                                        // focus node after the <iframe> so the browser will tab away from it instead
938                                        this.afterIframeNode.focus();
939                                }
940
941                                // Prevent onKeyPressed from firing in order to avoid triggering a display change event when the
942                                // editor is tabbed away; this fixes toolbar controls being inappropriately disabled in IE9+
943                                return true;
944                        }
945
946                        if(has("ie") < 9 && e.keyCode === keys.BACKSPACE && this.document.selection.type === "Control"){
947                                // IE has a bug where if a non-text object is selected in the editor,
948                                // hitting backspace would act as if the browser's back button was
949                                // clicked instead of deleting the object. see #1069
950                                e.stopPropagation();
951                                e.preventDefault();
952                                this.execCommand("delete");
953                        }
954
955                        if(has("ff")){
956                                if(e.keyCode === keys.PAGE_UP || e.keyCode === keys.PAGE_DOWN){
957                                        if(this.editNode.clientHeight >= this.editNode.scrollHeight){
958                                                // Stop the event to prevent firefox from trapping the cursor when there is no scroll bar.
959                                                e.preventDefault();
960                                        }
961                                }
962                        }
963
964                        var handlers = this._keyHandlers[e.keyCode],
965                                args = arguments;
966
967                        if(handlers && !e.altKey){
968                                array.some(handlers, function(h){
969                                        // treat meta- same as ctrl-, for benefit of mac users
970                                        if(!(h.shift ^ e.shiftKey) && !(h.ctrl ^ (e.ctrlKey || e.metaKey))){
971                                                if(!h.handler.apply(this, args)){
972                                                        e.preventDefault();
973                                                }
974                                                return true;
975                                        }
976                                }, this);
977                        }
978
979                        // function call after the character has been inserted
980                        this.defer("onKeyPressed", 1);
981
982                        return true;
983                },
984
985                onKeyUp: function(/*===== e =====*/){
986                        // summary:
987                        //              Handler for onkeyup event
988                        // tags:
989                        //              callback
990                },
991
992                setDisabled: function(/*Boolean*/ disabled){
993                        // summary:
994                        //              Deprecated, use set('disabled', ...) instead.
995                        // tags:
996                        //              deprecated
997                        kernel.deprecated('dijit.Editor::setDisabled is deprecated', 'use dijit.Editor::attr("disabled",boolean) instead', 2.0);
998                        this.set('disabled', disabled);
999                },
1000                _setValueAttr: function(/*String*/ value){
1001                        // summary:
1002                        //              Registers that attr("value", foo) should call setValue(foo)
1003                        this.setValue(value);
1004                },
1005                _setDisableSpellCheckAttr: function(/*Boolean*/ disabled){
1006                        if(this.document){
1007                                domAttr.set(this.document.body, "spellcheck", !disabled);
1008                        }else{
1009                                // try again after the editor is finished loading
1010                                this.onLoadDeferred.then(lang.hitch(this, function(){
1011                                        domAttr.set(this.document.body, "spellcheck", !disabled);
1012                                }));
1013                        }
1014                        this._set("disableSpellCheck", disabled);
1015                },
1016
1017                addKeyHandler: function(/*String|Number*/ key, /*Boolean*/ ctrl, /*Boolean*/ shift, /*Function*/ handler){
1018                        // summary:
1019                        //              Add a handler for a keyboard shortcut
1020                        // tags:
1021                        //              protected
1022
1023                        if(typeof key == "string"){
1024                                // Something like Ctrl-B.  Since using keydown event, we need to convert string to a number.
1025                                key = key.toUpperCase().charCodeAt(0);
1026                        }
1027
1028                        if(!lang.isArray(this._keyHandlers[key])){
1029                                this._keyHandlers[key] = [];
1030                        }
1031
1032                        this._keyHandlers[key].push({
1033                                shift: shift || false,
1034                                ctrl: ctrl || false,
1035                                handler: handler
1036                        });
1037                },
1038
1039                onKeyPressed: function(){
1040                        // summary:
1041                        //              Handler for after the user has pressed a key, and the display has been updated.
1042                        //              (Runs on a timer so that it runs after the display is updated)
1043                        // tags:
1044                        //              private
1045                        this.onDisplayChanged(/*e*/); // can't pass in e
1046                },
1047
1048                onClick: function(/*Event*/ e){
1049                        // summary:
1050                        //              Handler for when the user clicks.
1051                        // tags:
1052                        //              private
1053
1054                        // console.info('onClick',this._tryDesignModeOn);
1055                        this.onDisplayChanged(e);
1056                },
1057
1058                _onIEMouseDown: function(){
1059                        // summary:
1060                        //              IE only to prevent 2 clicks to focus
1061                        // tags:
1062                        //              protected
1063
1064                        if(!this.focused && !this.disabled){
1065                                this.focus();
1066                        }
1067                },
1068
1069                _onBlur: function(e){
1070                        // summary:
1071                        //              Called from focus manager when focus has moved away from this editor
1072                        // tags:
1073                        //              protected
1074
1075                        // Workaround IE problem when you blur the browser windows while an editor is focused: IE hangs
1076                        // when you focus editor #1, blur the browser window, and then click editor #0.  See #16939.
1077                        if(has("ie") || has("trident")){
1078                                this.defer(function(){
1079                                        if(!focus.curNode){
1080                                                this.ownerDocumentBody.focus();
1081                                        }
1082                                });
1083                        }
1084
1085                        this.inherited(arguments);
1086
1087                        var newValue = this.getValue(true);
1088                        if(newValue !== this.value){
1089                                this.onChange(newValue);
1090                        }
1091                        this._set("value", newValue);
1092                },
1093
1094                _onFocus: function(/*Event*/ e){
1095                        // summary:
1096                        //              Called from focus manager when focus has moved into this editor
1097                        // tags:
1098                        //              protected
1099
1100                        // console.info('_onFocus')
1101                        if(!this.disabled){
1102                                if(!this._disabledOK){
1103                                        this.set('disabled', false);
1104                                }
1105                                this.inherited(arguments);
1106                        }
1107                },
1108
1109                // TODO: remove in 2.0
1110                blur: function(){
1111                        // summary:
1112                        //              Remove focus from this instance.
1113                        // tags:
1114                        //              deprecated
1115                        if(!has("ie") && this.window.document.documentElement && this.window.document.documentElement.focus){
1116                                this.window.document.documentElement.focus();
1117                        }else if(this.ownerDocumentBody.focus){
1118                                this.ownerDocumentBody.focus();
1119                        }
1120                },
1121
1122                focus: function(){
1123                        // summary:
1124                        //              Move focus to this editor
1125                        if(!this.isLoaded){
1126                                this.focusOnLoad = true;
1127                                return;
1128                        }
1129                        if(this._cursorToStart){
1130                                delete this._cursorToStart;
1131                                if(this.editNode.childNodes){
1132                                        this.placeCursorAtStart(); // this calls focus() so return
1133                                        return;
1134                                }
1135                        }
1136                        if(has("ie") < 9){
1137                                //this.editNode.focus(); -> causes IE to scroll always (strict and quirks mode) to the top the Iframe
1138                                // if we fire the event manually and let the browser handle the focusing, the latest
1139                                // cursor position is focused like in FF
1140                                this.iframe.fireEvent('onfocus', document.createEventObject()); // createEventObject/fireEvent only in IE < 11
1141                        }else{
1142                                // Firefox and chrome
1143                                this.editNode.focus();
1144                        }
1145                },
1146
1147                // _lastUpdate: 0,
1148                updateInterval: 200,
1149                _updateTimer: null,
1150                onDisplayChanged: function(/*Event*/ /*===== e =====*/){
1151                        // summary:
1152                        //              This event will be fired every time the display context
1153                        //              changes and the result needs to be reflected in the UI.
1154                        // description:
1155                        //              If you don't want to have update too often,
1156                        //              onNormalizedDisplayChanged should be used instead
1157                        // tags:
1158                        //              private
1159
1160                        // var _t=new Date();
1161                        if(this._updateTimer){
1162                                this._updateTimer.remove();
1163                        }
1164                        this._updateTimer = this.defer("onNormalizedDisplayChanged", this.updateInterval);
1165
1166                        // Technically this should trigger a call to watch("value", ...) registered handlers,
1167                        // but getValue() is too slow to call on every keystroke so we don't.
1168                },
1169                onNormalizedDisplayChanged: function(){
1170                        // summary:
1171                        //              This event is fired every updateInterval ms or more
1172                        // description:
1173                        //              If something needs to happen immediately after a
1174                        //              user change, please use onDisplayChanged instead.
1175                        // tags:
1176                        //              private
1177                        delete this._updateTimer;
1178                },
1179                onChange: function(/*===== newContent =====*/){
1180                        // summary:
1181                        //              This is fired if and only if the editor loses focus and
1182                        //              the content is changed.
1183                },
1184                _normalizeCommand: function(/*String*/ cmd, /*Anything?*/argument){
1185                        // summary:
1186                        //              Used as the advice function to map our
1187                        //              normalized set of commands to those supported by the target
1188                        //              browser.
1189                        // tags:
1190                        //              private
1191
1192                        var command = cmd.toLowerCase();
1193                        if(command === "formatblock"){
1194                                if(has("safari") && argument === undefined){
1195                                        command = "heading";
1196                                }
1197                        }else if(command === "hilitecolor" && !has("mozilla")){
1198                                command = "backcolor";
1199                        }
1200
1201                        return command;
1202                },
1203
1204                _qcaCache: {},
1205                queryCommandAvailable: function(/*String*/ command){
1206                        // summary:
1207                        //              Tests whether a command is supported by the host. Clients
1208                        //              SHOULD check whether a command is supported before attempting
1209                        //              to use it, behaviour for unsupported commands is undefined.
1210                        // command:
1211                        //              The command to test for
1212                        // tags:
1213                        //              private
1214
1215                        // memoizing version. See _queryCommandAvailable for computing version
1216                        var ca = this._qcaCache[command];
1217                        if(ca !== undefined){
1218                                return ca;
1219                        }
1220                        return (this._qcaCache[command] = this._queryCommandAvailable(command));
1221                },
1222
1223                _queryCommandAvailable: function(/*String*/ command){
1224                        // summary:
1225                        //              See queryCommandAvailable().
1226                        // tags:
1227                        //              private
1228
1229                        var ie = 1;
1230                        var mozilla = 1 << 1;
1231                        var webkit = 1 << 2;
1232                        var opera = 1 << 3;
1233
1234                        function isSupportedBy(browsers){
1235                                return {
1236                                        ie: Boolean(browsers & ie),
1237                                        mozilla: Boolean(browsers & mozilla),
1238                                        webkit: Boolean(browsers & webkit),
1239                                        opera: Boolean(browsers & opera)
1240                                };
1241                        }
1242
1243                        var supportedBy = null;
1244
1245                        switch(command.toLowerCase()){
1246                                case "bold":
1247                                case "italic":
1248                                case "underline":
1249                                case "subscript":
1250                                case "superscript":
1251                                case "fontname":
1252                                case "fontsize":
1253                                case "forecolor":
1254                                case "hilitecolor":
1255                                case "justifycenter":
1256                                case "justifyfull":
1257                                case "justifyleft":
1258                                case "justifyright":
1259                                case "delete":
1260                                case "selectall":
1261                                case "toggledir":
1262                                        supportedBy = isSupportedBy(mozilla | ie | webkit | opera);
1263                                        break;
1264
1265                                case "createlink":
1266                                case "unlink":
1267                                case "removeformat":
1268                                case "inserthorizontalrule":
1269                                case "insertimage":
1270                                case "insertorderedlist":
1271                                case "insertunorderedlist":
1272                                case "indent":
1273                                case "outdent":
1274                                case "formatblock":
1275                                case "inserthtml":
1276                                case "undo":
1277                                case "redo":
1278                                case "strikethrough":
1279                                case "tabindent":
1280                                        supportedBy = isSupportedBy(mozilla | ie | opera | webkit);
1281                                        break;
1282
1283                                case "blockdirltr":
1284                                case "blockdirrtl":
1285                                case "dirltr":
1286                                case "dirrtl":
1287                                case "inlinedirltr":
1288                                case "inlinedirrtl":
1289                                        supportedBy = isSupportedBy(ie);
1290                                        break;
1291                                case "cut":
1292                                case "copy":
1293                                case "paste":
1294                                        supportedBy = isSupportedBy(ie | mozilla | webkit | opera);
1295                                        break;
1296
1297                                case "inserttable":
1298                                        supportedBy = isSupportedBy(mozilla | ie);
1299                                        break;
1300
1301                                case "insertcell":
1302                                case "insertcol":
1303                                case "insertrow":
1304                                case "deletecells":
1305                                case "deletecols":
1306                                case "deleterows":
1307                                case "mergecells":
1308                                case "splitcell":
1309                                        supportedBy = isSupportedBy(ie | mozilla);
1310                                        break;
1311
1312                                default:
1313                                        return false;
1314                        }
1315
1316                        return ((has("ie") || has("trident")) && supportedBy.ie) ||
1317                                (has("mozilla") && supportedBy.mozilla) ||
1318                                (has("webkit") && supportedBy.webkit) ||
1319                                (has("opera") && supportedBy.opera);    // Boolean return true if the command is supported, false otherwise
1320                },
1321
1322                execCommand: function(/*String*/ command, argument){
1323                        // summary:
1324                        //              Executes a command in the Rich Text area
1325                        // command:
1326                        //              The command to execute
1327                        // argument:
1328                        //              An optional argument to the command
1329                        // tags:
1330                        //              protected
1331                        var returnValue;
1332
1333                        //focus() is required for IE to work
1334                        //In addition, focus() makes sure after the execution of
1335                        //the command, the editor receives the focus as expected
1336                        if(this.focused){
1337                                // put focus back in the iframe, unless focus has somehow been shifted out of the editor completely
1338                                this.focus();
1339                        }
1340
1341                        command = this._normalizeCommand(command, argument);
1342
1343                        if(argument !== undefined){
1344                                if(command === "heading"){
1345                                        throw new Error("unimplemented");
1346                                }else if(command === "formatblock" && (has("ie") || has("trident"))){
1347                                        argument = '<' + argument + '>';
1348                                }
1349                        }
1350
1351                        //Check to see if we have any over-rides for commands, they will be functions on this
1352                        //widget of the form _commandImpl.  If we don't, fall through to the basic native
1353                        //exec command of the browser.
1354                        var implFunc = "_" + command + "Impl";
1355                        if(this[implFunc]){
1356                                returnValue = this[implFunc](argument);
1357                        }else{
1358                                argument = arguments.length > 1 ? argument : null;
1359                                if(argument || command !== "createlink"){
1360                                        returnValue = this.document.execCommand(command, false, argument);
1361                                }
1362                        }
1363
1364                        this.onDisplayChanged();
1365                        return returnValue;
1366                },
1367
1368                queryCommandEnabled: function(/*String*/ command){
1369                        // summary:
1370                        //              Check whether a command is enabled or not.
1371                        // command:
1372                        //              The command to execute
1373                        // tags:
1374                        //              protected
1375                        if(this.disabled || !this._disabledOK){
1376                                return false;
1377                        }
1378
1379                        command = this._normalizeCommand(command);
1380
1381                        //Check to see if we have any over-rides for commands, they will be functions on this
1382                        //widget of the form _commandEnabledImpl.  If we don't, fall through to the basic native
1383                        //command of the browser.
1384                        var implFunc = "_" + command + "EnabledImpl";
1385
1386                        if(this[implFunc]){
1387                                return  this[implFunc](command);
1388                        }else{
1389                                return this._browserQueryCommandEnabled(command);
1390                        }
1391                },
1392
1393                queryCommandState: function(command){
1394                        // summary:
1395                        //              Check the state of a given command and returns true or false.
1396                        // tags:
1397                        //              protected
1398
1399                        if(this.disabled || !this._disabledOK){
1400                                return false;
1401                        }
1402                        command = this._normalizeCommand(command);
1403                        try{
1404                                return this.document.queryCommandState(command);
1405                        }catch(e){
1406                                //Squelch, occurs if editor is hidden on FF 3 (and maybe others.)
1407                                return false;
1408                        }
1409                },
1410
1411                queryCommandValue: function(command){
1412                        // summary:
1413                        //              Check the value of a given command. This matters most for
1414                        //              custom selections and complex values like font value setting.
1415                        // tags:
1416                        //              protected
1417
1418                        if(this.disabled || !this._disabledOK){
1419                                return false;
1420                        }
1421                        var r;
1422                        command = this._normalizeCommand(command);
1423                        if((has("ie") || has("trident")) && command === "formatblock"){
1424                                r = this._native2LocalFormatNames[this.document.queryCommandValue(command)];
1425                        }else if(has("mozilla") && command === "hilitecolor"){
1426                                var oldValue;
1427                                try{
1428                                        oldValue = this.document.queryCommandValue("styleWithCSS");
1429                                }catch(e){
1430                                        oldValue = false;
1431                                }
1432                                this.document.execCommand("styleWithCSS", false, true);
1433                                r = this.document.queryCommandValue(command);
1434                                this.document.execCommand("styleWithCSS", false, oldValue);
1435                        }else{
1436                                r = this.document.queryCommandValue(command);
1437                        }
1438                        return r;
1439                },
1440
1441                // Misc.
1442
1443                _sCall: function(name, args){
1444                        // summary:
1445                        //              Deprecated, remove for 2.0.   New code should access this.selection directly.
1446                        //              Run the named method of dijit/selection over the
1447                        //              current editor instance's window, with the passed args.
1448                        // tags:
1449                        //              private deprecated
1450
1451                        return this.selection[name].apply(this.selection, args);
1452                },
1453
1454                // FIXME: this is a TON of code duplication. Why?
1455
1456                placeCursorAtStart: function(){
1457                        // summary:
1458                        //              Place the cursor at the start of the editing area.
1459                        // tags:
1460                        //              private
1461
1462                        this.focus();
1463
1464                        //see comments in placeCursorAtEnd
1465                        var isvalid = false;
1466                        if(has("mozilla")){
1467                                // TODO:  Is this branch even necessary?
1468                                var first = this.editNode.firstChild;
1469                                while(first){
1470                                        if(first.nodeType === 3){
1471                                                if(first.nodeValue.replace(/^\s+|\s+$/g, "").length > 0){
1472                                                        isvalid = true;
1473                                                        this.selection.selectElement(first);
1474                                                        break;
1475                                                }
1476                                        }else if(first.nodeType === 1){
1477                                                isvalid = true;
1478                                                var tg = first.tagName ? first.tagName.toLowerCase() : "";
1479                                                // Collapse before childless tags.
1480                                                if(/br|input|img|base|meta|area|basefont|hr|link/.test(tg)){
1481                                                        this.selection.selectElement(first);
1482                                                }else{
1483                                                        // Collapse inside tags with children.
1484                                                        this.selection.selectElementChildren(first);
1485                                                }
1486                                                break;
1487                                        }
1488                                        first = first.nextSibling;
1489                                }
1490                        }else{
1491                                isvalid = true;
1492                                this.selection.selectElementChildren(this.editNode);
1493                        }
1494                        if(isvalid){
1495                                this.selection.collapse(true);
1496                        }
1497                },
1498
1499                placeCursorAtEnd: function(){
1500                        // summary:
1501                        //              Place the cursor at the end of the editing area.
1502                        // tags:
1503                        //              private
1504
1505                        this.focus();
1506
1507                        //In mozilla, if last child is not a text node, we have to use
1508                        // selectElementChildren on this.editNode.lastChild otherwise the
1509                        // cursor would be placed at the end of the closing tag of
1510                        //this.editNode.lastChild
1511                        var isvalid = false;
1512                        if(has("mozilla")){
1513                                var last = this.editNode.lastChild;
1514                                while(last){
1515                                        if(last.nodeType === 3){
1516                                                if(last.nodeValue.replace(/^\s+|\s+$/g, "").length > 0){
1517                                                        isvalid = true;
1518                                                        this.selection.selectElement(last);
1519                                                        break;
1520                                                }
1521                                        }else if(last.nodeType === 1){
1522                                                isvalid = true;
1523                                                this.selection.selectElement(last.lastChild || last);
1524                                                break;
1525                                        }
1526                                        last = last.previousSibling;
1527                                }
1528                        }else{
1529                                isvalid = true;
1530                                this.selection.selectElementChildren(this.editNode);
1531                        }
1532                        if(isvalid){
1533                                this.selection.collapse(false);
1534                        }
1535                },
1536
1537                getValue: function(/*Boolean?*/ nonDestructive){
1538                        // summary:
1539                        //              Return the current content of the editing area (post filters
1540                        //              are applied).  Users should call get('value') instead.
1541                        // nonDestructive:
1542                        //              defaults to false. Should the post-filtering be run over a copy
1543                        //              of the live DOM? Most users should pass "true" here unless they
1544                        //              *really* know that none of the installed filters are going to
1545                        //              mess up the editing session.
1546                        // tags:
1547                        //              private
1548                        if(this.textarea){
1549                                if(this.isClosed || !this.isLoaded){
1550                                        return this.textarea.value;
1551                                }
1552                        }
1553
1554                        return this.isLoaded ? this._postFilterContent(null, nonDestructive) : this.value;
1555                },
1556                _getValueAttr: function(){
1557                        // summary:
1558                        //              Hook to make attr("value") work
1559                        return this.getValue(true);
1560                },
1561
1562                setValue: function(/*String*/ html){
1563                        // summary:
1564                        //              This function sets the content. No undo history is preserved.
1565                        //              Users should use set('value', ...) instead.
1566                        // tags:
1567                        //              deprecated
1568
1569                        // TODO: remove this and getValue() for 2.0, and move code to _setValueAttr()
1570
1571                        if(!this.isLoaded){
1572                                // try again after the editor is finished loading
1573                                this.onLoadDeferred.then(lang.hitch(this, function(){
1574                                        this.setValue(html);
1575                                }));
1576                                return;
1577                        }
1578                        this._cursorToStart = true;
1579                        if(this.textarea && (this.isClosed || !this.isLoaded)){
1580                                this.textarea.value = html;
1581                        }else{
1582                                html = this._preFilterContent(html);
1583                                var node = this.isClosed ? this.domNode : this.editNode;
1584
1585                                node.innerHTML = html;
1586                                this._preDomFilterContent(node);
1587                        }
1588
1589                        this.onDisplayChanged();
1590                        this._set("value", this.getValue(true));
1591                },
1592
1593                replaceValue: function(/*String*/ html){
1594                        // summary:
1595                        //              This function set the content while trying to maintain the undo stack
1596                        //              (now only works fine with Moz, this is identical to setValue in all
1597                        //              other browsers)
1598                        // tags:
1599                        //              protected
1600
1601                        if(this.isClosed){
1602                                this.setValue(html);
1603                        }else if(this.window && this.window.getSelection && !has("mozilla")){ // Safari
1604                                // look ma! it's a totally f'd browser!
1605                                this.setValue(html);
1606                        }else if(this.window && this.window.getSelection){ // Moz
1607                                html = this._preFilterContent(html);
1608                                this.execCommand("selectall");
1609                                this.execCommand("inserthtml", html);
1610                                this._preDomFilterContent(this.editNode);
1611                        }else if(this.document && this.document.selection){//IE
1612                                //In IE, when the first element is not a text node, say
1613                                //an <a> tag, when replacing the content of the editing
1614                                //area, the <a> tag will be around all the content
1615                                //so for now, use setValue for IE too
1616                                this.setValue(html);
1617                        }
1618
1619                        this._set("value", this.getValue(true));
1620                },
1621
1622                _preFilterContent: function(/*String*/ html){
1623                        // summary:
1624                        //              Filter the input before setting the content of the editing
1625                        //              area. DOM pre-filtering may happen after this
1626                        //              string-based filtering takes place but as of 1.2, this is not
1627                        //              guaranteed for operations such as the inserthtml command.
1628                        // tags:
1629                        //              private
1630
1631                        var ec = html;
1632                        array.forEach(this.contentPreFilters, function(ef){
1633                                if(ef){
1634                                        ec = ef(ec);
1635                                }
1636                        });
1637                        return ec;
1638                },
1639                _preDomFilterContent: function(/*DomNode*/ dom){
1640                        // summary:
1641                        //              filter the input's live DOM. All filter operations should be
1642                        //              considered to be "live" and operating on the DOM that the user
1643                        //              will be interacting with in their editing session.
1644                        // tags:
1645                        //              private
1646                        dom = dom || this.editNode;
1647                        array.forEach(this.contentDomPreFilters, function(ef){
1648                                if(ef && lang.isFunction(ef)){
1649                                        ef(dom);
1650                                }
1651                        }, this);
1652                },
1653
1654                _postFilterContent: function(/*DomNode|DomNode[]|String?*/ dom, /*Boolean?*/ nonDestructive){
1655                        // summary:
1656                        //              filter the output after getting the content of the editing area
1657                        //
1658                        // description:
1659                        //              post-filtering allows plug-ins and users to specify any number
1660                        //              of transforms over the editor's content, enabling many common
1661                        //              use-cases such as transforming absolute to relative URLs (and
1662                        //              vice-versa), ensuring conformance with a particular DTD, etc.
1663                        //              The filters are registered in the contentDomPostFilters and
1664                        //              contentPostFilters arrays. Each item in the
1665                        //              contentDomPostFilters array is a function which takes a DOM
1666                        //              Node or array of nodes as its only argument and returns the
1667                        //              same. It is then passed down the chain for further filtering.
1668                        //              The contentPostFilters array behaves the same way, except each
1669                        //              member operates on strings. Together, the DOM and string-based
1670                        //              filtering allow the full range of post-processing that should
1671                        //              be necessaray to enable even the most agressive of post-editing
1672                        //              conversions to take place.
1673                        //
1674                        //              If nonDestructive is set to "true", the nodes are cloned before
1675                        //              filtering proceeds to avoid potentially destructive transforms
1676                        //              to the content which may still needed to be edited further.
1677                        //              Once DOM filtering has taken place, the serialized version of
1678                        //              the DOM which is passed is run through each of the
1679                        //              contentPostFilters functions.
1680                        //
1681                        // dom:
1682                        //              a node, set of nodes, which to filter using each of the current
1683                        //              members of the contentDomPostFilters and contentPostFilters arrays.
1684                        //
1685                        // nonDestructive:
1686                        //              defaults to "false". If true, ensures that filtering happens on
1687                        //              a clone of the passed-in content and not the actual node
1688                        //              itself.
1689                        //
1690                        // tags:
1691                        //              private
1692
1693                        var ec;
1694                        if(!lang.isString(dom)){
1695                                dom = dom || this.editNode;
1696                                if(this.contentDomPostFilters.length){
1697                                        if(nonDestructive){
1698                                                dom = lang.clone(dom);
1699                                        }
1700                                        array.forEach(this.contentDomPostFilters, function(ef){
1701                                                dom = ef(dom);
1702                                        });
1703                                }
1704                                ec = htmlapi.getChildrenHtml(dom);
1705                        }else{
1706                                ec = dom;
1707                        }
1708
1709                        if(!lang.trim(ec.replace(/^\xA0\xA0*/, '').replace(/\xA0\xA0*$/, '')).length){
1710                                ec = "";
1711                        }
1712
1713                        array.forEach(this.contentPostFilters, function(ef){
1714                                ec = ef(ec);
1715                        });
1716
1717                        return ec;
1718                },
1719
1720                _saveContent: function(){
1721                        // summary:
1722                        //              Saves the content in an onunload event if the editor has not been closed
1723                        // tags:
1724                        //              private
1725
1726                        var saveTextarea = dom.byId(dijit._scopeName + "._editor.RichText.value");
1727                        if(saveTextarea){
1728                                if(saveTextarea.value){
1729                                        saveTextarea.value += this._SEPARATOR;
1730                                }
1731                                saveTextarea.value += this.name + this._NAME_CONTENT_SEP + this.getValue(true);
1732                        }
1733                },
1734
1735
1736                escapeXml: function(/*String*/ str, /*Boolean*/ noSingleQuotes){
1737                        // summary:
1738                        //              Adds escape sequences for special characters in XML.
1739                        //              Optionally skips escapes for single quotes
1740                        // tags:
1741                        //              private
1742
1743                        str = str.replace(/&/gm, "&amp;").replace(/</gm, "&lt;").replace(/>/gm, "&gt;").replace(/"/gm, "&quot;");
1744                        if(!noSingleQuotes){
1745                                str = str.replace(/'/gm, "&#39;");
1746                        }
1747                        return str; // string
1748                },
1749
1750                getNodeHtml: function(/* DomNode */ node){
1751                        // summary:
1752                        //              Deprecated.   Use dijit/_editor/html::_getNodeHtml() instead.
1753                        // tags:
1754                        //              deprecated
1755                        kernel.deprecated('dijit.Editor::getNodeHtml is deprecated', 'use dijit/_editor/html::getNodeHtml instead', 2);
1756                        return htmlapi.getNodeHtml(node); // String
1757                },
1758
1759                getNodeChildrenHtml: function(/* DomNode */ dom){
1760                        // summary:
1761                        //              Deprecated.   Use dijit/_editor/html::getChildrenHtml() instead.
1762                        // tags:
1763                        //              deprecated
1764                        kernel.deprecated('dijit.Editor::getNodeChildrenHtml is deprecated', 'use dijit/_editor/html::getChildrenHtml instead', 2);
1765                        return htmlapi.getChildrenHtml(dom);
1766                },
1767
1768                close: function(/*Boolean?*/ save){
1769                        // summary:
1770                        //              Kills the editor and optionally writes back the modified contents to the
1771                        //              element from which it originated.
1772                        // save:
1773                        //              Whether or not to save the changes. If false, the changes are discarded.
1774                        // tags:
1775                        //              private
1776
1777                        if(this.isClosed){
1778                                return;
1779                        }
1780
1781                        if(!arguments.length){
1782                                save = true;
1783                        }
1784                        if(save){
1785                                this._set("value", this.getValue(true));
1786                        }
1787
1788                        // line height is squashed for iframes
1789                        // FIXME: why was this here? if(this.iframe){ this.domNode.style.lineHeight = null; }
1790
1791                        if(this.interval){
1792                                clearInterval(this.interval);
1793                        }
1794
1795                        if(this._webkitListener){
1796                                // Cleanup of WebKit fix: #9532
1797                                this._webkitListener.remove();
1798                                delete this._webkitListener;
1799                        }
1800
1801                        // Guard against memory leaks on IE (see #9268)
1802                        if(has("ie")){
1803                                this.iframe.onfocus = null;
1804                        }
1805                        this.iframe._loadFunc = null;
1806
1807                        if(this._iframeRegHandle){
1808                                this._iframeRegHandle.remove();
1809                                delete this._iframeRegHandle;
1810                        }
1811
1812                        if(this.textarea){
1813                                var s = this.textarea.style;
1814                                s.position = "";
1815                                s.left = s.top = "";
1816                                if(has("ie")){
1817                                        s.overflow = this.__overflow;
1818                                        this.__overflow = null;
1819                                }
1820                                this.textarea.value = this.value;
1821                                domConstruct.destroy(this.domNode);
1822                                this.domNode = this.textarea;
1823                        }else{
1824                                // Note that this destroys the iframe
1825                                this.domNode.innerHTML = this.value;
1826                        }
1827                        delete this.iframe;
1828
1829                        domClass.remove(this.domNode, this.baseClass);
1830                        this.isClosed = true;
1831                        this.isLoaded = false;
1832
1833                        delete this.editNode;
1834                        delete this.focusNode;
1835
1836                        if(this.window && this.window._frameElement){
1837                                this.window._frameElement = null;
1838                        }
1839
1840                        this.window = null;
1841                        this.document = null;
1842                        this.editingArea = null;
1843                        this.editorObject = null;
1844                },
1845
1846                destroy: function(){
1847                        if(!this.isClosed){
1848                                this.close(false);
1849                        }
1850                        if(this._updateTimer){
1851                                this._updateTimer.remove();
1852                        }
1853                        this.inherited(arguments);
1854                        if(RichText._globalSaveHandler){
1855                                delete RichText._globalSaveHandler[this.id];
1856                        }
1857                },
1858
1859                _removeMozBogus: function(/* String */ html){
1860                        // summary:
1861                        //              Post filter to remove unwanted HTML attributes generated by mozilla
1862                        // tags:
1863                        //              private
1864                        return html.replace(/\stype="_moz"/gi, '').replace(/\s_moz_dirty=""/gi, '').replace(/_moz_resizing="(true|false)"/gi, ''); // String
1865                },
1866                _removeWebkitBogus: function(/* String */ html){
1867                        // summary:
1868                        //              Post filter to remove unwanted HTML attributes generated by webkit
1869                        // tags:
1870                        //              private
1871                        html = html.replace(/\sclass="webkit-block-placeholder"/gi, '');
1872                        html = html.replace(/\sclass="apple-style-span"/gi, '');
1873                        // For some reason copy/paste sometime adds extra meta tags for charset on
1874                        // webkit (chrome) on mac.They need to be removed.  See: #12007"
1875                        html = html.replace(/<meta charset=\"utf-8\" \/>/gi, '');
1876                        return html; // String
1877                },
1878                _normalizeFontStyle: function(/* String */ html){
1879                        // summary:
1880                        //              Convert 'strong' and 'em' to 'b' and 'i'.
1881                        // description:
1882                        //              Moz can not handle strong/em tags correctly, so to help
1883                        //              mozilla and also to normalize output, convert them to 'b' and 'i'.
1884                        //
1885                        //              Note the IE generates 'strong' and 'em' rather than 'b' and 'i'
1886                        // tags:
1887                        //              private
1888                        return html.replace(/<(\/)?strong([ \>])/gi, '<$1b$2')
1889                                .replace(/<(\/)?em([ \>])/gi, '<$1i$2'); // String
1890                },
1891
1892                _preFixUrlAttributes: function(/* String */ html){
1893                        // summary:
1894                        //              Pre-filter to do fixing to href attributes on `<a>` and `<img>` tags
1895                        // tags:
1896                        //              private
1897                        return html.replace(/(?:(<a(?=\s).*?\shref=)("|')(.*?)\2)|(?:(<a\s.*?href=)([^"'][^ >]+))/gi,
1898                                '$1$4$2$3$5$2 _djrealurl=$2$3$5$2')
1899                                .replace(/(?:(<img(?=\s).*?\ssrc=)("|')(.*?)\2)|(?:(<img\s.*?src=)([^"'][^ >]+))/gi,
1900                                '$1$4$2$3$5$2 _djrealurl=$2$3$5$2'); // String
1901                },
1902
1903                /*****************************************************************************
1904                 The following functions implement HTML manipulation commands for various
1905                 browser/contentEditable implementations.  The goal of them is to enforce
1906                 standard behaviors of them.
1907                 ******************************************************************************/
1908
1909                /*** queryCommandEnabled implementations ***/
1910
1911                _browserQueryCommandEnabled: function(command){
1912                        // summary:
1913                        //              Implementation to call to the native queryCommandEnabled of the browser.
1914                        // command:
1915                        //              The command to check.
1916                        // tags:
1917                        //              protected
1918                        if(!command){
1919                                return false;
1920                        }
1921                        var elem = has("ie") < 9 ? this.document.selection.createRange() : this.document;
1922                        try{
1923                                return elem.queryCommandEnabled(command);
1924                        }catch(e){
1925                                return false;
1926                        }
1927                },
1928
1929                _createlinkEnabledImpl: function(/*===== argument =====*/){
1930                        // summary:
1931                        //              This function implements the test for if the create link
1932                        //              command should be enabled or not.
1933                        // argument:
1934                        //              arguments to the exec command, if any.
1935                        // tags:
1936                        //              protected
1937                        var enabled = true;
1938                        if(has("opera")){
1939                                var sel = this.window.getSelection();
1940                                if(sel.isCollapsed){
1941                                        enabled = true;
1942                                }else{
1943                                        enabled = this.document.queryCommandEnabled("createlink");
1944                                }
1945                        }else{
1946                                enabled = this._browserQueryCommandEnabled("createlink");
1947                        }
1948                        return enabled;
1949                },
1950
1951                _unlinkEnabledImpl: function(/*===== argument =====*/){
1952                        // summary:
1953                        //              This function implements the test for if the unlink
1954                        //              command should be enabled or not.
1955                        // argument:
1956                        //              arguments to the exec command, if any.
1957                        // tags:
1958                        //              protected
1959                        var enabled = true;
1960                        if(has("mozilla") || has("webkit")){
1961                                enabled = this.selection.hasAncestorElement("a");
1962                        }else{
1963                                enabled = this._browserQueryCommandEnabled("unlink");
1964                        }
1965                        return enabled;
1966                },
1967
1968                _inserttableEnabledImpl: function(/*===== argument =====*/){
1969                        // summary:
1970                        //              This function implements the test for if the inserttable
1971                        //              command should be enabled or not.
1972                        // argument:
1973                        //              arguments to the exec command, if any.
1974                        // tags:
1975                        //              protected
1976                        var enabled = true;
1977                        if(has("mozilla") || has("webkit")){
1978                                enabled = true;
1979                        }else{
1980                                enabled = this._browserQueryCommandEnabled("inserttable");
1981                        }
1982                        return enabled;
1983                },
1984
1985                _cutEnabledImpl: function(/*===== argument =====*/){
1986                        // summary:
1987                        //              This function implements the test for if the cut
1988                        //              command should be enabled or not.
1989                        // argument:
1990                        //              arguments to the exec command, if any.
1991                        // tags:
1992                        //              protected
1993                        var enabled = true;
1994                        if(has("webkit")){
1995                                // WebKit deems clipboard activity as a security threat and natively would return false
1996                                var sel = this.window.getSelection();
1997                                if(sel){
1998                                        sel = sel.toString();
1999                                }
2000                                enabled = !!sel;
2001                        }else{
2002                                enabled = this._browserQueryCommandEnabled("cut");
2003                        }
2004                        return enabled;
2005                },
2006
2007                _copyEnabledImpl: function(/*===== argument =====*/){
2008                        // summary:
2009                        //              This function implements the test for if the copy
2010                        //              command should be enabled or not.
2011                        // argument:
2012                        //              arguments to the exec command, if any.
2013                        // tags:
2014                        //              protected
2015                        var enabled = true;
2016                        if(has("webkit")){
2017                                // WebKit deems clipboard activity as a security threat and natively would return false
2018                                var sel = this.window.getSelection();
2019                                if(sel){
2020                                        sel = sel.toString();
2021                                }
2022                                enabled = !!sel;
2023                        }else{
2024                                enabled = this._browserQueryCommandEnabled("copy");
2025                        }
2026                        return enabled;
2027                },
2028
2029                _pasteEnabledImpl: function(/*===== argument =====*/){
2030                        // summary:c
2031                        //              This function implements the test for if the paste
2032                        //              command should be enabled or not.
2033                        // argument:
2034                        //              arguments to the exec command, if any.
2035                        // tags:
2036                        //              protected
2037                        var enabled = true;
2038                        if(has("webkit")){
2039                                return true;
2040                        }else{
2041                                enabled = this._browserQueryCommandEnabled("paste");
2042                        }
2043                        return enabled;
2044                },
2045
2046                /*** execCommand implementations ***/
2047
2048                _inserthorizontalruleImpl: function(argument){
2049                        // summary:
2050                        //              This function implements the insertion of HTML 'HR' tags.
2051                        //              into a point on the page.  IE doesn't to it right, so
2052                        //              we have to use an alternate form
2053                        // argument:
2054                        //              arguments to the exec command, if any.
2055                        // tags:
2056                        //              protected
2057                        if(has("ie")){
2058                                return this._inserthtmlImpl("<hr>");
2059                        }
2060                        return this.document.execCommand("inserthorizontalrule", false, argument);
2061                },
2062
2063                _unlinkImpl: function(argument){
2064                        // summary:
2065                        //              This function implements the unlink of an 'a' tag.
2066                        // argument:
2067                        //              arguments to the exec command, if any.
2068                        // tags:
2069                        //              protected
2070                        if((this.queryCommandEnabled("unlink")) && (has("mozilla") || has("webkit"))){
2071                                var a = this.selection.getAncestorElement("a");
2072                                this.selection.selectElement(a);
2073                                return this.document.execCommand("unlink", false, null);
2074                        }
2075                        return this.document.execCommand("unlink", false, argument);
2076                },
2077
2078                _hilitecolorImpl: function(argument){
2079                        // summary:
2080                        //              This function implements the hilitecolor command
2081                        // argument:
2082                        //              arguments to the exec command, if any.
2083                        // tags:
2084                        //              protected
2085                        var returnValue;
2086                        var isApplied = this._handleTextColorOrProperties("hilitecolor", argument);
2087                        if(!isApplied){
2088                                if(has("mozilla")){
2089                                        // mozilla doesn't support hilitecolor properly when useCSS is
2090                                        // set to false (bugzilla #279330)
2091                                        this.document.execCommand("styleWithCSS", false, true);
2092                                        console.log("Executing color command.");
2093                                        returnValue = this.document.execCommand("hilitecolor", false, argument);
2094                                        this.document.execCommand("styleWithCSS", false, false);
2095                                }else{
2096                                        returnValue = this.document.execCommand("hilitecolor", false, argument);
2097                                }
2098                        }
2099                        return returnValue;
2100                },
2101
2102                _backcolorImpl: function(argument){
2103                        // summary:
2104                        //              This function implements the backcolor command
2105                        // argument:
2106                        //              arguments to the exec command, if any.
2107                        // tags:
2108                        //              protected
2109                        if(has("ie")){
2110                                // Tested under IE 6 XP2, no problem here, comment out
2111                                // IE weirdly collapses ranges when we exec these commands, so prevent it
2112                                //      var tr = this.document.selection.createRange();
2113                                argument = argument ? argument : null;
2114                        }
2115                        var isApplied = this._handleTextColorOrProperties("backcolor", argument);
2116                        if(!isApplied){
2117                                isApplied = this.document.execCommand("backcolor", false, argument);
2118                        }
2119                        return isApplied;
2120                },
2121
2122                _forecolorImpl: function(argument){
2123                        // summary:
2124                        //              This function implements the forecolor command
2125                        // argument:
2126                        //              arguments to the exec command, if any.
2127                        // tags:
2128                        //              protected
2129                        if(has("ie")){
2130                                // Tested under IE 6 XP2, no problem here, comment out
2131                                // IE weirdly collapses ranges when we exec these commands, so prevent it
2132                                //      var tr = this.document.selection.createRange();
2133                                argument = argument ? argument : null;
2134                        }
2135                        var isApplied = false;
2136                        isApplied = this._handleTextColorOrProperties("forecolor", argument);
2137                        if(!isApplied){
2138                                isApplied = this.document.execCommand("forecolor", false, argument);
2139                        }
2140                        return isApplied;
2141                },
2142
2143                _inserthtmlImpl: function(argument){
2144                        // summary:
2145                        //              This function implements the insertion of HTML content into
2146                        //              a point on the page.
2147                        // argument:
2148                        //              The content to insert, if any.
2149                        // tags:
2150                        //              protected
2151                        argument = this._preFilterContent(argument);
2152                        var rv = true;
2153                        if(has("ie") < 9){
2154                                var insertRange = this.document.selection.createRange();
2155                                if(this.document.selection.type.toUpperCase() === 'CONTROL'){
2156                                        var n = insertRange.item(0);
2157                                        while(insertRange.length){
2158                                                insertRange.remove(insertRange.item(0));
2159                                        }
2160                                        n.outerHTML = argument;
2161                                }else{
2162                                        insertRange.pasteHTML(argument);
2163                                }
2164                                insertRange.select();
2165                        }else if(has("trident") < 8){
2166                                var insertRange;
2167                                var selection = rangeapi.getSelection(this.window);
2168                                if(selection && selection.rangeCount && selection.getRangeAt){
2169                                        insertRange = selection.getRangeAt(0);
2170                                        insertRange.deleteContents();
2171
2172                                        var div = domConstruct.create('div');
2173                                        div.innerHTML = argument;
2174                                        var node, lastNode;
2175                                        var n = this.document.createDocumentFragment();
2176                                        while((node = div.firstChild)){
2177                                                lastNode = n.appendChild(node);
2178                                        }
2179                                        insertRange.insertNode(n);
2180                                        if(lastNode) {
2181                                                insertRange = insertRange.cloneRange();
2182                                                insertRange.setStartAfter(lastNode);
2183                                                insertRange.collapse(false);
2184                                                selection.removeAllRanges();
2185                                                selection.addRange(insertRange);
2186                                        }
2187                                }
2188                        }else if(has("mozilla") && !argument.length){
2189                                //mozilla can not inserthtml an empty html to delete current selection
2190                                //so we delete the selection instead in this case
2191                                this.selection.remove(); // FIXME
2192                        }else{
2193                                rv = this.document.execCommand("inserthtml", false, argument);
2194                        }
2195                        return rv;
2196                },
2197
2198                _boldImpl: function(argument){
2199                        // summary:
2200                        //              This function implements an over-ride of the bold command.
2201                        // argument:
2202                        //              Not used, operates by selection.
2203                        // tags:
2204                        //              protected
2205                        var applied = false;
2206                        if(has("ie")){
2207                                this._adaptIESelection();
2208                                applied = this._adaptIEFormatAreaAndExec("bold");
2209                        }
2210                        if(!applied){
2211                                applied = this.document.execCommand("bold", false, argument);
2212                        }
2213                        return applied;
2214                },
2215
2216                _italicImpl: function(argument){
2217                        // summary:
2218                        //              This function implements an over-ride of the italic command.
2219                        // argument:
2220                        //              Not used, operates by selection.
2221                        // tags:
2222                        //              protected
2223                        var applied = false;
2224                        if(has("ie")){
2225                                this._adaptIESelection();
2226                                applied = this._adaptIEFormatAreaAndExec("italic");
2227                        }
2228                        if(!applied){
2229                                applied = this.document.execCommand("italic", false, argument);
2230                        }
2231                        return applied;
2232                },
2233
2234                _underlineImpl: function(argument){
2235                        // summary:
2236                        //              This function implements an over-ride of the underline command.
2237                        // argument:
2238                        //              Not used, operates by selection.
2239                        // tags:
2240                        //              protected
2241                        var applied = false;
2242                        if(has("ie")){
2243                                this._adaptIESelection();
2244                                applied = this._adaptIEFormatAreaAndExec("underline");
2245                        }
2246                        if(!applied){
2247                                applied = this.document.execCommand("underline", false, argument);
2248                        }
2249                        return applied;
2250                },
2251
2252                _strikethroughImpl: function(argument){
2253                        // summary:
2254                        //              This function implements an over-ride of the strikethrough command.
2255                        // argument:
2256                        //              Not used, operates by selection.
2257                        // tags:
2258                        //              protected
2259                        var applied = false;
2260                        if(has("ie")){
2261                                this._adaptIESelection();
2262                                applied = this._adaptIEFormatAreaAndExec("strikethrough");
2263                        }
2264                        if(!applied){
2265                                applied = this.document.execCommand("strikethrough", false, argument);
2266                        }
2267                        return applied;
2268                },
2269
2270                _superscriptImpl: function(argument){
2271                        // summary:
2272                        //              This function implements an over-ride of the superscript command.
2273                        // argument:
2274                        //              Not used, operates by selection.
2275                        // tags:
2276                        //              protected
2277                        var applied = false;
2278                        if(has("ie")){
2279                                this._adaptIESelection();
2280                                applied = this._adaptIEFormatAreaAndExec("superscript");
2281                        }
2282                        if(!applied){
2283                                applied = this.document.execCommand("superscript", false, argument);
2284                        }
2285                        return applied;
2286                },
2287
2288                _subscriptImpl: function(argument){
2289                        // summary:
2290                        //              This function implements an over-ride of the superscript command.
2291                        // argument:
2292                        //              Not used, operates by selection.
2293                        // tags:
2294                        //              protected
2295                        var applied = false;
2296                        if(has("ie")){
2297                                this._adaptIESelection();
2298                                applied = this._adaptIEFormatAreaAndExec("subscript");
2299
2300                        }
2301                        if(!applied){
2302                                applied = this.document.execCommand("subscript", false, argument);
2303                        }
2304                        return applied;
2305                },
2306
2307                _fontnameImpl: function(argument){
2308                        // summary:
2309                        //              This function implements the fontname command
2310                        // argument:
2311                        //              arguments to the exec command, if any.
2312                        // tags:
2313                        //              protected
2314                        var isApplied;
2315                        if(has("ie")){
2316                                isApplied = this._handleTextColorOrProperties("fontname", argument);
2317                        }
2318                        if(!isApplied){
2319                                isApplied = this.document.execCommand("fontname", false, argument);
2320                        }
2321                        return isApplied;
2322                },
2323
2324                _fontsizeImpl: function(argument){
2325                        // summary:
2326                        //              This function implements the fontsize command
2327                        // argument:
2328                        //              arguments to the exec command, if any.
2329                        // tags:
2330                        //              protected
2331                        var isApplied;
2332                        if(has("ie")){
2333                                isApplied = this._handleTextColorOrProperties("fontsize", argument);
2334                        }
2335                        if(!isApplied){
2336                                isApplied = this.document.execCommand("fontsize", false, argument);
2337                        }
2338                        return isApplied;
2339                },
2340
2341                _insertorderedlistImpl: function(argument){
2342                        // summary:
2343                        //              This function implements the insertorderedlist command
2344                        // argument:
2345                        //              arguments to the exec command, if any.
2346                        // tags:
2347                        //              protected
2348                        var applied = false;
2349                        if(has("ie")){
2350                                applied = this._adaptIEList("insertorderedlist", argument);
2351                        }
2352                        if(!applied){
2353                                applied = this.document.execCommand("insertorderedlist", false, argument);
2354                        }
2355                        return applied;
2356                },
2357
2358                _insertunorderedlistImpl: function(argument){
2359                        // summary:
2360                        //              This function implements the insertunorderedlist command
2361                        // argument:
2362                        //              arguments to the exec command, if any.
2363                        // tags:
2364                        //              protected
2365                        var applied = false;
2366                        if(has("ie")){
2367                                applied = this._adaptIEList("insertunorderedlist", argument);
2368                        }
2369                        if(!applied){
2370                                applied = this.document.execCommand("insertunorderedlist", false, argument);
2371                        }
2372                        return applied;
2373                },
2374
2375                getHeaderHeight: function(){
2376                        // summary:
2377                        //              A function for obtaining the height of the header node
2378                        return this._getNodeChildrenHeight(this.header); // Number
2379                },
2380
2381                getFooterHeight: function(){
2382                        // summary:
2383                        //              A function for obtaining the height of the footer node
2384                        return this._getNodeChildrenHeight(this.footer); // Number
2385                },
2386
2387                _getNodeChildrenHeight: function(node){
2388                        // summary:
2389                        //              An internal function for computing the cumulative height of all child nodes of 'node'
2390                        // node:
2391                        //              The node to process the children of;
2392                        var h = 0;
2393                        if(node && node.childNodes){
2394                                // IE didn't compute it right when position was obtained on the node directly is some cases,
2395                                // so we have to walk over all the children manually.
2396                                var i;
2397                                for(i = 0; i < node.childNodes.length; i++){
2398                                        var size = domGeometry.position(node.childNodes[i]);
2399                                        h += size.h;
2400                                }
2401                        }
2402                        return h; // Number
2403                },
2404
2405                _isNodeEmpty: function(node, startOffset){
2406                        // summary:
2407                        //              Function to test if a node is devoid of real content.
2408                        // node:
2409                        //              The node to check.
2410                        // tags:
2411                        //              private.
2412                        if(node.nodeType === 1/*element*/){
2413                                if(node.childNodes.length > 0){
2414                                        return this._isNodeEmpty(node.childNodes[0], startOffset);      // huh?   why test just first child?
2415                                }
2416                                return true;
2417                        }else if(node.nodeType === 3/*text*/){
2418                                return (node.nodeValue.substring(startOffset) === "");
2419                        }
2420                        return false;
2421                },
2422
2423                _removeStartingRangeFromRange: function(node, range){
2424                        // summary:
2425                        //              Function to adjust selection range by removing the current
2426                        //              start node.
2427                        // node:
2428                        //              The node to remove from the starting range.
2429                        // range:
2430                        //              The range to adapt.
2431                        // tags:
2432                        //              private
2433                        if(node.nextSibling){
2434                                range.setStart(node.nextSibling, 0);
2435                        }else{
2436                                var parent = node.parentNode;
2437                                while(parent && parent.nextSibling == null){
2438                                        //move up the tree until we find a parent that has another node, that node will be the next node
2439                                        parent = parent.parentNode;
2440                                }
2441                                if(parent){
2442                                        range.setStart(parent.nextSibling, 0);
2443                                }
2444                        }
2445                        return range;
2446                },
2447
2448                _adaptIESelection: function(){
2449                        // summary:
2450                        //              Function to adapt the IE range by removing leading 'newlines'
2451                        //              Needed to fix issue with bold/italics/underline not working if
2452                        //              range included leading 'newlines'.
2453                        //              In IE, if a user starts a selection at the very end of a line,
2454                        //              then the native browser commands will fail to execute correctly.
2455                        //              To work around the issue,  we can remove all empty nodes from
2456                        //              the start of the range selection.
2457                        var selection = rangeapi.getSelection(this.window);
2458                        if(selection && selection.rangeCount && !selection.isCollapsed){
2459                                var range = selection.getRangeAt(0);
2460                                var firstNode = range.startContainer;
2461                                var startOffset = range.startOffset;
2462
2463                                while(firstNode.nodeType === 3/*text*/ && startOffset >= firstNode.length && firstNode.nextSibling){
2464                                        //traverse the text nodes until we get to the one that is actually highlighted
2465                                        startOffset = startOffset - firstNode.length;
2466                                        firstNode = firstNode.nextSibling;
2467                                }
2468
2469                                //Remove the starting ranges until the range does not start with an empty node.
2470                                var lastNode = null;
2471                                while(this._isNodeEmpty(firstNode, startOffset) && firstNode !== lastNode){
2472                                        lastNode = firstNode; //this will break the loop in case we can't find the next sibling
2473                                        range = this._removeStartingRangeFromRange(firstNode, range); //move the start container to the next node in the range
2474                                        firstNode = range.startContainer;
2475                                        startOffset = 0; //start at the beginning of the new starting range
2476                                }
2477                                selection.removeAllRanges();// this will work as long as users cannot select multiple ranges. I have not been able to do that in the editor.
2478                                selection.addRange(range);
2479                        }
2480                },
2481
2482                _adaptIEFormatAreaAndExec: function(command){
2483                        // summary:
2484                        //              Function to handle IE's quirkiness regarding how it handles
2485                        //              format commands on a word.  This involves a lit of node splitting
2486                        //              and format cloning.
2487                        // command:
2488                        //              The format command, needed to check if the desired
2489                        //              command is true or not.
2490                        var selection = rangeapi.getSelection(this.window);
2491                        var doc = this.document;
2492                        var rs, ret, range, txt, startNode, endNode, breaker, sNode;
2493                        if(command && selection && selection.isCollapsed){
2494                                var isApplied = this.queryCommandValue(command);
2495                                if(isApplied){
2496
2497                                        // We have to split backwards until we hit the format
2498                                        var nNames = this._tagNamesForCommand(command);
2499                                        range = selection.getRangeAt(0);
2500                                        var fs = range.startContainer;
2501                                        if(fs.nodeType === 3){
2502                                                var offset = range.endOffset;
2503                                                if(fs.length < offset){
2504                                                        //We are not looking from the right node, try to locate the correct one
2505                                                        ret = this._adjustNodeAndOffset(rs, offset);
2506                                                        fs = ret.node;
2507                                                        offset = ret.offset;
2508                                                }
2509                                        }
2510                                        var topNode;
2511                                        while(fs && fs !== this.editNode){
2512                                                // We have to walk back and see if this is still a format or not.
2513                                                // Hm, how do I do this?
2514                                                var tName = fs.tagName ? fs.tagName.toLowerCase() : "";
2515                                                if(array.indexOf(nNames, tName) > -1){
2516                                                        topNode = fs;
2517                                                        break;
2518                                                }
2519                                                fs = fs.parentNode;
2520                                        }
2521
2522                                        // Okay, we have a stopping place, time to split things apart.
2523                                        if(topNode){
2524                                                // Okay, we know how far we have to split backwards, so we have to split now.
2525                                                rs = range.startContainer;
2526                                                var newblock = doc.createElement(topNode.tagName);
2527                                                domConstruct.place(newblock, topNode, "after");
2528                                                if(rs && rs.nodeType === 3){
2529                                                        // Text node, we have to split it.
2530                                                        var nodeToMove, tNode;
2531                                                        var endOffset = range.endOffset;
2532                                                        if(rs.length < endOffset){
2533                                                                //We are not splitting the right node, try to locate the correct one
2534                                                                ret = this._adjustNodeAndOffset(rs, endOffset);
2535                                                                rs = ret.node;
2536                                                                endOffset = ret.offset;
2537                                                        }
2538
2539                                                        txt = rs.nodeValue;
2540                                                        startNode = doc.createTextNode(txt.substring(0, endOffset));
2541                                                        var endText = txt.substring(endOffset, txt.length);
2542                                                        if(endText){
2543                                                                endNode = doc.createTextNode(endText);
2544                                                        }
2545                                                        // Place the split, then remove original nodes.
2546                                                        domConstruct.place(startNode, rs, "before");
2547                                                        if(endNode){
2548                                                                breaker = doc.createElement("span");
2549                                                                breaker.className = "ieFormatBreakerSpan";
2550                                                                domConstruct.place(breaker, rs, "after");
2551                                                                domConstruct.place(endNode, breaker, "after");
2552                                                                endNode = breaker;
2553                                                        }
2554                                                        domConstruct.destroy(rs);
2555
2556                                                        // Okay, we split the text.  Now we need to see if we're
2557                                                        // parented to the block element we're splitting and if
2558                                                        // not, we have to split all the way up.  Ugh.
2559                                                        var parentC = startNode.parentNode;
2560                                                        var tagList = [];
2561                                                        var tagData;
2562                                                        while(parentC !== topNode){
2563                                                                var tg = parentC.tagName;
2564                                                                tagData = {tagName: tg};
2565                                                                tagList.push(tagData);
2566
2567                                                                var newTg = doc.createElement(tg);
2568                                                                // Clone over any 'style' data.
2569                                                                if(parentC.style){
2570                                                                        if(newTg.style){
2571                                                                                if(parentC.style.cssText){
2572                                                                                        newTg.style.cssText = parentC.style.cssText;
2573                                                                                        tagData.cssText = parentC.style.cssText;
2574                                                                                }
2575                                                                        }
2576                                                                }
2577                                                                // If font also need to clone over any font data.
2578                                                                if(parentC.tagName === "FONT"){
2579                                                                        if(parentC.color){
2580                                                                                newTg.color = parentC.color;
2581                                                                                tagData.color = parentC.color;
2582                                                                        }
2583                                                                        if(parentC.face){
2584                                                                                newTg.face = parentC.face;
2585                                                                                tagData.face = parentC.face;
2586                                                                        }
2587                                                                        if(parentC.size){  // this check was necessary on IE
2588                                                                                newTg.size = parentC.size;
2589                                                                                tagData.size = parentC.size;
2590                                                                        }
2591                                                                }
2592                                                                if(parentC.className){
2593                                                                        newTg.className = parentC.className;
2594                                                                        tagData.className = parentC.className;
2595                                                                }
2596
2597                                                                // Now move end node and every sibling
2598                                                                // after it over into the new tag.
2599                                                                if(endNode){
2600                                                                        nodeToMove = endNode;
2601                                                                        while(nodeToMove){
2602                                                                                tNode = nodeToMove.nextSibling;
2603                                                                                newTg.appendChild(nodeToMove);
2604                                                                                nodeToMove = tNode;
2605                                                                        }
2606                                                                }
2607                                                                if(newTg.tagName == parentC.tagName){
2608                                                                        breaker = doc.createElement("span");
2609                                                                        breaker.className = "ieFormatBreakerSpan";
2610                                                                        domConstruct.place(breaker, parentC, "after");
2611                                                                        domConstruct.place(newTg, breaker, "after");
2612                                                                }else{
2613                                                                        domConstruct.place(newTg, parentC, "after");
2614                                                                }
2615                                                                startNode = parentC;
2616                                                                endNode = newTg;
2617                                                                parentC = parentC.parentNode;
2618                                                        }
2619
2620                                                        // Lastly, move the split out all the split tags
2621                                                        // to the new block as they should now be split properly.
2622                                                        if(endNode){
2623                                                                nodeToMove = endNode;
2624                                                                if(nodeToMove.nodeType === 1 || (nodeToMove.nodeType === 3 && nodeToMove.nodeValue)){
2625                                                                        // Non-blank text and non-text nodes need to clear out that blank space
2626                                                                        // before moving the contents.
2627                                                                        newblock.innerHTML = "";
2628                                                                }
2629                                                                while(nodeToMove){
2630                                                                        tNode = nodeToMove.nextSibling;
2631                                                                        newblock.appendChild(nodeToMove);
2632                                                                        nodeToMove = tNode;
2633                                                                }
2634                                                        }
2635
2636                                                        // We had intermediate tags, we have to now recreate them inbetween the split
2637                                                        // and restore what styles, classnames, etc, we can.
2638                                                        var newrange;
2639                                                        if(tagList.length){
2640                                                                tagData = tagList.pop();
2641                                                                var newContTag = doc.createElement(tagData.tagName);
2642                                                                if(tagData.cssText && newContTag.style){
2643                                                                        newContTag.style.cssText = tagData.cssText;
2644                                                                }
2645                                                                if(tagData.className){
2646                                                                        newContTag.className = tagData.className;
2647                                                                }
2648                                                                if(tagData.tagName === "FONT"){
2649                                                                        if(tagData.color){
2650                                                                                newContTag.color = tagData.color;
2651                                                                        }
2652                                                                        if(tagData.face){
2653                                                                                newContTag.face = tagData.face;
2654                                                                        }
2655                                                                        if(tagData.size){
2656                                                                                newContTag.size = tagData.size;
2657                                                                        }
2658                                                                }
2659                                                                domConstruct.place(newContTag, newblock, "before");
2660                                                                while(tagList.length){
2661                                                                        tagData = tagList.pop();
2662                                                                        var newTgNode = doc.createElement(tagData.tagName);
2663                                                                        if(tagData.cssText && newTgNode.style){
2664                                                                                newTgNode.style.cssText = tagData.cssText;
2665                                                                        }
2666                                                                        if(tagData.className){
2667                                                                                newTgNode.className = tagData.className;
2668                                                                        }
2669                                                                        if(tagData.tagName === "FONT"){
2670                                                                                if(tagData.color){
2671                                                                                        newTgNode.color = tagData.color;
2672                                                                                }
2673                                                                                if(tagData.face){
2674                                                                                        newTgNode.face = tagData.face;
2675                                                                                }
2676                                                                                if(tagData.size){
2677                                                                                        newTgNode.size = tagData.size;
2678                                                                                }
2679                                                                        }
2680                                                                        newContTag.appendChild(newTgNode);
2681                                                                        newContTag = newTgNode;
2682                                                                }
2683
2684                                                                // Okay, everything is theoretically split apart and removed from the content
2685                                                                // so insert the dummy text to select, select it, then
2686                                                                // clear to position cursor.
2687                                                                sNode = doc.createTextNode(".");
2688                                                                breaker.appendChild(sNode);
2689                                                                newContTag.appendChild(sNode);
2690                                                                newrange = rangeapi.create(this.window);
2691                                                                newrange.setStart(sNode, 0);
2692                                                                newrange.setEnd(sNode, sNode.length);
2693                                                                selection.removeAllRanges();
2694                                                                selection.addRange(newrange);
2695                                                                this.selection.collapse(false);
2696                                                                sNode.parentNode.innerHTML = "";
2697                                                        }else{
2698                                                                // No extra tags, so we have to insert a breaker point and rely
2699                                                                // on filters to remove it later.
2700                                                                breaker = doc.createElement("span");
2701                                                                breaker.className = "ieFormatBreakerSpan";
2702                                                                sNode = doc.createTextNode(".");
2703                                                                breaker.appendChild(sNode);
2704                                                                domConstruct.place(breaker, newblock, "before");
2705                                                                newrange = rangeapi.create(this.window);
2706                                                                newrange.setStart(sNode, 0);
2707                                                                newrange.setEnd(sNode, sNode.length);
2708                                                                selection.removeAllRanges();
2709                                                                selection.addRange(newrange);
2710                                                                this.selection.collapse(false);
2711                                                                sNode.parentNode.innerHTML = "";
2712                                                        }
2713                                                        if(!newblock.firstChild){
2714                                                                // Empty, we don't need it.  Split was at end or similar
2715                                                                // So, remove it.
2716                                                                domConstruct.destroy(newblock);
2717                                                        }
2718                                                        return true;
2719                                                }
2720                                        }
2721                                        return false;
2722                                }else{
2723                                        range = selection.getRangeAt(0);
2724                                        rs = range.startContainer;
2725                                        if(rs && rs.nodeType === 3){
2726                                                // Text node, we have to split it.
2727                                                var offset = range.startOffset;
2728                                                if(rs.length < offset){
2729                                                        //We are not splitting the right node, try to locate the correct one
2730                                                        ret = this._adjustNodeAndOffset(rs, offset);
2731                                                        rs = ret.node;
2732                                                        offset = ret.offset;
2733                                                }
2734                                                txt = rs.nodeValue;
2735                                                startNode = doc.createTextNode(txt.substring(0, offset));
2736                                                var endText = txt.substring(offset);
2737                                                if(endText !== ""){
2738                                                        endNode = doc.createTextNode(txt.substring(offset));
2739                                                }
2740                                                // Create a space, we'll select and bold it, so
2741                                                // the whole word doesn't get bolded
2742                                                breaker = doc.createElement("span");
2743                                                sNode = doc.createTextNode(".");
2744                                                breaker.appendChild(sNode);
2745                                                if(startNode.length){
2746                                                        domConstruct.place(startNode, rs, "after");
2747                                                }else{
2748                                                        startNode = rs;
2749                                                }
2750                                                domConstruct.place(breaker, startNode, "after");
2751                                                if(endNode){
2752                                                        domConstruct.place(endNode, breaker, "after");
2753                                                }
2754                                                domConstruct.destroy(rs);
2755                                                var newrange = rangeapi.create(this.window);
2756                                                newrange.setStart(sNode, 0);
2757                                                newrange.setEnd(sNode, sNode.length);
2758                                                selection.removeAllRanges();
2759                                                selection.addRange(newrange);
2760                                                doc.execCommand(command);
2761                                                domConstruct.place(breaker.firstChild, breaker, "before");
2762                                                domConstruct.destroy(breaker);
2763                                                newrange.setStart(sNode, 0);
2764                                                newrange.setEnd(sNode, sNode.length);
2765                                                selection.removeAllRanges();
2766                                                selection.addRange(newrange);
2767                                                this.selection.collapse(false);
2768                                                sNode.parentNode.innerHTML = "";
2769                                                return true;
2770                                        }
2771                                }
2772                        }else{
2773                                return false;
2774                        }
2775                },
2776
2777                _adaptIEList: function(command /*===== , argument =====*/){
2778                        // summary:
2779                        //              This function handles normalizing the IE list behavior as
2780                        //              much as possible.
2781                        // command:
2782                        //              The list command to execute.
2783                        // argument:
2784                        //              Any additional argument.
2785                        // tags:
2786                        //              private
2787                        var selection = rangeapi.getSelection(this.window);
2788                        if(selection.isCollapsed){
2789                                // In the case of no selection, lets commonize the behavior and
2790                                // make sure that it indents if needed.
2791                                if(selection.rangeCount && !this.queryCommandValue(command)){
2792                                        var range = selection.getRangeAt(0);
2793                                        var sc = range.startContainer;
2794                                        if(sc && sc.nodeType == 3){
2795                                                // text node.  Lets see if there is a node before it that isn't
2796                                                // some sort of breaker.
2797                                                if(!range.startOffset){
2798                                                        // We're at the beginning of a text area.  It may have been br split
2799                                                        // Who knows?  In any event, we must create the list manually
2800                                                        // or IE may shove too much into the list element.  It seems to
2801                                                        // grab content before the text node too if it's br split.
2802                                                        // Why can't IE work like everyone else?
2803
2804                                                        // Create a space, we'll select and bold it, so
2805                                                        // the whole word doesn't get bolded
2806                                                        var lType = "ul";
2807                                                        if(command === "insertorderedlist"){
2808                                                                lType = "ol";
2809                                                        }
2810                                                        var list = this.document.createElement(lType);
2811                                                        var li = domConstruct.create("li", null, list);
2812                                                        domConstruct.place(list, sc, "before");
2813                                                        // Move in the text node as part of the li.
2814                                                        li.appendChild(sc);
2815                                                        // We need a br after it or the enter key handler
2816                                                        // sometimes throws errors.
2817                                                        domConstruct.create("br", null, list, "after");
2818                                                        // Okay, now lets move our cursor to the beginning.
2819                                                        var newrange = rangeapi.create(this.window);
2820                                                        newrange.setStart(sc, 0);
2821                                                        newrange.setEnd(sc, sc.length);
2822                                                        selection.removeAllRanges();
2823                                                        selection.addRange(newrange);
2824                                                        this.selection.collapse(true);
2825                                                        return true;
2826                                                }
2827                                        }
2828                                }
2829                        }
2830                        return false;
2831                },
2832
2833                _handleTextColorOrProperties: function(command, argument){
2834                        // summary:
2835                        //              This function handles appplying text color as best it is
2836                        //              able to do so when the selection is collapsed, making the
2837                        //              behavior cross-browser consistent. It also handles the name
2838                        //              and size for IE.
2839                        // command:
2840                        //              The command.
2841                        // argument:
2842                        //              Any additional arguments.
2843                        // tags:
2844                        //              private
2845                        var selection = rangeapi.getSelection(this.window);
2846                        var doc = this.document;
2847                        var rs, ret, range, txt, startNode, endNode, breaker, sNode;
2848                        argument = argument || null;
2849                        if(command && selection && selection.isCollapsed){
2850                                if(selection.rangeCount){
2851                                        range = selection.getRangeAt(0);
2852                                        rs = range.startContainer;
2853                                        if(rs && rs.nodeType === 3){
2854                                                // Text node, we have to split it.
2855                                                var offset = range.startOffset;
2856                                                if(rs.length < offset){
2857                                                        //We are not splitting the right node, try to locate the correct one
2858                                                        ret = this._adjustNodeAndOffset(rs, offset);
2859                                                        rs = ret.node;
2860                                                        offset = ret.offset;
2861                                                }
2862                                                txt = rs.nodeValue;
2863                                                startNode = doc.createTextNode(txt.substring(0, offset));
2864                                                var endText = txt.substring(offset);
2865                                                if(endText !== ""){
2866                                                        endNode = doc.createTextNode(txt.substring(offset));
2867                                                }
2868                                                // Create a space, we'll select and bold it, so
2869                                                // the whole word doesn't get bolded
2870                                                breaker = doc.createElement("span");
2871                                                sNode = doc.createTextNode(".");
2872                                                breaker.appendChild(sNode);
2873                                                // Create a junk node to avoid it trying to style the breaker.
2874                                                // This will get destroyed later.
2875                                                var extraSpan = doc.createElement("span");
2876                                                breaker.appendChild(extraSpan);
2877                                                if(startNode.length){
2878                                                        domConstruct.place(startNode, rs, "after");
2879                                                }else{
2880                                                        startNode = rs;
2881                                                }
2882                                                domConstruct.place(breaker, startNode, "after");
2883                                                if(endNode){
2884                                                        domConstruct.place(endNode, breaker, "after");
2885                                                }
2886                                                domConstruct.destroy(rs);
2887                                                var newrange = rangeapi.create(this.window);
2888                                                newrange.setStart(sNode, 0);
2889                                                newrange.setEnd(sNode, sNode.length);
2890                                                selection.removeAllRanges();
2891                                                selection.addRange(newrange);
2892                                                if(has("webkit")){
2893                                                        // WebKit is frustrating with positioning the cursor.
2894                                                        // It stinks to have a selected space, but there really
2895                                                        // isn't much choice here.
2896                                                        var style = "color";
2897                                                        if(command === "hilitecolor" || command === "backcolor"){
2898                                                                style = "backgroundColor";
2899                                                        }
2900                                                        domStyle.set(breaker, style, argument);
2901                                                        this.selection.remove();
2902                                                        domConstruct.destroy(extraSpan);
2903                                                        breaker.innerHTML = "&#160;";   // &nbsp;
2904                                                        this.selection.selectElement(breaker);
2905                                                        this.focus();
2906                                                }else{
2907                                                        this.execCommand(command, argument);
2908                                                        domConstruct.place(breaker.firstChild, breaker, "before");
2909                                                        domConstruct.destroy(breaker);
2910                                                        newrange.setStart(sNode, 0);
2911                                                        newrange.setEnd(sNode, sNode.length);
2912                                                        selection.removeAllRanges();
2913                                                        selection.addRange(newrange);
2914                                                        this.selection.collapse(false);
2915                                                        sNode.parentNode.removeChild(sNode);
2916                                                }
2917                                                return true;
2918                                        }
2919                                }
2920                        }
2921                        return false;
2922                },
2923
2924                _adjustNodeAndOffset: function(/*DomNode*/node, /*Int*/offset){
2925                        // summary:
2926                        //              In the case there are multiple text nodes in a row the offset may not be within the node.
2927                        //              If the offset is larger than the node length, it will attempt to find
2928                        //              the next text sibling until it locates the text node in which the offset refers to
2929                        // node:
2930                        //              The node to check.
2931                        // offset:
2932                        //              The position to find within the text node
2933                        // tags:
2934                        //              private.
2935                        while(node.length < offset && node.nextSibling && node.nextSibling.nodeType === 3){
2936                                //Adjust the offset and node in the case of multiple text nodes in a row
2937                                offset = offset - node.length;
2938                                node = node.nextSibling;
2939                        }
2940                        return {"node": node, "offset": offset};
2941                },
2942
2943                _tagNamesForCommand: function(command){
2944                        // summary:
2945                        //              Function to return the tab names that are associated
2946                        //              with a particular style.
2947                        // command: String
2948                        //              The command to return tags for.
2949                        // tags:
2950                        //              private
2951                        if(command === "bold"){
2952                                return ["b", "strong"];
2953                        }else if(command === "italic"){
2954                                return ["i", "em"];
2955                        }else if(command === "strikethrough"){
2956                                return ["s", "strike"];
2957                        }else if(command === "superscript"){
2958                                return ["sup"];
2959                        }else if(command === "subscript"){
2960                                return ["sub"];
2961                        }else if(command === "underline"){
2962                                return ["u"];
2963                        }
2964                        return [];
2965                },
2966
2967                _stripBreakerNodes: function(/*DOMNode*/ node){
2968                        // summary:
2969                        //              Function for stripping out the breaker spans inserted by the formatting command.
2970                        //              Registered as a filter for IE, handles the breaker spans needed to fix up
2971                        //              How bold/italic/etc, work when selection is collapsed (single cursor).
2972                        if(!this.isLoaded){
2973                                return;
2974                        } // this method requires init to be complete
2975                        query(".ieFormatBreakerSpan", node).forEach(function(b){
2976                                while(b.firstChild){
2977                                        domConstruct.place(b.firstChild, b, "before");
2978                                }
2979                                domConstruct.destroy(b);
2980                        });
2981                        return node;
2982                },
2983
2984                _stripTrailingEmptyNodes: function(/*DOMNode*/ node){
2985                        // summary:
2986                        //              Function for stripping trailing nodes without any text, excluding trailing nodes
2987                        //              like <img> or <div><img></div>, even though they don't have text either.
2988
2989                        function isEmpty(node){
2990                                // If not for old IE we could check for Element children by node.firstElementChild
2991                                return (/^(p|div|br)$/i.test(node.nodeName) && node.children.length == 0 &&
2992                                        /^[\s\xA0]*$/.test(node.textContent || node.innerText || "")) ||
2993                                        (node.nodeType === 3/*text*/ && /^[\s\xA0]*$/.test(node.nodeValue));
2994                        }
2995                        while(node.lastChild && isEmpty(node.lastChild)){
2996                                domConstruct.destroy(node.lastChild);
2997                        }
2998
2999                        return node;
3000                }
3001        });
3002
3003        return RichText;
3004
3005});
Note: See TracBrowser for help on using the repository browser.