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