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.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
17 "dojo/query", // query
18 "dojo/ready", // ready
19 "dojo/sniff", // has("ie") has("mozilla") has("opera") has("safari") has("webkit")
20 "dojo/topic", // topic.publish() (publish)
21 "dojo/_base/unload", // unload
22 "dojo/_base/url", // url
23 "dojo/_base/window", // win.global
30 "../main" // dijit._scopeName
31 ], function(array
, config
, declare
, Deferred
, dom
, domAttr
, domClass
, domConstruct
, domGeometry
, domStyle
,
32 event
, kernel
, keys
, lang
, on
, query
, ready
, has
, topic
, unload
, _Url
, win
,
33 _Widget
, _CssStateMixin
, selectionapi
, rangeapi
, htmlapi
, focus
, dijit
){
36 // dijit/_editor/RichText
38 // dijit/_editor/RichText is the core of dijit/Editor, which provides basic
39 // WYSIWYG editing features.
41 // if you want to allow for rich text saving with back/forward actions, you must add a text area to your page with
42 // the id==dijit._scopeName + "._editor.RichText.value" (typically "dijit/_editor/RichText.value). For example,
43 // something like this will work:
45 // <textarea id="dijit._editor.RichText.value" style="display:none;position:absolute;top:-100px;left:-100px;height:3px;width:3px;overflow:hidden;"></textarea>
48 var RichText
= declare("dijit._editor.RichText", [_Widget
, _CssStateMixin
], {
50 // dijit/_editor/RichText is the core of dijit.Editor, which provides basic
51 // WYSIWYG editing features.
54 // dijit/_editor/RichText is the core of dijit.Editor, which provides basic
55 // WYSIWYG editing features. It also encapsulates the differences
56 // of different js engines for various browsers. Do not use this widget
57 // with an HTML <TEXTAREA> tag, since the browser unescapes XML escape characters,
58 // like <. This can have unexpected behavior and lead to security issues
59 // such as scripting attacks.
64 constructor: function(params
/*===== , srcNodeRef =====*/){
67 // params: Object|null
68 // Initial settings for any of the widget attributes, except readonly attributes.
69 // srcNodeRef: DOMNode
70 // The widget replaces the specified DOMNode.
72 // contentPreFilters: Function(String)[]
73 // Pre content filter function register array.
74 // these filters will be executed before the actual
75 // editing area gets the html content.
76 this.contentPreFilters
= [];
78 // contentPostFilters: Function(String)[]
79 // post content filter function register array.
80 // These will be used on the resulting html
81 // from contentDomPostFilters. The resulting
82 // content is the final html (returned by getValue()).
83 this.contentPostFilters
= [];
85 // contentDomPreFilters: Function(DomNode)[]
86 // Pre content dom filter function register array.
87 // These filters are applied after the result from
88 // contentPreFilters are set to the editing area.
89 this.contentDomPreFilters
= [];
91 // contentDomPostFilters: Function(DomNode)[]
92 // Post content dom filter function register array.
93 // These filters are executed on the editing area dom.
94 // The result from these will be passed to contentPostFilters.
95 this.contentDomPostFilters
= [];
97 // editingAreaStyleSheets: dojo._URL[]
98 // array to store all the stylesheets applied to the editing area
99 this.editingAreaStyleSheets
= [];
101 // Make a copy of this.events before we start writing into it, otherwise we
102 // will modify the prototype which leads to bad things on pages w/multiple editors
103 this.events
= [].concat(this.events
);
105 this._keyHandlers
= {};
107 if(params
&& lang
.isString(params
.value
)){
108 this.value
= params
.value
;
111 this.onLoadDeferred
= new Deferred();
114 baseClass
: "dijitEditor",
116 // inheritWidth: Boolean
117 // whether to inherit the parent's width or simply use 100%
120 // focusOnLoad: [deprecated] Boolean
121 // Focus into this widget when the page is loaded
125 // Specifies the name of a (hidden) `<textarea>` node on the page that's used to save
126 // the editor content on page leave. Used to restore editor contents after navigating
127 // to a new page and then hitting the back button.
130 // styleSheets: [const] String
131 // semicolon (";") separated list of css files for the editing area
135 // Set height to fix the editor at a specific height, with scrolling.
136 // By default, this is 300px. If you want to have the editor always
137 // resizes to accommodate the content, use AlwaysShowToolbar plugin
138 // and set height="". If this editor is used within a layout widget,
139 // set height="100%".
143 // The minimum height that the editor should have.
146 // isClosed: [private] Boolean
149 // isLoaded: [private] Boolean
152 // _SEPARATOR: [private] String
153 // Used to concat contents from multiple editors into a single string,
154 // so they can be saved into a single `<textarea>` node. See "name" attribute.
155 _SEPARATOR
: "@@**%%__RICHTEXTBOUNDRY__%%**@@",
157 // _NAME_CONTENT_SEP: [private] String
158 // USed to separate name from content. Just a colon isn't safe.
159 _NAME_CONTENT_SEP
: "@@**%%:%%**@@",
161 // onLoadDeferred: [readonly] dojo.Deferred
162 // Deferred which is fired when the editor finishes loading.
163 // Call myEditor.onLoadDeferred.then(callback) it to be informed
164 // when the rich-text area initialization is finalized.
165 onLoadDeferred
: null,
167 // isTabIndent: Boolean
168 // Make tab key and shift-tab indent and outdent rather than navigating.
169 // Caution: sing this makes web pages inaccessible to users unable to use a mouse.
172 // disableSpellCheck: [const] Boolean
173 // When true, disables the browser's native spell checking, if supported.
174 // Works only in Firefox.
175 disableSpellCheck
: false,
177 postCreate: function(){
178 if("textarea" === this.domNode
.tagName
.toLowerCase()){
179 console
.warn("RichText should not be used with the TEXTAREA tag. See dijit._editor.RichText docs.");
182 // Push in the builtin filters now, making them the first executed, but not over-riding anything
183 // users passed in. See: #6062
184 this.contentPreFilters
= [lang
.hitch(this, "_preFixUrlAttributes")].concat(this.contentPreFilters
);
186 this.contentPreFilters
= [this._normalizeFontStyle
].concat(this.contentPreFilters
);
187 this.contentPostFilters
= [this._removeMozBogus
].concat(this.contentPostFilters
);
190 // Try to clean up WebKit bogus artifacts. The inserted classes
191 // made by WebKit sometimes messes things up.
192 this.contentPreFilters
= [this._removeWebkitBogus
].concat(this.contentPreFilters
);
193 this.contentPostFilters
= [this._removeWebkitBogus
].concat(this.contentPostFilters
);
196 // IE generates <strong> and <em> but we want to normalize to <b> and <i>
197 this.contentPostFilters
= [this._normalizeFontStyle
].concat(this.contentPostFilters
);
198 this.contentDomPostFilters
= [lang
.hitch(this, this._stripBreakerNodes
)].concat(this.contentDomPostFilters
);
200 this.inherited(arguments
);
202 topic
.publish(dijit
._scopeName
+ "._editor.RichText::init", this);
204 this.setupDefaultShortcuts();
207 setupDefaultShortcuts: function(){
209 // Add some default key handlers
211 // Overwrite this to setup your own handlers. The default
212 // implementation does not use Editor commands, but directly
213 // executes the builtin commands within the underlying browser
217 var exec
= lang
.hitch(this, function(cmd
, arg
){
219 return !this.execCommand(cmd
,arg
);
223 var ctrlKeyHandlers
= {
226 u
: exec("underline"),
227 a
: exec("selectall"),
228 s: function(){ this.save(true); },
229 m: function(){ this.isTabIndent
= !this.isTabIndent
; },
231 "1": exec("formatblock", "h1"),
232 "2": exec("formatblock", "h2"),
233 "3": exec("formatblock", "h3"),
234 "4": exec("formatblock", "h4"),
236 "\\": exec("insertunorderedlist")
240 ctrlKeyHandlers
.Z
= exec("redo"); //FIXME: undo?
244 for(key
in ctrlKeyHandlers
){
245 this.addKeyHandler(key
, true, false, ctrlKeyHandlers
[key
]);
249 // events: [private] String[]
250 // events which should be connected to the underlying editing area
251 events
: ["onKeyPress", "onKeyDown", "onKeyUp"], // onClick handled specially
253 // captureEvents: [deprecated] String[]
254 // Events which should be connected to the underlying editing
255 // area, events in this array will be addListener with
257 // TODO: looking at the code I don't see any distinction between events and captureEvents,
258 // so get rid of this for 2.0 if not sooner
261 _editorCommandsLocalized
: false,
262 _localizeEditorCommands: function(){
264 // When IE is running in a non-English locale, the API actually changes,
265 // so that we have to say (for example) danraku instead of p (for paragraph).
269 if(RichText
._editorCommandsLocalized
){
270 // Use the already generate cache of mappings.
271 this._local2NativeFormatNames
= RichText
._local2NativeFormatNames
;
272 this._native2LocalFormatNames
= RichText
._native2LocalFormatNames
;
275 RichText
._editorCommandsLocalized
= true;
276 RichText
._local2NativeFormatNames
= {};
277 RichText
._native2LocalFormatNames
= {};
278 this._local2NativeFormatNames
= RichText
._local2NativeFormatNames
;
279 this._native2LocalFormatNames
= RichText
._native2LocalFormatNames
;
280 //in IE, names for blockformat is locale dependent, so we cache the values here
282 //put p after div, so if IE returns Normal, we show it as paragraph
283 //We can distinguish p and div if IE returns Normal, however, in order to detect that,
284 //we have to call this.document.selection.createRange().parentElement() or such, which
285 //could slow things down. Leave it as it is for now
286 var formats
= ['div', 'p', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'ul', 'address'];
287 var localhtml
= "", format
, i
=0;
288 while((format
=formats
[i
++])){
289 //append a <br> after each element to separate the elements more reliably
290 if(format
.charAt(1) !== 'l'){
291 localhtml
+= "<"+format
+"><span>content</span></"+format
+"><br/>";
293 localhtml
+= "<"+format
+"><li>content</li></"+format
+"><br/>";
296 // queryCommandValue returns empty if we hide editNode, so move it out of screen temporary
297 // Also, IE9 does weird stuff unless we do it inside the editor iframe.
298 var style
= { position
: "absolute", top
: "0px", zIndex
: 10, opacity
: 0.01 };
299 var div
= domConstruct
.create('div', {style
: style
, innerHTML
: localhtml
});
300 this.ownerDocumentBody
.appendChild(div
);
302 // IE9 has a timing issue with doing this right after setting
303 // the inner HTML, so put a delay in.
304 var inject
= lang
.hitch(this, function(){
305 var node
= div
.firstChild
;
308 this._sCall("selectElement", [node
.firstChild
]);
309 var nativename
= node
.tagName
.toLowerCase();
310 this._local2NativeFormatNames
[nativename
] = document
.queryCommandValue("formatblock");
311 this._native2LocalFormatNames
[this._local2NativeFormatNames
[nativename
]] = nativename
;
312 node
= node
.nextSibling
.nextSibling
;
313 //console.log("Mapped: ", nativename, " to: ", this._local2NativeFormatNames[nativename]);
314 }catch(e
){ /*Sqelch the occasional IE9 error */ }
316 domConstruct
.destroy(div
);
321 open: function(/*DomNode?*/ element
){
323 // Transforms the node referenced in this.domNode into a rich text editing
326 // Sets up the editing area asynchronously. This will result in
327 // the creation and replacement with an iframe.
331 if(!this.onLoadDeferred
|| this.onLoadDeferred
.fired
>= 0){
332 this.onLoadDeferred
= new Deferred();
335 if(!this.isClosed
){ this.close(); }
336 topic
.publish(dijit
._scopeName
+ "._editor.RichText::open", this);
338 if(arguments
.length
=== 1 && element
.nodeName
){ // else unchanged
339 this.domNode
= element
;
342 var dn
= this.domNode
;
344 // "html" will hold the innerHTML of the srcNodeRef and will be used to
345 // initialize the editor.
348 if(lang
.isString(this.value
)){
349 // Allow setting the editor content programmatically instead of
350 // relying on the initial content being contained within the target
355 }else if(dn
.nodeName
&& dn
.nodeName
.toLowerCase() == "textarea"){
356 // if we were created from a textarea, then we need to create a
357 // new editing harness node.
358 var ta
= (this.textarea
= dn
);
361 dn
= this.domNode
= this.ownerDocument
.createElement("div");
362 dn
.setAttribute('widgetId', this.id
);
363 ta
.removeAttribute('widgetId');
364 dn
.cssText
= ta
.cssText
;
365 dn
.className
+= " " + ta
.className
;
366 domConstruct
.place(dn
, ta
, "before");
367 var tmpFunc
= lang
.hitch(this, function(){
368 //some browsers refuse to submit display=none textarea, so
369 //move the textarea off screen instead
372 position
: "absolute",
376 if(has("ie")){ //nasty IE bug: abnormal formatting if overflow is not hidden
378 this.__overflow
= s
.overflow
;
379 s
.overflow
= "hidden";
383 this.defer(tmpFunc
, 10);
389 var resetValue
= ta
.value
;
390 this.reset = function(){
391 var current
= this.getValue();
392 if(current
!== resetValue
){
393 this.replaceValue(resetValue
);
396 on(ta
.form
, "submit", lang
.hitch(this, function(){
397 // Copy value to the <textarea> so it gets submitted along with form.
398 // FIXME: should we be calling close() here instead?
399 domAttr
.set(ta
, 'disabled', this.disabled
); // don't submit the value if disabled
400 ta
.value
= this.getValue();
404 html
= htmlapi
.getChildrenHtml(dn
);
410 // If we're a list item we have to put in a blank line to force the
411 // bullet to nicely align at the top of text
412 if(dn
.nodeName
&& dn
.nodeName
=== "LI"){
413 dn
.innerHTML
= " <br>";
416 // Construct the editor div structure.
417 this.header
= dn
.ownerDocument
.createElement("div");
418 dn
.appendChild(this.header
);
419 this.editingArea
= dn
.ownerDocument
.createElement("div");
420 dn
.appendChild(this.editingArea
);
421 this.footer
= dn
.ownerDocument
.createElement("div");
422 dn
.appendChild(this.footer
);
425 this.name
= this.id
+ "_AUTOGEN";
428 // User has pressed back/forward button so we lost the text in the editor, but it's saved
429 // in a hidden <textarea> (which contains the data for all the editors on this page),
430 // so get editor value from there
431 if(this.name
!== "" && (!config
["useXDomain"] || config
["allowXdRichTextSave"])){
432 var saveTextarea
= dom
.byId(dijit
._scopeName
+ "._editor.RichText.value");
433 if(saveTextarea
&& saveTextarea
.value
!== ""){
434 var datas
= saveTextarea
.value
.split(this._SEPARATOR
), i
=0, dat
;
435 while((dat
=datas
[i
++])){
436 var data
= dat
.split(this._NAME_CONTENT_SEP
);
437 if(data
[0] === this.name
){
439 datas
= datas
.splice(i
, 1);
440 saveTextarea
.value
= datas
.join(this._SEPARATOR
);
446 if(!RichText
._globalSaveHandler
){
447 RichText
._globalSaveHandler
= {};
448 unload
.addOnUnload(function(){
450 for(id
in RichText
._globalSaveHandler
){
451 var f
= RichText
._globalSaveHandler
[id
];
452 if(lang
.isFunction(f
)){
458 RichText
._globalSaveHandler
[this.id
] = lang
.hitch(this, "_saveContent");
461 this.isClosed
= false;
463 var ifr
= (this.editorObject
= this.iframe
= this.ownerDocument
.createElement('iframe'));
464 ifr
.id
= this.id
+"_iframe";
465 ifr
.style
.border
= "none";
466 ifr
.style
.width
= "100%";
467 if(this._layoutMode
){
468 // iframe should be 100% height, thus getting it's height from surrounding
469 // <div> (which has the correct height set by Editor)
470 ifr
.style
.height
= "100%";
474 ifr
.style
.height
= this.height
;
477 ifr
.style
.minHeight
= this.minHeight
;
480 ifr
.style
.height
= this.height
? this.height
: this.minHeight
;
484 ifr
._loadFunc
= lang
.hitch( this, function(w
){
486 this.document
= this.window
.document
;
489 this._localizeEditorCommands();
492 // Do final setup and set initial contents of editor
496 // Set the iframe's initial (blank) content.
497 var src
= this._getIframeDocTxt(),
498 s
= "javascript: '" + src
.replace(/\\/g, "\\\\").replace(/'/g, "\\'") + "'";
499 ifr.setAttribute('src
', s);
500 this.editingArea.appendChild(ifr);
502 if(has("safari") <= 4){
503 src = ifr.getAttribute("src");
504 if(!src || src.indexOf("javascript") === -1){
505 // Safari 4 and earlier sometimes act oddly
506 // So we have to set it again.
507 this.defer(function(){ ifr.setAttribute('src
', s); });
511 // TODO: this is a guess at the default line-height, kinda works
512 if(dn.nodeName === "LI"){
513 dn.lastChild.style.marginTop = "-1.2em";
516 domClass.add(this.domNode, this.baseClass);
519 //static cache variables shared among all instance of this class
520 _local2NativeFormatNames: {},
521 _native2LocalFormatNames: {},
523 _getIframeDocTxt: function(){
525 // Generates the boilerplate text of the document inside the iframe (ie, `<html><head>...</head><body/></html>`).
526 // Editor content (if not blank) should be added afterwards.
529 var _cs = domStyle.getComputedStyle(this.domNode);
531 // The contents inside of <body>. The real contents are set later via a call to setValue().
533 var setBodyId = true;
534 if(has("ie") || has("webkit") || (!this.height && !has("mozilla"))){
535 // In auto-expand mode, need a wrapper div for AlwaysShowToolbar plugin to correctly
536 // expand/contract the editor as the content changes.
537 html = "<div id='dijitEditorBody
'></div>";
539 }else if(has("mozilla")){
540 // workaround bug where can't select then
delete text (until user types something
541 // into the editor)... and/or issue where typing doesn't erase selected text
542 this._cursorToStart
= true;
543 html
= " "; //
546 var font
= [ _cs
.fontWeight
, _cs
.fontSize
, _cs
.fontFamily
].join(" ");
548 // line height is tricky - applying a units value will mess things up.
549 // if we can't get a non-units value, bail out.
550 var lineHeight
= _cs
.lineHeight
;
551 if(lineHeight
.indexOf("px") >= 0){
552 lineHeight
= parseFloat(lineHeight
)/parseFloat(_cs
.fontSize
);
553 // console.debug(lineHeight);
554 }else if(lineHeight
.indexOf("em")>=0){
555 lineHeight
= parseFloat(lineHeight
);
557 // If we can't get a non-units value, just default
558 // it to the CSS spec default of 'normal'. Seems to
559 // work better, esp on IE, than '1.0'
560 lineHeight
= "normal";
564 this.style
.replace(/(^|;)\s*(line-|font-?)[^;]+/ig, function(match
){
565 match
= match
.replace(/^;/ig,"") + ';';
566 var s
= match
.split(":")[0];
572 for(i
= 0; i
< s
.length
; i
++){
577 c
= s
.charAt(i
).toUpperCase();
582 domStyle
.set(self
.domNode
, sC
, "");
584 userStyle
+= match
+ ';';
588 // need to find any associated label element and update iframe document title
589 var label
=query('label[for="'+this.id
+'"]');
592 this.isLeftToRight() ? "<html>\n<head>\n" : "<html dir='rtl'>\n<head>\n",
593 (has("mozilla") && label
.length
? "<title>" + label
[0].innerHTML
+ "</title>\n" : ""),
594 "<meta http-equiv='Content-Type' content='text/html'>\n",
597 "\t\tbackground:transparent;\n",
598 "\t\tpadding: 1px 0 0 0;\n",
599 "\t\tmargin: -1px 0 0 0;\n", // remove extraneous vertical scrollbar on safari and firefox
601 // Set the html/body sizing. Webkit always needs this, other browsers
602 // only set it when height is defined (not auto-expanding), otherwise
603 // scrollers do not appear.
604 ((has("webkit"))?"\t\twidth: 100%;\n":""),
605 ((has("webkit"))?"\t\theight: 100%;\n":""),
608 // TODO: left positioning will cause contents to disappear out of view
609 // if it gets too wide for the visible area
614 "\t\tfont:", font
, ";\n",
615 ((this.height
||has("opera")) ? "" : "\t\tposition: fixed;\n"),
616 // FIXME: IE 6 won't understand min-height?
617 "\t\tmin-height:", this.minHeight
, ";\n",
618 "\t\tline-height:", lineHeight
,";\n",
620 "\tp{ margin: 1em 0; }\n",
622 // Determine how scrollers should be applied. In autoexpand mode (height = "") no scrollers on y at all.
623 // But in fixed height mode we want both x/y scrollers. Also, if it's using wrapping div and in auto-expand
624 // (Mainly IE) we need to kill the y scroller on body and html.
625 (!setBodyId
&& !this.height
? "\tbody,html {overflow-y: hidden;}\n" : ""),
626 "\t#dijitEditorBody{overflow-x: auto; overflow-y:" + (this.height
? "auto;" : "hidden;") + " outline: 0px;}\n",
627 "\tli > ul:-moz-first-node, li > ol:-moz-first-node{ padding-top: 1.2em; }\n",
628 // Can't set min-height in IE9, it puts layout on li, which puts move/resize handles.
629 (!has("ie") ? "\tli{ min-height:1.2em; }\n" : ""),
631 this._applyEditingAreaStyleSheets(),"\n",
633 (setBodyId
?"id='dijitEditorBody' ":""),
635 // Onload handler fills in real editor content.
636 // On IE9, sometimes onload is called twice, and the first time frameElement is null (test_FullScreen.html)
637 "onload='frameElement && frameElement._loadFunc(window,document)' ",
638 "style='"+userStyle
+"'>", html
, "</body>\n</html>"
639 ].join(""); // String
642 _applyEditingAreaStyleSheets: function(){
644 // apply the specified css files in styleSheets
648 if(this.styleSheets
){
649 files
= this.styleSheets
.split(';');
650 this.styleSheets
= '';
653 //empty this.editingAreaStyleSheets here, as it will be filled in addStyleSheet
654 files
= files
.concat(this.editingAreaStyleSheets
);
655 this.editingAreaStyleSheets
= [];
657 var text
='', i
=0, url
;
658 while((url
=files
[i
++])){
659 var abstring
= (new _Url(win
.global
.location
, url
)).toString();
660 this.editingAreaStyleSheets
.push(abstring
);
661 text
+= '<link rel="stylesheet" type="text/css" href="'+abstring
+'"/>';
666 addStyleSheet: function(/*dojo/_base/url*/ uri
){
668 // add an external stylesheet for the editing area
670 // Url of the external css file
671 var url
=uri
.toString();
673 //if uri is relative, then convert it to absolute so that it can be resolved correctly in iframe
674 if(url
.charAt(0) === '.' || (url
.charAt(0) !== '/' && !uri
.host
)){
675 url
= (new _Url(win
.global
.location
, url
)).toString();
678 if(array
.indexOf(this.editingAreaStyleSheets
, url
) > -1){
679 // console.debug("dijit/_editor/RichText.addStyleSheet(): Style sheet "+url+" is already applied");
683 this.editingAreaStyleSheets
.push(url
);
684 this.onLoadDeferred
.then(lang
.hitch(this, function(){
685 if(this.document
.createStyleSheet
){ //IE
686 this.document
.createStyleSheet(url
);
687 }else{ //other browser
688 var head
= this.document
.getElementsByTagName("head")[0];
689 var stylesheet
= this.document
.createElement("link");
690 stylesheet
.rel
="stylesheet";
691 stylesheet
.type
="text/css";
693 head
.appendChild(stylesheet
);
698 removeStyleSheet: function(/*dojo/_base/url*/ uri
){
700 // remove an external stylesheet for the editing area
701 var url
=uri
.toString();
702 //if uri is relative, then convert it to absolute so that it can be resolved correctly in iframe
703 if(url
.charAt(0) === '.' || (url
.charAt(0) !== '/' && !uri
.host
)){
704 url
= (new _Url(win
.global
.location
, url
)).toString();
706 var index
= array
.indexOf(this.editingAreaStyleSheets
, url
);
708 // console.debug("dijit/_editor/RichText.removeStyleSheet(): Style sheet "+url+" has not been applied");
711 delete this.editingAreaStyleSheets
[index
];
712 query('link:[href="'+url
+'"]', this.window
.document
).orphan();
716 // The editor is disabled; the text cannot be changed.
719 _mozSettingProps
: {'styleWithCSS':false},
720 _setDisabledAttr: function(/*Boolean*/ value
){
722 this._set("disabled", value
);
723 if(!this.isLoaded
){ return; } // this method requires init to be complete
724 if(has("ie") || has("webkit") || has("opera")){
725 var preventIEfocus
= has("ie") && (this.isLoaded
|| !this.focusOnLoad
);
726 if(preventIEfocus
){ this.editNode
.unselectable
= "on"; }
727 this.editNode
.contentEditable
= !value
;
729 this.defer(function(){
730 if(this.editNode
){ // guard in case widget destroyed before timeout
731 this.editNode
.unselectable
= "off";
737 this.document
.designMode
=(value
?'off':'on');
738 }catch(e
){ return; } // ! _disabledOK
739 if(!value
&& this._mozSettingProps
){
740 var ps
= this._mozSettingProps
;
743 if(ps
.hasOwnProperty(n
)){
745 this.document
.execCommand(n
,false,ps
[n
]);
750 // this.document.execCommand('contentReadOnly', false, value);
752 // this.blur(); //to remove the blinking caret
755 this._disabledOK
= true;
761 onLoad: function(/*String*/ html
){
763 // Handler after the iframe finishes loading.
765 // Editor contents should be set to this value
769 // TODO: rename this to _onLoad, make empty public onLoad() method, deprecate/make protected onLoadDeferred handler?
771 if(!this.window
.__registeredWindow
){
772 this.window
.__registeredWindow
= true;
773 this._iframeRegHandle
= focus
.registerIframe(this.iframe
);
775 if(!has("ie") && !has("webkit") && (this.height
|| has("mozilla"))){
776 this.editNode
=this.document
.body
;
778 // there's a wrapper div around the content, see _getIframeDocTxt().
779 this.editNode
=this.document
.body
.firstChild
;
781 if(has("ie")){ // #4996 IE wants to focus the BODY tag
782 this.tabStop
= domConstruct
.create('div', { tabIndex
: -1 }, this.editingArea
);
783 this.iframe
.onfocus = function(){ _this
.editNode
.setActive(); };
786 this.focusNode
= this.editNode
; // for InlineEditBox
789 var events
= this.events
.concat(this.captureEvents
);
790 var ap
= this.iframe
? this.document
: this.editNode
;
791 array
.forEach(events
, function(item
){
792 this.connect(ap
, item
.toLowerCase(), item
);
795 this.connect(ap
, "onmouseup", "onClick"); // mouseup in the margin does not generate an onclick event
797 if(has("ie")){ // IE contentEditable
798 this.connect(this.document
, "onmousedown", "_onIEMouseDown"); // #4996 fix focus
800 // give the node Layout on IE
801 // TODO: this may no longer be needed, since we've reverted IE to using an iframe,
802 // not contentEditable. Removing it would also probably remove the need for creating
803 // the extra <div> in _getIframeDocTxt()
804 this.editNode
.style
.zoom
= 1.0;
806 this.connect(this.document
, "onmousedown", function(){
807 // Clear the moveToStart focus, as mouse
808 // down will set cursor point. Required to properly
809 // work with selection/position driven plugins and clicks in
810 // the window. refs: #10678
811 delete this._cursorToStart
;
816 //WebKit sometimes doesn't fire right on selections, so the toolbar
817 //doesn't update right. Therefore, help it out a bit with an additional
818 //listener. A mouse up will typically indicate a display change, so fire this
819 //and get the toolbar to adapt. Reference: #9532
820 this._webkitListener
= this.connect(this.document
, "onmouseup", "onDisplayChanged");
821 this.connect(this.document
, "onmousedown", function(e
){
823 if(t
&& (t
=== this.document
.body
|| t
=== this.document
)){
824 // Since WebKit uses the inner DIV, we need to check and set position.
825 // See: #12024 as to why the change was made.
826 this.defer("placeCursorAtEnd");
832 // Try to make sure 'hidden' elements aren't visible in edit mode (like browsers other than IE
835 this.document
.execCommand('RespectVisibilityInDesign', true, null);
836 }catch(e
){/* squelch */}
839 this.isLoaded
= true;
841 this.set('disabled', this.disabled
); // initialize content to editable (or not)
843 // Note that setValue() call will only work after isLoaded is set to true (above)
845 // Set up a function to allow delaying the setValue until a callback is fired
846 // This ensures extensions like dijit.Editor have a way to hold the value set
847 // until plugins load (and do things like register filters).
848 var setContent
= lang
.hitch(this, function(){
850 if(this.onLoadDeferred
){
851 this.onLoadDeferred
.resolve(true);
853 this.onDisplayChanged();
854 if(this.focusOnLoad
){
855 // after the document loads, then set focus after updateInterval expires so that
856 // onNormalizedDisplayChanged has run to avoid input caret issues
857 ready(lang
.hitch(this, "defer", "focus", this.updateInterval
));
859 // Save off the initial content now
860 this.value
= this.getValue(true);
862 if(this.setValueDeferred
){
863 this.setValueDeferred
.then(setContent
);
869 onKeyDown: function(/* Event */ e
){
871 // Handler for onkeydown event
875 // we need this event at the moment to get the events from control keys
876 // such as the backspace. It might be possible to add this to Dojo, so that
877 // keyPress events can be emulated by the keyDown and keyUp detection.
879 if(e
.keyCode
=== keys
.TAB
&& this.isTabIndent
){
880 event
.stop(e
); //prevent tab from moving focus out of editor
882 // FIXME: this is a poor-man's indent/outdent. It would be
883 // better if it added 4 " " chars in an undoable way.
884 // Unfortunately pasteHTML does not prove to be undoable
885 if(this.queryCommandEnabled((e
.shiftKey
? "outdent" : "indent"))){
886 this.execCommand((e
.shiftKey
? "outdent" : "indent"));
890 if(e
.keyCode
== keys
.TAB
&& !this.isTabIndent
){
891 if(e
.shiftKey
&& !e
.ctrlKey
&& !e
.altKey
){
892 // focus the BODY so the browser will tab away from it instead
894 }else if(!e
.shiftKey
&& !e
.ctrlKey
&& !e
.altKey
){
895 // focus the BODY so the browser will tab away from it instead
896 this.tabStop
.focus();
898 }else if(e
.keyCode
=== keys
.BACKSPACE
&& this.document
.selection
.type
=== "Control"){
899 // IE has a bug where if a non-text object is selected in the editor,
900 // hitting backspace would act as if the browser's back button was
901 // clicked instead of deleting the object. see #1069
903 this.execCommand("delete");
904 }else if((65 <= e
.keyCode
&& e
.keyCode
<= 90) ||
905 (e
.keyCode
>=37 && e
.keyCode
<=40) // FIXME: get this from connect() instead!
907 e
.charCode
= e
.keyCode
;
912 if(e
.keyCode
=== keys
.PAGE_UP
|| e
.keyCode
=== keys
.PAGE_DOWN
){
913 if(this.editNode
.clientHeight
>= this.editNode
.scrollHeight
){
914 // Stop the event to prevent firefox from trapping the cursor when there is no scroll bar.
922 onKeyUp: function(/*===== e =====*/){
924 // Handler for onkeyup event
929 setDisabled: function(/*Boolean*/ disabled
){
931 // Deprecated, use set('disabled', ...) instead.
934 kernel
.deprecated('dijit.Editor::setDisabled is deprecated','use dijit.Editor::attr("disabled",boolean) instead', 2.0);
935 this.set('disabled',disabled
);
937 _setValueAttr: function(/*String*/ value
){
939 // Registers that attr("value", foo) should call setValue(foo)
940 this.setValue(value
);
942 _setDisableSpellCheckAttr: function(/*Boolean*/ disabled
){
944 domAttr
.set(this.document
.body
, "spellcheck", !disabled
);
946 // try again after the editor is finished loading
947 this.onLoadDeferred
.then(lang
.hitch(this, function(){
948 domAttr
.set(this.document
.body
, "spellcheck", !disabled
);
951 this._set("disableSpellCheck", disabled
);
954 onKeyPress: function(e
){
956 // Handle the various key events
960 var c
= (e
.keyChar
&& e
.keyChar
.toLowerCase()) || e
.keyCode
,
961 handlers
= this._keyHandlers
[c
],
964 if(handlers
&& !e
.altKey
){
965 array
.some(handlers
, function(h
){
966 // treat meta- same as ctrl-, for benefit of mac users
967 if(!(h
.shift
^ e
.shiftKey
) && !(h
.ctrl
^ (e
.ctrlKey
||e
.metaKey
))){
968 if(!h
.handler
.apply(this, args
)){
976 // function call after the character has been inserted
977 if(!this._onKeyHitch
){
978 this._onKeyHitch
= lang
.hitch(this, "onKeyPressed");
980 this.defer("_onKeyHitch", 1);
984 addKeyHandler: function(/*String*/ key
, /*Boolean*/ ctrl
, /*Boolean*/ shift
, /*Function*/ handler
){
986 // Add a handler for a keyboard shortcut
988 // The key argument should be in lowercase if it is a letter character
991 if(!lang
.isArray(this._keyHandlers
[key
])){
992 this._keyHandlers
[key
] = [];
994 //TODO: would be nice to make this a hash instead of an array for quick lookups
995 this._keyHandlers
[key
].push({
996 shift
: shift
|| false,
1002 onKeyPressed: function(){
1004 // Handler for after the user has pressed a key, and the display has been updated.
1005 // (Runs on a timer so that it runs after the display is updated)
1008 this.onDisplayChanged(/*e*/); // can't pass in e
1011 onClick: function(/*Event*/ e
){
1013 // Handler for when the user clicks.
1017 // console.info('onClick',this._tryDesignModeOn);
1018 this.onDisplayChanged(e
);
1021 _onIEMouseDown: function(){
1023 // IE only to prevent 2 clicks to focus
1027 if(!this.focused
&& !this.disabled
){
1032 _onBlur: function(e
){
1034 // Called from focus manager when focus has moved away from this editor
1038 // console.info('_onBlur')
1040 this.inherited(arguments
);
1042 var newValue
= this.getValue(true);
1043 if(newValue
!== this.value
){
1044 this.onChange(newValue
);
1046 this._set("value", newValue
);
1049 _onFocus: function(/*Event*/ e
){
1051 // Called from focus manager when focus has moved into this editor
1055 // console.info('_onFocus')
1057 if(!this._disabledOK
){
1058 this.set('disabled', false);
1060 this.inherited(arguments
);
1064 // TODO: remove in 2.0
1067 // Remove focus from this instance.
1070 if(!has("ie") && this.window
.document
.documentElement
&& this.window
.document
.documentElement
.focus
){
1071 this.window
.document
.documentElement
.focus();
1072 }else if(this.ownerDocumentBody
.focus
){
1073 this.ownerDocumentBody
.focus();
1079 // Move focus to this editor
1081 this.focusOnLoad
= true;
1084 if(this._cursorToStart
){
1085 delete this._cursorToStart
;
1086 if(this.editNode
.childNodes
){
1087 this.placeCursorAtStart(); // this calls focus() so return
1092 focus
.focus(this.iframe
);
1093 }else if(this.editNode
&& this.editNode
.focus
){
1094 // editNode may be hidden in display:none div, lets just punt in this case
1095 //this.editNode.focus(); -> causes IE to scroll always (strict and quirks mode) to the top the Iframe
1096 // if we fire the event manually and let the browser handle the focusing, the latest
1097 // cursor position is focused like in FF
1098 this.iframe
.fireEvent('onfocus', document
.createEventObject()); // createEventObject only in IE
1100 // TODO: should we throw here?
1101 // console.debug("Have no idea how to focus into the editor!");
1106 updateInterval
: 200,
1108 onDisplayChanged: function(/*Event*/ /*===== e =====*/){
1110 // This event will be fired every time the display context
1111 // changes and the result needs to be reflected in the UI.
1113 // If you don't want to have update too often,
1114 // onNormalizedDisplayChanged should be used instead
1118 // var _t=new Date();
1119 if(this._updateTimer
){
1120 this._updateTimer
.remove();
1122 this._updateTimer
= this.defer("onNormalizedDisplayChanged", this.updateInterval
);
1124 // Technically this should trigger a call to watch("value", ...) registered handlers,
1125 // but getValue() is too slow to call on every keystroke so we don't.
1127 onNormalizedDisplayChanged: function(){
1129 // This event is fired every updateInterval ms or more
1131 // If something needs to happen immediately after a
1132 // user change, please use onDisplayChanged instead.
1135 delete this._updateTimer
;
1137 onChange: function(/*===== newContent =====*/){
1139 // This is fired if and only if the editor loses focus and
1140 // the content is changed.
1142 _normalizeCommand: function(/*String*/ cmd
, /*Anything?*/argument
){
1144 // Used as the advice function to map our
1145 // normalized set of commands to those supported by the target
1150 var command
= cmd
.toLowerCase();
1151 if(command
=== "formatblock"){
1152 if(has("safari") && argument
=== undefined){ command
= "heading"; }
1153 }else if(command
=== "hilitecolor" && !has("mozilla")){
1154 command
= "backcolor";
1161 queryCommandAvailable: function(/*String*/ command
){
1163 // Tests whether a command is supported by the host. Clients
1164 // SHOULD check whether a command is supported before attempting
1165 // to use it, behaviour for unsupported commands is undefined.
1167 // The command to test for
1171 // memoizing version. See _queryCommandAvailable for computing version
1172 var ca
= this._qcaCache
[command
];
1173 if(ca
!== undefined){ return ca
; }
1174 return (this._qcaCache
[command
] = this._queryCommandAvailable(command
));
1177 _queryCommandAvailable: function(/*String*/ command
){
1179 // See queryCommandAvailable().
1184 var mozilla
= 1 << 1;
1185 var webkit
= 1 << 2;
1188 function isSupportedBy(browsers
){
1190 ie
: Boolean(browsers
& ie
),
1191 mozilla
: Boolean(browsers
& mozilla
),
1192 webkit
: Boolean(browsers
& webkit
),
1193 opera
: Boolean(browsers
& opera
)
1197 var supportedBy
= null;
1199 switch(command
.toLowerCase()){
1200 case "bold": case "italic": case "underline":
1201 case "subscript": case "superscript":
1202 case "fontname": case "fontsize":
1203 case "forecolor": case "hilitecolor":
1204 case "justifycenter": case "justifyfull": case "justifyleft":
1205 case "justifyright": case "delete": case "selectall": case "toggledir":
1206 supportedBy
= isSupportedBy(mozilla
| ie
| webkit
| opera
);
1209 case "createlink": case "unlink": case "removeformat":
1210 case "inserthorizontalrule": case "insertimage":
1211 case "insertorderedlist": case "insertunorderedlist":
1212 case "indent": case "outdent": case "formatblock":
1213 case "inserthtml": case "undo": case "redo": case "strikethrough": case "tabindent":
1214 supportedBy
= isSupportedBy(mozilla
| ie
| opera
| webkit
);
1217 case "blockdirltr": case "blockdirrtl":
1218 case "dirltr": case "dirrtl":
1219 case "inlinedirltr": case "inlinedirrtl":
1220 supportedBy
= isSupportedBy(ie
);
1222 case "cut": case "copy": case "paste":
1223 supportedBy
= isSupportedBy( ie
| mozilla
| webkit
| opera
);
1227 supportedBy
= isSupportedBy(mozilla
| ie
);
1230 case "insertcell": case "insertcol": case "insertrow":
1231 case "deletecells": case "deletecols": case "deleterows":
1232 case "mergecells": case "splitcell":
1233 supportedBy
= isSupportedBy(ie
| mozilla
);
1236 default: return false;
1239 return (has("ie") && supportedBy
.ie
) ||
1240 (has("mozilla") && supportedBy
.mozilla
) ||
1241 (has("webkit") && supportedBy
.webkit
) ||
1242 (has("opera") && supportedBy
.opera
); // Boolean return true if the command is supported, false otherwise
1245 execCommand: function(/*String*/ command
, argument
){
1247 // Executes a command in the Rich Text area
1249 // The command to execute
1251 // An optional argument to the command
1256 //focus() is required for IE to work
1257 //In addition, focus() makes sure after the execution of
1258 //the command, the editor receives the focus as expected
1261 command
= this._normalizeCommand(command
, argument
);
1263 if(argument
!== undefined){
1264 if(command
=== "heading"){
1265 throw new Error("unimplemented");
1266 }else if((command
=== "formatblock") && has("ie")){
1267 argument
= '<'+argument
+'>';
1271 //Check to see if we have any over-rides for commands, they will be functions on this
1272 //widget of the form _commandImpl. If we don't, fall through to the basic native
1273 //exec command of the browser.
1274 var implFunc
= "_" + command
+ "Impl";
1276 returnValue
= this[implFunc
](argument
);
1278 argument
= arguments
.length
> 1 ? argument
: null;
1279 if(argument
|| command
!== "createlink"){
1280 returnValue
= this.document
.execCommand(command
, false, argument
);
1284 this.onDisplayChanged();
1288 queryCommandEnabled: function(/*String*/ command
){
1290 // Check whether a command is enabled or not.
1292 // The command to execute
1295 if(this.disabled
|| !this._disabledOK
){ return false; }
1297 command
= this._normalizeCommand(command
);
1299 //Check to see if we have any over-rides for commands, they will be functions on this
1300 //widget of the form _commandEnabledImpl. If we don't, fall through to the basic native
1301 //command of the browser.
1302 var implFunc
= "_" + command
+ "EnabledImpl";
1305 return this[implFunc
](command
);
1307 return this._browserQueryCommandEnabled(command
);
1311 queryCommandState: function(command
){
1313 // Check the state of a given command and returns true or false.
1317 if(this.disabled
|| !this._disabledOK
){ return false; }
1318 command
= this._normalizeCommand(command
);
1320 return this.document
.queryCommandState(command
);
1322 //Squelch, occurs if editor is hidden on FF 3 (and maybe others.)
1327 queryCommandValue: function(command
){
1329 // Check the value of a given command. This matters most for
1330 // custom selections and complex values like font value setting.
1334 if(this.disabled
|| !this._disabledOK
){ return false; }
1336 command
= this._normalizeCommand(command
);
1337 if(has("ie") && command
=== "formatblock"){
1338 r
= this._native2LocalFormatNames
[this.document
.queryCommandValue(command
)];
1339 }else if(has("mozilla") && command
=== "hilitecolor"){
1342 oldValue
= this.document
.queryCommandValue("styleWithCSS");
1346 this.document
.execCommand("styleWithCSS", false, true);
1347 r
= this.document
.queryCommandValue(command
);
1348 this.document
.execCommand("styleWithCSS", false, oldValue
);
1350 r
= this.document
.queryCommandValue(command
);
1357 _sCall: function(name
, args
){
1359 // Run the named method of dijit/_editor/selection over the
1360 // current editor instance's window, with the passed args.
1363 return win
.withGlobal(this.window
, name
, selectionapi
, args
);
1366 // FIXME: this is a TON of code duplication. Why?
1368 placeCursorAtStart: function(){
1370 // Place the cursor at the start of the editing area.
1376 //see comments in placeCursorAtEnd
1379 // TODO: Is this branch even necessary?
1380 var first
=this.editNode
.firstChild
;
1382 if(first
.nodeType
=== 3){
1383 if(first
.nodeValue
.replace(/^\s+|\s+$/g, "").length
>0){
1385 this._sCall("selectElement", [ first
]);
1388 }else if(first
.nodeType
=== 1){
1390 var tg
= first
.tagName
? first
.tagName
.toLowerCase() : "";
1391 // Collapse before childless tags.
1392 if(/br|input|img|base|meta|area|basefont|hr|link/.test(tg
)){
1393 this._sCall("selectElement", [ first
]);
1395 // Collapse inside tags with children.
1396 this._sCall("selectElementChildren", [ first
]);
1400 first
= first
.nextSibling
;
1404 this._sCall("selectElementChildren", [ this.editNode
]);
1407 this._sCall("collapse", [ true ]);
1411 placeCursorAtEnd: function(){
1413 // Place the cursor at the end of the editing area.
1419 //In mozilla, if last child is not a text node, we have to use
1420 // selectElementChildren on this.editNode.lastChild otherwise the
1421 // cursor would be placed at the end of the closing tag of
1422 //this.editNode.lastChild
1425 var last
=this.editNode
.lastChild
;
1427 if(last
.nodeType
=== 3){
1428 if(last
.nodeValue
.replace(/^\s+|\s+$/g, "").length
>0){
1430 this._sCall("selectElement", [ last
]);
1433 }else if(last
.nodeType
=== 1){
1435 this._sCall("selectElement", [ last
.lastChild
|| last
]);
1438 last
= last
.previousSibling
;
1442 this._sCall("selectElementChildren", [ this.editNode
]);
1445 this._sCall("collapse", [ false ]);
1449 getValue: function(/*Boolean?*/ nonDestructive
){
1451 // Return the current content of the editing area (post filters
1452 // are applied). Users should call get('value') instead.
1454 // defaults to false. Should the post-filtering be run over a copy
1455 // of the live DOM? Most users should pass "true" here unless they
1456 // *really* know that none of the installed filters are going to
1457 // mess up the editing session.
1461 if(this.isClosed
|| !this.isLoaded
){
1462 return this.textarea
.value
;
1466 return this._postFilterContent(null, nonDestructive
);
1468 _getValueAttr: function(){
1470 // Hook to make attr("value") work
1471 return this.getValue(true);
1474 setValue: function(/*String*/ html
){
1476 // This function sets the content. No undo history is preserved.
1477 // Users should use set('value', ...) instead.
1481 // TODO: remove this and getValue() for 2.0, and move code to _setValueAttr()
1484 // try again after the editor is finished loading
1485 this.onLoadDeferred
.then(lang
.hitch(this, function(){
1486 this.setValue(html
);
1490 this._cursorToStart
= true;
1491 if(this.textarea
&& (this.isClosed
|| !this.isLoaded
)){
1492 this.textarea
.value
=html
;
1494 html
= this._preFilterContent(html
);
1495 var node
= this.isClosed
? this.domNode
: this.editNode
;
1496 if(html
&& has("mozilla") && html
.toLowerCase() === "<p></p>"){
1497 html
= "<p> </p>"; //
1500 // Use to avoid webkit problems where editor is disabled until the user clicks it
1501 if(!html
&& has("webkit")){
1502 html
= " "; //
1504 node
.innerHTML
= html
;
1505 this._preDomFilterContent(node
);
1508 this.onDisplayChanged();
1509 this._set("value", this.getValue(true));
1512 replaceValue: function(/*String*/ html
){
1514 // This function set the content while trying to maintain the undo stack
1515 // (now only works fine with Moz, this is identical to setValue in all
1521 this.setValue(html
);
1522 }else if(this.window
&& this.window
.getSelection
&& !has("mozilla")){ // Safari
1523 // look ma! it's a totally f'd browser!
1524 this.setValue(html
);
1525 }else if(this.window
&& this.window
.getSelection
){ // Moz
1526 html
= this._preFilterContent(html
);
1527 this.execCommand("selectall");
1529 this._cursorToStart
= true;
1530 html
= " "; //
1532 this.execCommand("inserthtml", html
);
1533 this._preDomFilterContent(this.editNode
);
1534 }else if(this.document
&& this.document
.selection
){//IE
1535 //In IE, when the first element is not a text node, say
1536 //an <a> tag, when replacing the content of the editing
1537 //area, the <a> tag will be around all the content
1538 //so for now, use setValue for IE too
1539 this.setValue(html
);
1542 this._set("value", this.getValue(true));
1545 _preFilterContent: function(/*String*/ html
){
1547 // Filter the input before setting the content of the editing
1548 // area. DOM pre-filtering may happen after this
1549 // string-based filtering takes place but as of 1.2, this is not
1550 // guaranteed for operations such as the inserthtml command.
1555 array
.forEach(this.contentPreFilters
, function(ef
){ if(ef
){ ec
= ef(ec
); } });
1558 _preDomFilterContent: function(/*DomNode*/ dom
){
1560 // filter the input's live DOM. All filter operations should be
1561 // considered to be "live" and operating on the DOM that the user
1562 // will be interacting with in their editing session.
1565 dom
= dom
|| this.editNode
;
1566 array
.forEach(this.contentDomPreFilters
, function(ef
){
1567 if(ef
&& lang
.isFunction(ef
)){
1573 _postFilterContent: function(
1574 /*DomNode|DomNode[]|String?*/ dom
,
1575 /*Boolean?*/ nonDestructive
){
1577 // filter the output after getting the content of the editing area
1580 // post-filtering allows plug-ins and users to specify any number
1581 // of transforms over the editor's content, enabling many common
1582 // use-cases such as transforming absolute to relative URLs (and
1583 // vice-versa), ensuring conformance with a particular DTD, etc.
1584 // The filters are registered in the contentDomPostFilters and
1585 // contentPostFilters arrays. Each item in the
1586 // contentDomPostFilters array is a function which takes a DOM
1587 // Node or array of nodes as its only argument and returns the
1588 // same. It is then passed down the chain for further filtering.
1589 // The contentPostFilters array behaves the same way, except each
1590 // member operates on strings. Together, the DOM and string-based
1591 // filtering allow the full range of post-processing that should
1592 // be necessaray to enable even the most agressive of post-editing
1593 // conversions to take place.
1595 // If nonDestructive is set to "true", the nodes are cloned before
1596 // filtering proceeds to avoid potentially destructive transforms
1597 // to the content which may still needed to be edited further.
1598 // Once DOM filtering has taken place, the serialized version of
1599 // the DOM which is passed is run through each of the
1600 // contentPostFilters functions.
1603 // a node, set of nodes, which to filter using each of the current
1604 // members of the contentDomPostFilters and contentPostFilters arrays.
1607 // defaults to "false". If true, ensures that filtering happens on
1608 // a clone of the passed-in content and not the actual node
1615 if(!lang
.isString(dom
)){
1616 dom
= dom
|| this.editNode
;
1617 if(this.contentDomPostFilters
.length
){
1619 dom
= lang
.clone(dom
);
1621 array
.forEach(this.contentDomPostFilters
, function(ef
){
1625 ec
= htmlapi
.getChildrenHtml(dom
);
1630 if(!lang
.trim(ec
.replace(/^\xA0\xA0*/, '').replace(/\xA0\xA0*$/, '')).length
){
1635 // //removing appended <P> </P> for IE
1636 // ec = ec.replace(/(?:<p> </p>[\n\r]*)+$/i,"");
1638 array
.forEach(this.contentPostFilters
, function(ef
){
1645 _saveContent: function(){
1647 // Saves the content in an onunload event if the editor has not been closed
1651 var saveTextarea
= dom
.byId(dijit
._scopeName
+ "._editor.RichText.value");
1653 if(saveTextarea
.value
){
1654 saveTextarea
.value
+= this._SEPARATOR
;
1656 saveTextarea
.value
+= this.name
+ this._NAME_CONTENT_SEP
+ this.getValue(true);
1661 escapeXml: function(/*String*/ str
, /*Boolean*/ noSingleQuotes
){
1663 // Adds escape sequences for special characters in XML.
1664 // Optionally skips escapes for single quotes
1668 str
= str
.replace(/&/gm, "&").replace(/</gm, "<").replace(/>/gm, ">").replace(/"/gm, ""
;");
1669 if(!noSingleQuotes){
1670 str = str.replace(/'/gm, "'");
1672 return str; // string
1675 getNodeHtml: function(/* DomNode */ node){
1677 // Deprecated. Use dijit/_editor/html::_getNodeHtml() instead.
1680 kernel.deprecated('dijit.Editor::getNodeHtml is deprecated','use dijit/_editor/html::getNodeHtml instead', 2);
1681 return htmlapi.getNodeHtml(node); // String
1684 getNodeChildrenHtml: function(/* DomNode */ dom){
1686 // Deprecated. Use dijit/_editor/html::getChildrenHtml() instead.
1689 kernel.deprecated('dijit.Editor::getNodeChildrenHtml is deprecated','use dijit/_editor/html::getChildrenHtml instead', 2);
1690 return htmlapi.getChildrenHtml(dom);
1693 close: function(/*Boolean?*/ save){
1695 // Kills the editor and optionally writes back the modified contents to the
1696 // element from which it originated.
1698 // Whether or not to save the changes. If false, the changes are discarded.
1702 if(this.isClosed){ return; }
1704 if(!arguments.length){ save = true; }
1706 this._set("value
", this.getValue(true));
1709 // line height is squashed for iframes
1710 // FIXME: why was this here? if(this.iframe){ this.domNode.style.lineHeight = null; }
1712 if(this.interval){ clearInterval(this.interval); }
1714 if(this._webkitListener){
1715 //Cleaup of WebKit fix: #9532
1716 this.disconnect(this._webkitListener);
1717 delete this._webkitListener;
1720 // Guard against memory leaks on IE (see #9268)
1722 this.iframe.onfocus = null;
1724 this.iframe._loadFunc = null;
1726 if(this._iframeRegHandle){
1727 this._iframeRegHandle.remove();
1728 delete this._iframeRegHandle;
1732 var s = this.textarea.style;
1734 s.left = s.top = "";
1736 s.overflow = this.__overflow;
1737 this.__overflow = null;
1739 this.textarea.value = this.value;
1740 domConstruct.destroy(this.domNode);
1741 this.domNode = this.textarea;
1743 // Note that this destroys the iframe
1744 this.domNode.innerHTML = this.value;
1748 domClass.remove(this.domNode, this.baseClass);
1749 this.isClosed = true;
1750 this.isLoaded = false;
1752 delete this.editNode;
1753 delete this.focusNode;
1755 if(this.window && this.window._frameElement){
1756 this.window._frameElement = null;
1760 this.document = null;
1761 this.editingArea = null;
1762 this.editorObject = null;
1765 destroy: function(){
1766 if(!this.isClosed){ this.close(false); }
1767 if(this._updateTimer){
1768 this._updateTimer.remove();
1770 this.inherited(arguments);
1771 if(RichText._globalSaveHandler){
1772 delete RichText._globalSaveHandler[this.id];
1776 _removeMozBogus: function(/* String */ html){
1778 // Post filter to remove unwanted HTML attributes generated by mozilla
1781 return html.replace(/\stype="_moz
"/gi, '').replace(/\s_moz_dirty=""/gi, '').replace(/_moz_resizing="(true|false)"/gi,''); // String
1783 _removeWebkitBogus: function(/* String */ html){
1785 // Post filter to remove unwanted HTML attributes generated by webkit
1788 html = html.replace(/\sclass="webkit
-block
-placeholder
"/gi, '');
1789 html = html.replace(/\sclass="apple
-style
-span
"/gi, '');
1790 // For some reason copy/paste sometime adds extra meta tags for charset on
1791 // webkit (chrome) on mac.They need to be removed. See: #12007"
1792 html
= html
.replace(/<meta charset=\"utf-8\" \/>/gi, '');
1793 return html
; // String
1795 _normalizeFontStyle: function(/* String */ html
){
1797 // Convert 'strong' and 'em' to 'b' and 'i'.
1799 // Moz can not handle strong/em tags correctly, so to help
1800 // mozilla and also to normalize output, convert them to 'b' and 'i'.
1802 // Note the IE generates 'strong' and 'em' rather than 'b' and 'i'
1805 return html
.replace(/<(\/)?strong([ \>])/gi, '<$1b$2')
1806 .replace(/<(\/)?em([ \>])/gi, '<$1i$2' ); // String
1809 _preFixUrlAttributes: function(/* String */ html
){
1811 // Pre-filter to do fixing to href attributes on `<a>` and `<img>` tags
1814 return html
.replace(/(?:(<a(?=\s).*?\shref=)("|')(.*?)\2)|(?:(<a\s.*?href=)([^"'][^ >]+))/gi,
1815 '$1$4$2$3$5$2 _djrealurl=$2$3$5$2')
1816 .replace(/(?:(<img(?=\s).*?\ssrc=)("|')(.*?)\2)|(?:(<img\s.*?src=)([^"'][^ >]+))/gi,
1817 '$1$4$2$3$5$2 _djrealurl=$2$3$5$2'); // String
1820 /*****************************************************************************
1821 The following functions implement HTML manipulation commands for various
1822 browser/contentEditable implementations. The goal of them is to enforce
1823 standard behaviors of them.
1824 ******************************************************************************/
1826 /*** queryCommandEnabled implementations ***/
1828 _browserQueryCommandEnabled: function(command
){
1830 // Implementation to call to the native queryCommandEnabled of the browser.
1832 // The command to check.
1835 if(!command
) { return false; }
1836 var elem
= has("ie") ? this.document
.selection
.createRange() : this.document
;
1838 return elem
.queryCommandEnabled(command
);
1844 _createlinkEnabledImpl: function(/*===== argument =====*/){
1846 // This function implements the test for if the create link
1847 // command should be enabled or not.
1849 // arguments to the exec command, if any.
1854 var sel
= this.window
.getSelection();
1855 if(sel
.isCollapsed
){
1858 enabled
= this.document
.queryCommandEnabled("createlink");
1861 enabled
= this._browserQueryCommandEnabled("createlink");
1866 _unlinkEnabledImpl: function(/*===== argument =====*/){
1868 // This function implements the test for if the unlink
1869 // command should be enabled or not.
1871 // arguments to the exec command, if any.
1875 if(has("mozilla") || has("webkit")){
1876 enabled
= this._sCall("hasAncestorElement", ["a"]);
1878 enabled
= this._browserQueryCommandEnabled("unlink");
1883 _inserttableEnabledImpl: function(/*===== argument =====*/){
1885 // This function implements the test for if the inserttable
1886 // command should be enabled or not.
1888 // arguments to the exec command, if any.
1892 if(has("mozilla") || has("webkit")){
1895 enabled
= this._browserQueryCommandEnabled("inserttable");
1900 _cutEnabledImpl: function(/*===== argument =====*/){
1902 // This function implements the test for if the cut
1903 // command should be enabled or not.
1905 // arguments to the exec command, if any.
1910 // WebKit deems clipboard activity as a security threat and natively would return false
1911 var sel
= this.window
.getSelection();
1912 if(sel
){ sel
= sel
.toString(); }
1915 enabled
= this._browserQueryCommandEnabled("cut");
1920 _copyEnabledImpl: function(/*===== argument =====*/){
1922 // This function implements the test for if the copy
1923 // command should be enabled or not.
1925 // arguments to the exec command, if any.
1930 // WebKit deems clipboard activity as a security threat and natively would return false
1931 var sel
= this.window
.getSelection();
1932 if(sel
){ sel
= sel
.toString(); }
1935 enabled
= this._browserQueryCommandEnabled("copy");
1940 _pasteEnabledImpl: function(/*===== argument =====*/){
1942 // This function implements the test for if the paste
1943 // command should be enabled or not.
1945 // arguments to the exec command, if any.
1952 enabled
= this._browserQueryCommandEnabled("paste");
1957 /*** execCommand implementations ***/
1959 _inserthorizontalruleImpl: function(argument
){
1961 // This function implements the insertion of HTML 'HR' tags.
1962 // into a point on the page. IE doesn't to it right, so
1963 // we have to use an alternate form
1965 // arguments to the exec command, if any.
1969 return this._inserthtmlImpl("<hr>");
1971 return this.document
.execCommand("inserthorizontalrule", false, argument
);
1974 _unlinkImpl: function(argument
){
1976 // This function implements the unlink of an 'a' tag.
1978 // arguments to the exec command, if any.
1981 if((this.queryCommandEnabled("unlink")) && (has("mozilla") || has("webkit"))){
1982 var a
= this._sCall("getAncestorElement", [ "a" ]);
1983 this._sCall("selectElement", [ a
]);
1984 return this.document
.execCommand("unlink", false, null);
1986 return this.document
.execCommand("unlink", false, argument
);
1989 _hilitecolorImpl: function(argument
){
1991 // This function implements the hilitecolor command
1993 // arguments to the exec command, if any.
1997 var isApplied
= this._handleTextColorOrProperties("hilitecolor", argument
);
2000 // mozilla doesn't support hilitecolor properly when useCSS is
2001 // set to false (bugzilla #279330)
2002 this.document
.execCommand("styleWithCSS", false, true);
2003 console
.log("Executing color command.");
2004 returnValue
= this.document
.execCommand("hilitecolor", false, argument
);
2005 this.document
.execCommand("styleWithCSS", false, false);
2007 returnValue
= this.document
.execCommand("hilitecolor", false, argument
);
2013 _backcolorImpl: function(argument
){
2015 // This function implements the backcolor command
2017 // arguments to the exec command, if any.
2021 // Tested under IE 6 XP2, no problem here, comment out
2022 // IE weirdly collapses ranges when we exec these commands, so prevent it
2023 // var tr = this.document.selection.createRange();
2024 argument
= argument
? argument
: null;
2026 var isApplied
= this._handleTextColorOrProperties("backcolor", argument
);
2028 isApplied
= this.document
.execCommand("backcolor", false, argument
);
2033 _forecolorImpl: function(argument
){
2035 // This function implements the forecolor command
2037 // arguments to the exec command, if any.
2041 // Tested under IE 6 XP2, no problem here, comment out
2042 // IE weirdly collapses ranges when we exec these commands, so prevent it
2043 // var tr = this.document.selection.createRange();
2044 argument
= argument
? argument
: null;
2046 var isApplied
= false;
2047 isApplied
= this._handleTextColorOrProperties("forecolor", argument
);
2049 isApplied
= this.document
.execCommand("forecolor", false, argument
);
2054 _inserthtmlImpl: function(argument
){
2056 // This function implements the insertion of HTML content into
2057 // a point on the page.
2059 // The content to insert, if any.
2062 argument
= this._preFilterContent(argument
);
2065 var insertRange
= this.document
.selection
.createRange();
2066 if(this.document
.selection
.type
.toUpperCase() === 'CONTROL'){
2067 var n
=insertRange
.item(0);
2068 while(insertRange
.length
){
2069 insertRange
.remove(insertRange
.item(0));
2071 n
.outerHTML
=argument
;
2073 insertRange
.pasteHTML(argument
);
2075 insertRange
.select();
2076 //insertRange.collapse(true);
2077 }else if(has("mozilla") && !argument
.length
){
2078 //mozilla can not inserthtml an empty html to delete current selection
2079 //so we delete the selection instead in this case
2080 this._sCall("remove"); // FIXME
2082 rv
= this.document
.execCommand("inserthtml", false, argument
);
2087 _boldImpl: function(argument
){
2089 // This function implements an over-ride of the bold command.
2091 // Not used, operates by selection.
2094 var applied
= false;
2096 this._adaptIESelection();
2097 applied
= this._adaptIEFormatAreaAndExec("bold");
2100 applied
= this.document
.execCommand("bold", false, argument
);
2105 _italicImpl: function(argument
){
2107 // This function implements an over-ride of the italic command.
2109 // Not used, operates by selection.
2112 var applied
= false;
2114 this._adaptIESelection();
2115 applied
= this._adaptIEFormatAreaAndExec("italic");
2118 applied
= this.document
.execCommand("italic", false, argument
);
2123 _underlineImpl: function(argument
){
2125 // This function implements an over-ride of the underline command.
2127 // Not used, operates by selection.
2130 var applied
= false;
2132 this._adaptIESelection();
2133 applied
= this._adaptIEFormatAreaAndExec("underline");
2136 applied
= this.document
.execCommand("underline", false, argument
);
2141 _strikethroughImpl: function(argument
){
2143 // This function implements an over-ride of the strikethrough command.
2145 // Not used, operates by selection.
2148 var applied
= false;
2150 this._adaptIESelection();
2151 applied
= this._adaptIEFormatAreaAndExec("strikethrough");
2154 applied
= this.document
.execCommand("strikethrough", false, argument
);
2159 _superscriptImpl: function(argument
){
2161 // This function implements an over-ride of the superscript command.
2163 // Not used, operates by selection.
2166 var applied
= false;
2168 this._adaptIESelection();
2169 applied
= this._adaptIEFormatAreaAndExec("superscript");
2172 applied
= this.document
.execCommand("superscript", false, argument
);
2177 _subscriptImpl: function(argument
){
2179 // This function implements an over-ride of the superscript command.
2181 // Not used, operates by selection.
2184 var applied
= false;
2186 this._adaptIESelection();
2187 applied
= this._adaptIEFormatAreaAndExec("subscript");
2191 applied
= this.document
.execCommand("subscript", false, argument
);
2196 _fontnameImpl: function(argument
){
2198 // This function implements the fontname command
2200 // arguments to the exec command, if any.
2205 isApplied
= this._handleTextColorOrProperties("fontname", argument
);
2208 isApplied
= this.document
.execCommand("fontname", false, argument
);
2213 _fontsizeImpl: function(argument
){
2215 // This function implements the fontsize command
2217 // arguments to the exec command, if any.
2222 isApplied
= this._handleTextColorOrProperties("fontsize", argument
);
2225 isApplied
= this.document
.execCommand("fontsize", false, argument
);
2230 _insertorderedlistImpl: function(argument
){
2232 // This function implements the insertorderedlist command
2234 // arguments to the exec command, if any.
2237 var applied
= false;
2239 applied
= this._adaptIEList("insertorderedlist", argument
);
2242 applied
= this.document
.execCommand("insertorderedlist", false, argument
);
2247 _insertunorderedlistImpl: function(argument
){
2249 // This function implements the insertunorderedlist command
2251 // arguments to the exec command, if any.
2254 var applied
= false;
2256 applied
= this._adaptIEList("insertunorderedlist", argument
);
2259 applied
= this.document
.execCommand("insertunorderedlist", false, argument
);
2264 getHeaderHeight: function(){
2266 // A function for obtaining the height of the header node
2267 return this._getNodeChildrenHeight(this.header
); // Number
2270 getFooterHeight: function(){
2272 // A function for obtaining the height of the footer node
2273 return this._getNodeChildrenHeight(this.footer
); // Number
2276 _getNodeChildrenHeight: function(node
){
2278 // An internal function for computing the cumulative height of all child nodes of 'node'
2280 // The node to process the children of;
2282 if(node
&& node
.childNodes
){
2283 // IE didn't compute it right when position was obtained on the node directly is some cases,
2284 // so we have to walk over all the children manually.
2286 for(i
= 0; i
< node
.childNodes
.length
; i
++){
2287 var size
= domGeometry
.position(node
.childNodes
[i
]);
2294 _isNodeEmpty: function(node
, startOffset
){
2296 // Function to test if a node is devoid of real content.
2298 // The node to check.
2301 if(node
.nodeType
=== 1/*element*/){
2302 if(node
.childNodes
.length
> 0){
2303 return this._isNodeEmpty(node
.childNodes
[0], startOffset
);
2306 }else if(node
.nodeType
=== 3/*text*/){
2307 return (node
.nodeValue
.substring(startOffset
) === "");
2312 _removeStartingRangeFromRange: function(node
, range
){
2314 // Function to adjust selection range by removing the current
2317 // The node to remove from the starting range.
2319 // The range to adapt.
2322 if(node
.nextSibling
){
2323 range
.setStart(node
.nextSibling
,0);
2325 var parent
= node
.parentNode
;
2326 while(parent
&& parent
.nextSibling
== null){
2327 //move up the tree until we find a parent that has another node, that node will be the next node
2328 parent
= parent
.parentNode
;
2331 range
.setStart(parent
.nextSibling
,0);
2337 _adaptIESelection: function(){
2339 // Function to adapt the IE range by removing leading 'newlines'
2340 // Needed to fix issue with bold/italics/underline not working if
2341 // range included leading 'newlines'.
2342 // In IE, if a user starts a selection at the very end of a line,
2343 // then the native browser commands will fail to execute correctly.
2344 // To work around the issue, we can remove all empty nodes from
2345 // the start of the range selection.
2346 var selection
= rangeapi
.getSelection(this.window
);
2347 if(selection
&& selection
.rangeCount
&& !selection
.isCollapsed
){
2348 var range
= selection
.getRangeAt(0);
2349 var firstNode
= range
.startContainer
;
2350 var startOffset
= range
.startOffset
;
2352 while(firstNode
.nodeType
=== 3/*text*/ && startOffset
>= firstNode
.length
&& firstNode
.nextSibling
){
2353 //traverse the text nodes until we get to the one that is actually highlighted
2354 startOffset
= startOffset
- firstNode
.length
;
2355 firstNode
= firstNode
.nextSibling
;
2358 //Remove the starting ranges until the range does not start with an empty node.
2360 while(this._isNodeEmpty(firstNode
, startOffset
) && firstNode
!== lastNode
){
2361 lastNode
=firstNode
; //this will break the loop in case we can't find the next sibling
2362 range
= this._removeStartingRangeFromRange(firstNode
, range
); //move the start container to the next node in the range
2363 firstNode
= range
.startContainer
;
2364 startOffset
= 0; //start at the beginning of the new starting range
2366 selection
.removeAllRanges();// this will work as long as users cannot select multiple ranges. I have not been able to do that in the editor.
2367 selection
.addRange(range
);
2371 _adaptIEFormatAreaAndExec: function(command
){
2373 // Function to handle IE's quirkiness regarding how it handles
2374 // format commands on a word. This involves a lit of node splitting
2375 // and format cloning.
2377 // The format command, needed to check if the desired
2378 // command is true or not.
2379 var selection
= rangeapi
.getSelection(this.window
);
2380 var doc
= this.document
;
2381 var rs
, ret
, range
, txt
, startNode
, endNode
, breaker
, sNode
;
2382 if(command
&& selection
&& selection
.isCollapsed
){
2383 var isApplied
= this.queryCommandValue(command
);
2386 // We have to split backwards until we hit the format
2387 var nNames
= this._tagNamesForCommand(command
);
2388 range
= selection
.getRangeAt(0);
2389 var fs
= range
.startContainer
;
2390 if(fs
.nodeType
=== 3){
2391 var offset
= range
.endOffset
;
2392 if(fs
.length
< offset
){
2393 //We are not looking from the right node, try to locate the correct one
2394 ret
= this._adjustNodeAndOffset(rs
, offset
);
2396 offset
= ret
.offset
;
2400 while(fs
&& fs
!== this.editNode
){
2401 // We have to walk back and see if this is still a format or not.
2402 // Hm, how do I do this?
2403 var tName
= fs
.tagName
? fs
.tagName
.toLowerCase() : "";
2404 if(array
.indexOf(nNames
, tName
) > -1){
2411 // Okay, we have a stopping place, time to split things apart.
2413 // Okay, we know how far we have to split backwards, so we have to split now.
2414 rs
= range
.startContainer
;
2415 var newblock
= doc
.createElement(topNode
.tagName
);
2416 domConstruct
.place(newblock
, topNode
, "after");
2417 if(rs
&& rs
.nodeType
=== 3){
2418 // Text node, we have to split it.
2419 var nodeToMove
, tNode
;
2420 var endOffset
= range
.endOffset
;
2421 if(rs
.length
< endOffset
){
2422 //We are not splitting the right node, try to locate the correct one
2423 ret
= this._adjustNodeAndOffset(rs
, endOffset
);
2425 endOffset
= ret
.offset
;
2429 startNode
= doc
.createTextNode(txt
.substring(0, endOffset
));
2430 var endText
= txt
.substring(endOffset
, txt
.length
);
2432 endNode
= doc
.createTextNode(endText
);
2434 // Place the split, then remove original nodes.
2435 domConstruct
.place(startNode
, rs
, "before");
2437 breaker
= doc
.createElement("span");
2438 breaker
.className
= "ieFormatBreakerSpan";
2439 domConstruct
.place(breaker
, rs
, "after");
2440 domConstruct
.place(endNode
, breaker
, "after");
2443 domConstruct
.destroy(rs
);
2445 // Okay, we split the text. Now we need to see if we're
2446 // parented to the block element we're splitting and if
2447 // not, we have to split all the way up. Ugh.
2448 var parentC
= startNode
.parentNode
;
2451 while(parentC
!== topNode
){
2452 var tg
= parentC
.tagName
;
2453 tagData
= {tagName
: tg
};
2454 tagList
.push(tagData
);
2456 var newTg
= doc
.createElement(tg
);
2457 // Clone over any 'style' data.
2460 if(parentC
.style
.cssText
){
2461 newTg
.style
.cssText
= parentC
.style
.cssText
;
2462 tagData
.cssText
= parentC
.style
.cssText
;
2466 // If font also need to clone over any font data.
2467 if(parentC
.tagName
=== "FONT"){
2469 newTg
.color
= parentC
.color
;
2470 tagData
.color
= parentC
.color
;
2473 newTg
.face
= parentC
.face
;
2474 tagData
.face
= parentC
.face
;
2476 if(parentC
.size
){ // this check was necessary on IE
2477 newTg
.size
= parentC
.size
;
2478 tagData
.size
= parentC
.size
;
2481 if(parentC
.className
){
2482 newTg
.className
= parentC
.className
;
2483 tagData
.className
= parentC
.className
;
2486 // Now move end node and every sibling
2487 // after it over into the new tag.
2489 nodeToMove
= endNode
;
2491 tNode
= nodeToMove
.nextSibling
;
2492 newTg
.appendChild(nodeToMove
);
2496 if(newTg
.tagName
== parentC
.tagName
){
2497 breaker
= doc
.createElement("span");
2498 breaker
.className
= "ieFormatBreakerSpan";
2499 domConstruct
.place(breaker
, parentC
, "after");
2500 domConstruct
.place(newTg
, breaker
, "after");
2502 domConstruct
.place(newTg
, parentC
, "after");
2504 startNode
= parentC
;
2506 parentC
= parentC
.parentNode
;
2509 // Lastly, move the split out all the split tags
2510 // to the new block as they should now be split properly.
2512 nodeToMove
= endNode
;
2513 if(nodeToMove
.nodeType
=== 1 || (nodeToMove
.nodeType
=== 3 && nodeToMove
.nodeValue
)){
2514 // Non-blank text and non-text nodes need to clear out that blank space
2515 // before moving the contents.
2516 newblock
.innerHTML
= "";
2519 tNode
= nodeToMove
.nextSibling
;
2520 newblock
.appendChild(nodeToMove
);
2525 // We had intermediate tags, we have to now recreate them inbetween the split
2526 // and restore what styles, classnames, etc, we can.
2529 tagData
= tagList
.pop();
2530 var newContTag
= doc
.createElement(tagData
.tagName
);
2531 if(tagData
.cssText
&& newContTag
.style
){
2532 newContTag
.style
.cssText
= tagData
.cssText
;
2534 if(tagData
.className
){
2535 newContTag
.className
= tagData
.className
;
2537 if(tagData
.tagName
=== "FONT"){
2539 newContTag
.color
= tagData
.color
;
2542 newContTag
.face
= tagData
.face
;
2545 newContTag
.size
= tagData
.size
;
2548 domConstruct
.place(newContTag
, newblock
, "before");
2549 while(tagList
.length
){
2550 tagData
= tagList
.pop();
2551 var newTgNode
= doc
.createElement(tagData
.tagName
);
2552 if(tagData
.cssText
&& newTgNode
.style
){
2553 newTgNode
.style
.cssText
= tagData
.cssText
;
2555 if(tagData
.className
){
2556 newTgNode
.className
= tagData
.className
;
2558 if(tagData
.tagName
=== "FONT"){
2560 newTgNode
.color
= tagData
.color
;
2563 newTgNode
.face
= tagData
.face
;
2566 newTgNode
.size
= tagData
.size
;
2569 newContTag
.appendChild(newTgNode
);
2570 newContTag
= newTgNode
;
2573 // Okay, everything is theoretically split apart and removed from the content
2574 // so insert the dummy text to select, select it, then
2575 // clear to position cursor.
2576 sNode
= doc
.createTextNode(".");
2577 breaker
.appendChild(sNode
);
2578 newContTag
.appendChild(sNode
);
2579 newrange
= rangeapi
.create(this.window
);
2580 newrange
.setStart(sNode
, 0);
2581 newrange
.setEnd(sNode
, sNode
.length
);
2582 selection
.removeAllRanges();
2583 selection
.addRange(newrange
);
2584 this._sCall("collapse", [false]);
2585 sNode
.parentNode
.innerHTML
= "";
2587 // No extra tags, so we have to insert a breaker point and rely
2588 // on filters to remove it later.
2589 breaker
= doc
.createElement("span");
2590 breaker
.className
="ieFormatBreakerSpan";
2591 sNode
= doc
.createTextNode(".");
2592 breaker
.appendChild(sNode
);
2593 domConstruct
.place(breaker
, newblock
, "before");
2594 newrange
= rangeapi
.create(this.window
);
2595 newrange
.setStart(sNode
, 0);
2596 newrange
.setEnd(sNode
, sNode
.length
);
2597 selection
.removeAllRanges();
2598 selection
.addRange(newrange
);
2599 this._sCall("collapse", [false]);
2600 sNode
.parentNode
.innerHTML
= "";
2602 if(!newblock
.firstChild
){
2603 // Empty, we don't need it. Split was at end or similar
2605 domConstruct
.destroy(newblock
);
2612 range
= selection
.getRangeAt(0);
2613 rs
= range
.startContainer
;
2614 if(rs
&& rs
.nodeType
=== 3){
2615 // Text node, we have to split it.
2616 var offset
= range
.startOffset
;
2617 if(rs
.length
< offset
){
2618 //We are not splitting the right node, try to locate the correct one
2619 ret
= this._adjustNodeAndOffset(rs
, offset
);
2621 offset
= ret
.offset
;
2624 startNode
= doc
.createTextNode(txt
.substring(0, offset
));
2625 var endText
= txt
.substring(offset
);
2627 endNode
= doc
.createTextNode(txt
.substring(offset
));
2629 // Create a space, we'll select and bold it, so
2630 // the whole word doesn't get bolded
2631 breaker
= doc
.createElement("span");
2632 sNode
= doc
.createTextNode(".");
2633 breaker
.appendChild(sNode
);
2634 if(startNode
.length
){
2635 domConstruct
.place(startNode
, rs
, "after");
2639 domConstruct
.place(breaker
, startNode
, "after");
2641 domConstruct
.place(endNode
, breaker
, "after");
2643 domConstruct
.destroy(rs
);
2644 var newrange
= rangeapi
.create(this.window
);
2645 newrange
.setStart(sNode
, 0);
2646 newrange
.setEnd(sNode
, sNode
.length
);
2647 selection
.removeAllRanges();
2648 selection
.addRange(newrange
);
2649 doc
.execCommand(command
);
2650 domConstruct
.place(breaker
.firstChild
, breaker
, "before");
2651 domConstruct
.destroy(breaker
);
2652 newrange
.setStart(sNode
, 0);
2653 newrange
.setEnd(sNode
, sNode
.length
);
2654 selection
.removeAllRanges();
2655 selection
.addRange(newrange
);
2656 this._sCall("collapse", [false]);
2657 sNode
.parentNode
.innerHTML
= "";
2666 _adaptIEList: function(command
/*===== , argument =====*/){
2668 // This function handles normalizing the IE list behavior as
2669 // much as possible.
2671 // The list command to execute.
2673 // Any additional argument.
2676 var selection
= rangeapi
.getSelection(this.window
);
2677 if(selection
.isCollapsed
){
2678 // In the case of no selection, lets commonize the behavior and
2679 // make sure that it indents if needed.
2680 if(selection
.rangeCount
&& !this.queryCommandValue(command
)){
2681 var range
= selection
.getRangeAt(0);
2682 var sc
= range
.startContainer
;
2683 if(sc
&& sc
.nodeType
== 3){
2684 // text node. Lets see if there is a node before it that isn't
2685 // some sort of breaker.
2686 if(!range
.startOffset
){
2687 // We're at the beginning of a text area. It may have been br split
2688 // Who knows? In any event, we must create the list manually
2689 // or IE may shove too much into the list element. It seems to
2690 // grab content before the text node too if it's br split.
2691 // Why can't IE work like everyone else?
2693 // Create a space, we'll select and bold it, so
2694 // the whole word doesn't get bolded
2696 if(command
=== "insertorderedlist"){
2699 var list
= this.document
.createElement(lType
);
2700 var li
= domConstruct
.create("li", null, list
);
2701 domConstruct
.place(list
, sc
, "before");
2702 // Move in the text node as part of the li.
2704 // We need a br after it or the enter key handler
2705 // sometimes throws errors.
2706 domConstruct
.create("br", null, list
, "after");
2707 // Okay, now lets move our cursor to the beginning.
2708 var newrange
= rangeapi
.create(this.window
);
2709 newrange
.setStart(sc
, 0);
2710 newrange
.setEnd(sc
, sc
.length
);
2711 selection
.removeAllRanges();
2712 selection
.addRange(newrange
);
2713 this._sCall("collapse", [true]);
2722 _handleTextColorOrProperties: function(command
, argument
){
2724 // This function handles appplying text color as best it is
2725 // able to do so when the selection is collapsed, making the
2726 // behavior cross-browser consistent. It also handles the name
2731 // Any additional arguments.
2734 var selection
= rangeapi
.getSelection(this.window
);
2735 var doc
= this.document
;
2736 var rs
, ret
, range
, txt
, startNode
, endNode
, breaker
, sNode
;
2737 argument
= argument
|| null;
2738 if(command
&& selection
&& selection
.isCollapsed
){
2739 if(selection
.rangeCount
){
2740 range
= selection
.getRangeAt(0);
2741 rs
= range
.startContainer
;
2742 if(rs
&& rs
.nodeType
=== 3){
2743 // Text node, we have to split it.
2744 var offset
= range
.startOffset
;
2745 if(rs
.length
< offset
){
2746 //We are not splitting the right node, try to locate the correct one
2747 ret
= this._adjustNodeAndOffset(rs
, offset
);
2749 offset
= ret
.offset
;
2752 startNode
= doc
.createTextNode(txt
.substring(0, offset
));
2753 var endText
= txt
.substring(offset
);
2755 endNode
= doc
.createTextNode(txt
.substring(offset
));
2757 // Create a space, we'll select and bold it, so
2758 // the whole word doesn't get bolded
2759 breaker
= doc
.createElement("span");
2760 sNode
= doc
.createTextNode(".");
2761 breaker
.appendChild(sNode
);
2762 // Create a junk node to avoid it trying to style the breaker.
2763 // This will get destroyed later.
2764 var extraSpan
= doc
.createElement("span");
2765 breaker
.appendChild(extraSpan
);
2766 if(startNode
.length
){
2767 domConstruct
.place(startNode
, rs
, "after");
2771 domConstruct
.place(breaker
, startNode
, "after");
2773 domConstruct
.place(endNode
, breaker
, "after");
2775 domConstruct
.destroy(rs
);
2776 var newrange
= rangeapi
.create(this.window
);
2777 newrange
.setStart(sNode
, 0);
2778 newrange
.setEnd(sNode
, sNode
.length
);
2779 selection
.removeAllRanges();
2780 selection
.addRange(newrange
);
2782 // WebKit is frustrating with positioning the cursor.
2783 // It stinks to have a selected space, but there really
2784 // isn't much choice here.
2785 var style
= "color";
2786 if(command
=== "hilitecolor" || command
=== "backcolor"){
2787 style
= "backgroundColor";
2789 domStyle
.set(breaker
, style
, argument
);
2790 this._sCall("remove", []);
2791 domConstruct
.destroy(extraSpan
);
2792 breaker
.innerHTML
= " "; //
2793 this._sCall("selectElement", [breaker
]);
2796 this.execCommand(command
, argument
);
2797 domConstruct
.place(breaker
.firstChild
, breaker
, "before");
2798 domConstruct
.destroy(breaker
);
2799 newrange
.setStart(sNode
, 0);
2800 newrange
.setEnd(sNode
, sNode
.length
);
2801 selection
.removeAllRanges();
2802 selection
.addRange(newrange
);
2803 this._sCall("collapse", [false]);
2804 sNode
.parentNode
.removeChild(sNode
);
2813 _adjustNodeAndOffset: function(/*DomNode*/node
, /*Int*/offset
){
2815 // In the case there are multiple text nodes in a row the offset may not be within the node.
2816 // If the offset is larger than the node length, it will attempt to find
2817 // the next text sibling until it locates the text node in which the offset refers to
2819 // The node to check.
2821 // The position to find within the text node
2824 while(node
.length
< offset
&& node
.nextSibling
&& node
.nextSibling
.nodeType
=== 3){
2825 //Adjust the offset and node in the case of multiple text nodes in a row
2826 offset
= offset
- node
.length
;
2827 node
= node
.nextSibling
;
2829 return {"node": node
, "offset": offset
};
2832 _tagNamesForCommand: function(command
){
2834 // Function to return the tab names that are associated
2835 // with a particular style.
2837 // The command to return tags for.
2840 if(command
=== "bold"){
2841 return ["b", "strong"];
2842 }else if(command
=== "italic"){
2844 }else if(command
=== "strikethrough"){
2845 return ["s", "strike"];
2846 }else if(command
=== "superscript"){
2848 }else if(command
=== "subscript"){
2850 }else if(command
=== "underline"){
2856 _stripBreakerNodes: function(/*DOMNode*/ node
){
2858 // Function for stripping out the breaker spans inserted by the formatting command.
2859 // Registered as a filter for IE, handles the breaker spans needed to fix up
2860 // How bold/italic/etc, work when selection is collapsed (single cursor).
2861 if(!this.isLoaded
){ return; } // this method requires init to be complete
2862 query(".ieFormatBreakerSpan", node
).forEach(function(b
){
2863 while(b
.firstChild
){
2864 domConstruct
.place(b
.firstChild
, b
, "before");
2866 domConstruct
.destroy(b
);