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