]>
git.wh0rd.org - tt-rss.git/blob - 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
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
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
){
36 var _Widget = dijit._Widget;
37 var _CssStateMixin = dijit._CssStateMixin;
41 // dijit/_editor/RichText
43 // dijit._editor.RichText is the core of dijit.Editor, which provides basic
44 // WYSIWYG editing features.
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:
50 // <textarea id="dijit._editor.RichText.value" style="display:none;position:absolute;top:-100px;left:-100px;height:3px;width:3px;overflow:hidden;"></textarea>
53 var RichText
= declare("dijit._editor.RichText", [_Widget
, _CssStateMixin
], {
55 // dijit._editor.RichText is the core of dijit.Editor, which provides basic
56 // WYSIWYG editing features.
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 <TEXTAREA> tag, since the browser unescapes XML escape characters,
63 // like <. This can have unexpected behavior and lead to security issues
64 // such as scripting attacks.
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
= [];
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
= [];
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
= [];
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
= [];
95 // editingAreaStyleSheets: dojo._URL[]
96 // array to store all the stylesheets applied to the editing area
97 this.editingAreaStyleSheets
= [];
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
);
103 this._keyHandlers
= {};
105 if(params
&& lang
.isString(params
.value
)){
106 this.value
= params
.value
;
109 this.onLoadDeferred
= new Deferred();
112 baseClass
: "dijitEditor",
114 // inheritWidth: Boolean
115 // whether to inherit the parent's width or simply use 100%
118 // focusOnLoad: [deprecated] Boolean
119 // Focus into this widget when the page is loaded
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.
128 // styleSheets: [const] String
129 // semicolon (";") separated list of css files for the editing area
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%".
141 // The minimum height that the editor should have.
144 // isClosed: [private] Boolean
147 // isLoaded: [private] Boolean
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__%%**@@",
155 // _NAME_CONTENT_SEP: [private] String
156 // USed to separate name from content. Just a colon isn't safe.
157 _NAME_CONTENT_SEP
: "@@**%%:%%**@@",
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,
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.
170 // disableSpellCheck: [const] Boolean
171 // When true, disables the browser's native spell checking, if supported.
172 // Works only in Firefox.
173 disableSpellCheck
: false,
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.");
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
);
184 this.contentPreFilters
= [this._normalizeFontStyle
].concat(this.contentPreFilters
);
185 this.contentPostFilters
= [this._removeMozBogus
].concat(this.contentPostFilters
);
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
);
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
);
198 this.inherited(arguments
);
200 topic
.publish(dijit
._scopeName
+ "._editor.RichText::init", this);
202 this.setupDefaultShortcuts();
205 setupDefaultShortcuts: function(){
207 // Add some default key handlers
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
215 var exec
= lang
.hitch(this, function(cmd
, arg
){
217 return !this.execCommand(cmd
,arg
);
221 var ctrlKeyHandlers
= {
224 u
: exec("underline"),
225 a
: exec("selectall"),
226 s: function(){ this.save(true); },
227 m: function(){ this.isTabIndent
= !this.isTabIndent
; },
229 "1": exec("formatblock", "h1"),
230 "2": exec("formatblock", "h2"),
231 "3": exec("formatblock", "h3"),
232 "4": exec("formatblock", "h4"),
234 "\\": exec("insertunorderedlist")
238 ctrlKeyHandlers
.Z
= exec("redo"); //FIXME: undo?
242 for(key
in ctrlKeyHandlers
){
243 this.addKeyHandler(key
, true, false, ctrlKeyHandlers
[key
]);
247 // events: [private] String[]
248 // events which should be connected to the underlying editing area
249 events
: ["onKeyPress", "onKeyDown", "onKeyUp"], // onClick handled specially
251 // captureEvents: [deprecated] String[]
252 // Events which should be connected to the underlying editing
253 // area, events in this array will be addListener with
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
259 _editorCommandsLocalized
: false,
260 _localizeEditorCommands: function(){
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).
267 if(RichText
._editorCommandsLocalized
){
268 // Use the already generate cache of mappings.
269 this._local2NativeFormatNames
= RichText
._local2NativeFormatNames
;
270 this._native2LocalFormatNames
= RichText
._native2LocalFormatNames
;
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
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/>";
291 localhtml
+= "<"+format
+"><li>content</li></"+format
+"><br/>";
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
);
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
;
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 */ }
314 div
.parentNode
.removeChild(div
);
317 setTimeout(inject
, 0);
320 open: function(/*DomNode?*/ element
){
322 // Transforms the node referenced in this.domNode into a rich text editing
325 // Sets up the editing area asynchronously. This will result in
326 // the creation and replacement with an iframe.
330 if(!this.onLoadDeferred
|| this.onLoadDeferred
.fired
>= 0){
331 this.onLoadDeferred
= new Deferred();
334 if(!this.isClosed
){ this.close(); }
335 topic
.publish(dijit
._scopeName
+ "._editor.RichText::open", this);
337 if(arguments
.length
=== 1 && element
.nodeName
){ // else unchanged
338 this.domNode
= element
;
341 var dn
= this.domNode
;
343 // "html" will hold the innerHTML of the srcNodeRef and will be used to
344 // initialize the editor.
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
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
);
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
371 position
: "absolute",
375 if(has("ie")){ //nasty IE bug: abnormal formatting if overflow is not hidden
377 this.__overflow
= s
.overflow
;
378 s
.overflow
= "hidden";
382 setTimeout(tmpFunc
, 10);
388 var resetValue
= ta
.value
;
389 this.reset = function(){
390 var current
= this.getValue();
391 if(current
!== resetValue
){
392 this.replaceValue(resetValue
);
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();
403 html
= htmlapi
.getChildrenHtml(dn
);
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>";
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
);
424 this.name
= this.id
+ "_AUTOGEN";
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
){
438 datas
= datas
.splice(i
, 1);
439 saveTextarea
.value
= datas
.join(this._SEPARATOR
);
445 if(!RichText
._globalSaveHandler
){
446 RichText
._globalSaveHandler
= {};
447 unload
.addOnUnload(function(){
449 for(id
in RichText
._globalSaveHandler
){
450 var f
= RichText
._globalSaveHandler
[id
];
451 if(lang
.isFunction(f
)){
457 RichText
._globalSaveHandler
[this.id
] = lang
.hitch(this, "_saveContent");
460 this.isClosed
= false;
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%";
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 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
);
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);
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";
517 domClass
.add(this.domNode
, this.baseClass
);
520 //static cache variables shared among all instance of this class
521 _local2NativeFormatNames
: {},
522 _native2LocalFormatNames
: {},
524 _getIframeDocTxt: function(){
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.
530 var _cs
= domStyle
.getComputedStyle(this.domNode
);
532 // The contents inside of <body>. The real contents are set later via a call to setValue().
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>";
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
= " "; //
547 var font
= [ _cs
.fontWeight
, _cs
.fontSize
, _cs
.fontFamily
].join(" ");
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
);
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";
565 this.style
.replace(/(^|;)\s*(line-|font-?)[^;]+/ig, function(match
){
566 match
= match
.replace(/^;/ig,"") + ';';
567 var s
= match
.split(":")[0];
573 for(i
= 0; i
< s
.length
; i
++){
578 c
= s
.charAt(i
).toUpperCase();
583 domStyle
.set(self
.domNode
, sC
, "");
585 userStyle
+= match
+ ';';
589 // need to find any associated label element and update iframe document title
590 var label
=query('label[for="'+this.id
+'"]');
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",
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
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":""),
609 // TODO: left positioning will cause contents to disappear out of view
610 // if it gets too wide for the visible area
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",
621 "\tp{ margin: 1em 0; }\n",
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" : ""),
632 this._applyEditingAreaStyleSheets(),"\n",
634 (setBodyId
?"id='dijitEditorBody' ":""),
635 "onload='frameElement._loadFunc(window,document)' style='"+userStyle
+"'>", html
, "</body>\n</html>"
636 ].join(""); // String
639 _applyEditingAreaStyleSheets: function(){
641 // apply the specified css files in styleSheets
645 if(this.styleSheets
){
646 files
= this.styleSheets
.split(';');
647 this.styleSheets
= '';
650 //empty this.editingAreaStyleSheets here, as it will be filled in addStyleSheet
651 files
= files
.concat(this.editingAreaStyleSheets
);
652 this.editingAreaStyleSheets
= [];
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
+'"/>';
663 addStyleSheet: function(/*dojo._Url*/ uri
){
665 // add an external stylesheet for the editing area
667 // A dojo.uri.Uri pointing to the url of the external css file
668 var url
=uri
.toString();
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();
675 if(array
.indexOf(this.editingAreaStyleSheets
, url
) > -1){
676 // console.debug("dijit._editor.RichText.addStyleSheet: Style sheet "+url+" is already applied");
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";
690 head
.appendChild(stylesheet
);
695 removeStyleSheet: function(/*dojo._Url*/ uri
){
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();
703 var index
= array
.indexOf(this.editingAreaStyleSheets
, url
);
705 // console.debug("dijit._editor.RichText.removeStyleSheet: Style sheet "+url+" has not been applied");
708 delete this.editingAreaStyleSheets
[index
];
709 win
.withGlobal(this.window
,'query', dojo
, ['link:[href="'+url
+'"]']).orphan();
713 // The editor is disabled; the text cannot be changed.
716 _mozSettingProps
: {'styleWithCSS':false},
717 _setDisabledAttr: function(/*Boolean*/ 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
;
727 setTimeout(function(){
728 if(_this
.editNode
){ // guard in case widget destroyed before timeout
729 _this
.editNode
.unselectable
= "off";
735 this.document
.designMode
=(value
?'off':'on');
736 }catch(e
){ return; } // ! _disabledOK
737 if(!value
&& this._mozSettingProps
){
738 var ps
= this._mozSettingProps
;
741 if(ps
.hasOwnProperty(n
)){
743 this.document
.execCommand(n
,false,ps
[n
]);
748 // this.document.execCommand('contentReadOnly', false, value);
750 // this.blur(); //to remove the blinking caret
753 this._disabledOK
= true;
759 onLoad: function(/*String*/ html
){
761 // Handler after the iframe finishes loading.
763 // Editor contents should be set to this value
767 // TODO: rename this to _onLoad, make empty public onLoad() method, deprecate/make protected onLoadDeferred handler?
769 if(!this.window
.__registeredWindow
){
770 this.window
.__registeredWindow
= true;
771 this._iframeRegHandle
= focus
.registerIframe(this.iframe
);
773 if(!has("ie") && !has("webkit") && (this.height
|| has("mozilla"))){
774 this.editNode
=this.document
.body
;
776 // there's a wrapper div around the content, see _getIframeDocTxt().
777 this.editNode
=this.document
.body
.firstChild
;
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(); };
784 this.focusNode
= this.editNode
; // for InlineEditBox
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
);
793 this.connect(ap
, "onmouseup", "onClick"); // mouseup in the margin does not generate an onclick event
795 if(has("ie")){ // IE contentEditable
796 this.connect(this.document
, "onmousedown", "_onIEMouseDown"); // #4996 fix focus
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;
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
;
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
){
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);
830 // Try to make sure 'hidden' elements aren't visible in edit mode (like browsers other than IE
833 this.document
.execCommand('RespectVisibilityInDesign', true, null);
834 }catch(e
){/* squelch */}
837 this.isLoaded
= true;
839 this.set('disabled', this.disabled
); // initialize content to editable (or not)
841 // Note that setValue() call will only work after isLoaded is set to true (above)
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(){
848 if(this.onLoadDeferred
){
849 this.onLoadDeferred
.callback(true);
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
); }));
857 // Save off the initial content now
858 this.value
= this.getValue(true);
860 if(this.setValueDeferred
){
861 this.setValueDeferred
.addCallback(setContent
);
867 onKeyDown: function(/* Event */ e
){
869 // Handler for onkeydown event
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.
877 if(e
.keyCode
=== keys
.TAB
&& this.isTabIndent
){
878 event
.stop(e
); //prevent tab from moving focus out of editor
880 // FIXME: this is a poor-man's indent/outdent. It would be
881 // better if it added 4 " " 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"));
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
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();
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
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!
905 e
.charCode
= e
.keyCode
;
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.
920 onKeyUp: function(/*===== e =====*/){
922 // Handler for onkeyup event
927 setDisabled: function(/*Boolean*/ disabled
){
929 // Deprecated, use set('disabled', ...) instead.
932 kernel
.deprecated('dijit.Editor::setDisabled is deprecated','use dijit.Editor::attr("disabled",boolean) instead', 2.0);
933 this.set('disabled',disabled
);
935 _setValueAttr: function(/*String*/ value
){
937 // Registers that attr("value", foo) should call setValue(foo)
938 this.setValue(value
);
940 _setDisableSpellCheckAttr: function(/*Boolean*/ disabled
){
942 domAttr
.set(this.document
.body
, "spellcheck", !disabled
);
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
);
949 this._set("disableSpellCheck", disabled
);
952 onKeyPress: function(e
){
954 // Handle the various key events
958 var c
= (e
.keyChar
&& e
.keyChar
.toLowerCase()) || e
.keyCode
,
959 handlers
= this._keyHandlers
[c
],
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
)){
974 // function call after the character has been inserted
975 if(!this._onKeyHitch
){
976 this._onKeyHitch
= lang
.hitch(this, "onKeyPressed");
978 setTimeout(this._onKeyHitch
, 1);
982 addKeyHandler: function(/*String*/ key
, /*Boolean*/ ctrl
, /*Boolean*/ shift
, /*Function*/ handler
){
984 // Add a handler for a keyboard shortcut
986 // The key argument should be in lowercase if it is a letter character
989 if(!lang
.isArray(this._keyHandlers
[key
])){
990 this._keyHandlers
[key
] = [];
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,
1000 onKeyPressed: function(){
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)
1006 this.onDisplayChanged(/*e*/); // can't pass in e
1009 onClick: function(/*Event*/ e
){
1011 // Handler for when the user clicks.
1015 // console.info('onClick',this._tryDesignModeOn);
1016 this.onDisplayChanged(e
);
1019 _onIEMouseDown: function(){
1021 // IE only to prevent 2 clicks to focus
1025 if(!this.focused
&& !this.disabled
){
1030 _onBlur: function(e
){
1032 // Called from focus manager when focus has moved away from this editor
1036 // console.info('_onBlur')
1038 this.inherited(arguments
);
1040 var newValue
= this.getValue(true);
1041 if(newValue
!== this.value
){
1042 this.onChange(newValue
);
1044 this._set("value", newValue
);
1047 _onFocus: function(/*Event*/ e
){
1049 // Called from focus manager when focus has moved into this editor
1053 // console.info('_onFocus')
1055 if(!this._disabledOK
){
1056 this.set('disabled', false);
1058 this.inherited(arguments
);
1062 // TODO: remove in 2.0
1065 // Remove focus from this instance.
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();
1077 // Move focus to this editor
1079 this.focusOnLoad
= true;
1082 if(this._cursorToStart
){
1083 delete this._cursorToStart
;
1084 if(this.editNode
.childNodes
){
1085 this.placeCursorAtStart(); // this calls focus() so return
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
1098 // TODO: should we throw here?
1099 // console.debug("Have no idea how to focus into the editor!");
1104 updateInterval
: 200,
1106 onDisplayChanged: function(/*Event*/ /*===== e =====*/){
1108 // This event will be fired every time the display context
1109 // changes and the result needs to be reflected in the UI.
1111 // If you don't want to have update too often,
1112 // onNormalizedDisplayChanged should be used instead
1116 // var _t=new Date();
1117 if(this._updateTimer
){
1118 clearTimeout(this._updateTimer
);
1120 if(!this._updateHandler
){
1121 this._updateHandler
= lang
.hitch(this,"onNormalizedDisplayChanged");
1123 this._updateTimer
= setTimeout(this._updateHandler
, this.updateInterval
);
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.
1128 onNormalizedDisplayChanged: function(){
1130 // This event is fired every updateInterval ms or more
1132 // If something needs to happen immediately after a
1133 // user change, please use onDisplayChanged instead.
1136 delete this._updateTimer
;
1138 onChange: function(/*===== newContent =====*/){
1140 // This is fired if and only if the editor loses focus and
1141 // the content is changed.
1143 _normalizeCommand: function(/*String*/ cmd
, /*Anything?*/argument
){
1145 // Used as the advice function to map our
1146 // normalized set of commands to those supported by the target
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";
1162 queryCommandAvailable: function(/*String*/ command
){
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.
1168 // The command to test for
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
));
1178 _queryCommandAvailable: function(/*String*/ command
){
1180 // See queryCommandAvailable().
1185 var mozilla
= 1 << 1;
1186 var webkit
= 1 << 2;
1189 function isSupportedBy(browsers
){
1191 ie
: Boolean(browsers
& ie
),
1192 mozilla
: Boolean(browsers
& mozilla
),
1193 webkit
: Boolean(browsers
& webkit
),
1194 opera
: Boolean(browsers
& opera
)
1198 var supportedBy
= null;
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
);
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
);
1218 case "blockdirltr": case "blockdirrtl":
1219 case "dirltr": case "dirrtl":
1220 case "inlinedirltr": case "inlinedirrtl":
1221 supportedBy
= isSupportedBy(ie
);
1223 case "cut": case "copy": case "paste":
1224 supportedBy
= isSupportedBy( ie
| mozilla
| webkit
);
1228 supportedBy
= isSupportedBy(mozilla
| ie
);
1231 case "insertcell": case "insertcol": case "insertrow":
1232 case "deletecells": case "deletecols": case "deleterows":
1233 case "mergecells": case "splitcell":
1234 supportedBy
= isSupportedBy(ie
| mozilla
);
1237 default: return false;
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
1246 execCommand: function(/*String*/ command
, argument
){
1248 // Executes a command in the Rich Text area
1250 // The command to execute
1252 // An optional argument to the command
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
1262 command
= this._normalizeCommand(command
, argument
);
1264 if(argument
!== undefined){
1265 if(command
=== "heading"){
1266 throw new Error("unimplemented");
1267 }else if((command
=== "formatblock") && has("ie")){
1268 argument
= '<'+argument
+'>';
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";
1277 returnValue
= this[implFunc
](argument
);
1279 argument
= arguments
.length
> 1 ? argument
: null;
1280 if(argument
|| command
!== "createlink"){
1281 returnValue
= this.document
.execCommand(command
, false, argument
);
1285 this.onDisplayChanged();
1289 queryCommandEnabled: function(/*String*/ command
){
1291 // Check whether a command is enabled or not.
1293 // The command to execute
1296 if(this.disabled
|| !this._disabledOK
){ return false; }
1298 command
= this._normalizeCommand(command
);
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";
1306 return this[implFunc
](command
);
1308 return this._browserQueryCommandEnabled(command
);
1312 queryCommandState: function(command
){
1314 // Check the state of a given command and returns true or false.
1318 if(this.disabled
|| !this._disabledOK
){ return false; }
1319 command
= this._normalizeCommand(command
);
1321 return this.document
.queryCommandState(command
);
1323 //Squelch, occurs if editor is hidden on FF 3 (and maybe others.)
1328 queryCommandValue: function(command
){
1330 // Check the value of a given command. This matters most for
1331 // custom selections and complex values like font value setting.
1335 if(this.disabled
|| !this._disabledOK
){ return false; }
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"){
1343 oldValue
= this.document
.queryCommandValue("styleWithCSS");
1347 this.document
.execCommand("styleWithCSS", false, true);
1348 r
= this.document
.queryCommandValue(command
);
1349 this.document
.execCommand("styleWithCSS", false, oldValue
);
1351 r
= this.document
.queryCommandValue(command
);
1358 _sCall: function(name
, args
){
1360 // Run the named method of dijit._editor.selection over the
1361 // current editor instance's window, with the passed args.
1364 return win
.withGlobal(this.window
, name
, selectionapi
, args
);
1367 // FIXME: this is a TON of code duplication. Why?
1369 placeCursorAtStart: function(){
1371 // Place the cursor at the start of the editing area.
1377 //see comments in placeCursorAtEnd
1380 // TODO: Is this branch even necessary?
1381 var first
=this.editNode
.firstChild
;
1383 if(first
.nodeType
=== 3){
1384 if(first
.nodeValue
.replace(/^\s+|\s+$/g, "").length
>0){
1386 this._sCall("selectElement", [ first
]);
1389 }else if(first
.nodeType
=== 1){
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
]);
1396 // Collapse inside tags with children.
1397 this._sCall("selectElementChildren", [ first
]);
1401 first
= first
.nextSibling
;
1405 this._sCall("selectElementChildren", [ this.editNode
]);
1408 this._sCall("collapse", [ true ]);
1412 placeCursorAtEnd: function(){
1414 // Place the cursor at the end of the editing area.
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
1426 var last
=this.editNode
.lastChild
;
1428 if(last
.nodeType
=== 3){
1429 if(last
.nodeValue
.replace(/^\s+|\s+$/g, "").length
>0){
1431 this._sCall("selectElement", [ last
]);
1434 }else if(last
.nodeType
=== 1){
1437 this._sCall("selectElement", [ last
.lastChild
]);
1439 this._sCall("selectElement", [ last
]);
1443 last
= last
.previousSibling
;
1447 this._sCall("selectElementChildren", [ this.editNode
]);
1450 this._sCall("collapse", [ false ]);
1454 getValue: function(/*Boolean?*/ nonDestructive
){
1456 // Return the current content of the editing area (post filters
1457 // are applied). Users should call get('value') instead.
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.
1466 if(this.isClosed
|| !this.isLoaded
){
1467 return this.textarea
.value
;
1471 return this._postFilterContent(null, nonDestructive
);
1473 _getValueAttr: function(){
1475 // Hook to make attr("value") work
1476 return this.getValue(true);
1479 setValue: function(/*String*/ html
){
1481 // This function sets the content. No undo history is preserved.
1482 // Users should use set('value', ...) instead.
1486 // TODO: remove this and getValue() for 2.0, and move code to _setValueAttr()
1489 // try again after the editor is finished loading
1490 this.onLoadDeferred
.addCallback(lang
.hitch(this, function(){
1491 this.setValue(html
);
1495 this._cursorToStart
= true;
1496 if(this.textarea
&& (this.isClosed
|| !this.isLoaded
)){
1497 this.textarea
.value
=html
;
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> </p>"; //
1505 // Use to avoid webkit problems where editor is disabled until the user clicks it
1506 if(!html
&& has("webkit")){
1507 html
= " "; //
1509 node
.innerHTML
= html
;
1510 this._preDomFilterContent(node
);
1513 this.onDisplayChanged();
1514 this._set("value", this.getValue(true));
1517 replaceValue: function(/*String*/ html
){
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
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");
1534 this._cursorToStart
= true;
1535 html
= " "; //
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
);
1547 this._set("value", this.getValue(true));
1550 _preFilterContent: function(/*String*/ html
){
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.
1560 array
.forEach(this.contentPreFilters
, function(ef
){ if(ef
){ ec
= ef(ec
); } });
1563 _preDomFilterContent: function(/*DomNode*/ dom
){
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.
1570 dom
= dom
|| this.editNode
;
1571 array
.forEach(this.contentDomPreFilters
, function(ef
){
1572 if(ef
&& lang
.isFunction(ef
)){
1578 _postFilterContent: function(
1579 /*DomNode|DomNode[]|String?*/ dom
,
1580 /*Boolean?*/ nonDestructive
){
1582 // filter the output after getting the content of the editing area
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.
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.
1608 // a node, set of nodes, which to filter using each of the current
1609 // members of the contentDomPostFilters and contentPostFilters arrays.
1612 // defaults to "false". If true, ensures that filtering happens on
1613 // a clone of the passed-in content and not the actual node
1620 if(!lang
.isString(dom
)){
1621 dom
= dom
|| this.editNode
;
1622 if(this.contentDomPostFilters
.length
){
1624 dom
= lang
.clone(dom
);
1626 array
.forEach(this.contentDomPostFilters
, function(ef
){
1630 ec
= htmlapi
.getChildrenHtml(dom
);
1635 if(!lang
.trim(ec
.replace(/^\xA0\xA0*/, '').replace(/\xA0\xA0*$/, '')).length
){
1640 // //removing appended <P> </P> for IE
1641 // ec = ec.replace(/(?:<p> </p>[\n\r]*)+$/i,"");
1643 array
.forEach(this.contentPostFilters
, function(ef
){
1650 _saveContent: function(){
1652 // Saves the content in an onunload event if the editor has not been closed
1656 var saveTextarea
= dom
.byId(dijit
._scopeName
+ "._editor.RichText.value");
1658 if(saveTextarea
.value
){
1659 saveTextarea
.value
+= this._SEPARATOR
;
1661 saveTextarea
.value
+= this.name
+ this._NAME_CONTENT_SEP
+ this.getValue(true);
1666 escapeXml: function(/*String*/ str
, /*Boolean*/ noSingleQuotes
){
1668 // Adds escape sequences for special characters in XML.
1669 // Optionally skips escapes for single quotes
1673 str
= str
.replace(/&/gm, "&").replace(/</gm, "<").replace(/>/gm, ">").replace(/"/gm, ""
;");
1674 if(!noSingleQuotes){
1675 str = str.replace(/'/gm, "'");
1677 return str; // string
1680 getNodeHtml: function(/* DomNode */ node){
1682 // Deprecated. Use dijit/_editor/html::_getNodeHtml() instead.
1685 kernel.deprecated('dijit.Editor::getNodeHtml is deprecated','use dijit/_editor/html::getNodeHtml instead', 2);
1686 return htmlapi.getNodeHtml(node); // String
1689 getNodeChildrenHtml: function(/* DomNode */ dom){
1691 // Deprecated. Use dijit/_editor/html::getChildrenHtml() instead.
1694 kernel.deprecated('dijit.Editor::getNodeChildrenHtml is deprecated','use dijit/_editor/html::getChildrenHtml instead', 2);
1695 return htmlapi.getChildrenHtml(dom);
1698 close: function(/*Boolean?*/ save){
1700 // Kills the editor and optionally writes back the modified contents to the
1701 // element from which it originated.
1703 // Whether or not to save the changes. If false, the changes are discarded.
1707 if(this.isClosed){ return; }
1709 if(!arguments.length){ save = true; }
1711 this._set("value
", this.getValue(true));
1714 // line height is squashed for iframes
1715 // FIXME: why was this here? if(this.iframe){ this.domNode.style.lineHeight = null; }
1717 if(this.interval){ clearInterval(this.interval); }
1719 if(this._webkitListener){
1720 //Cleaup of WebKit fix: #9532
1721 this.disconnect(this._webkitListener);
1722 delete this._webkitListener;
1725 // Guard against memory leaks on IE (see #9268)
1727 this.iframe.onfocus = null;
1729 this.iframe._loadFunc = null;
1731 if(this._iframeRegHandle){
1732 this._iframeRegHandle.remove();
1733 delete this._iframeRegHandle;
1737 var s = this.textarea.style;
1739 s.left = s.top = "";
1741 s.overflow = this.__overflow;
1742 this.__overflow = null;
1744 this.textarea.value = this.value;
1745 domConstruct.destroy(this.domNode);
1746 this.domNode = this.textarea;
1748 // Note that this destroys the iframe
1749 this.domNode.innerHTML = this.value;
1753 domClass.remove(this.domNode, this.baseClass);
1754 this.isClosed = true;
1755 this.isLoaded = false;
1757 delete this.editNode;
1758 delete this.focusNode;
1760 if(this.window && this.window._frameElement){
1761 this.window._frameElement = null;
1765 this.document = null;
1766 this.editingArea = null;
1767 this.editorObject = null;
1770 destroy: function(){
1771 if(!this.isClosed){ this.close(false); }
1772 if(this._updateTimer){
1773 clearTimeout(this._updateTimer);
1775 this.inherited(arguments);
1776 if(RichText._globalSaveHandler){
1777 delete RichText._globalSaveHandler[this.id];
1781 _removeMozBogus: function(/* String */ html){
1783 // Post filter to remove unwanted HTML attributes generated by mozilla
1786 return html.replace(/\stype="_moz
"/gi, '').replace(/\s_moz_dirty=""/gi, '').replace(/_moz_resizing="(true|false)"/gi,''); // String
1788 _removeWebkitBogus: function(/* String */ html){
1790 // Post filter to remove unwanted HTML attributes generated by webkit
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
1800 _normalizeFontStyle: function(/* String */ html
){
1802 // Convert 'strong' and 'em' to 'b' and 'i'.
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'.
1807 // Note the IE generates 'strong' and 'em' rather than 'b' and 'i'
1810 return html
.replace(/<(\/)?strong([ \>])/gi, '<$1b$2')
1811 .replace(/<(\/)?em([ \>])/gi, '<$1i$2' ); // String
1814 _preFixUrlAttributes: function(/* String */ html
){
1816 // Pre-filter to do fixing to href attributes on <a> and <img> tags
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
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 ******************************************************************************/
1831 /*** queryCommandEnabled implementations ***/
1833 _browserQueryCommandEnabled: function(command
){
1835 // Implementation to call to the native queryCommandEnabled of the browser.
1837 // The command to check.
1840 if(!command
) { return false; }
1841 var elem
= has("ie") ? this.document
.selection
.createRange() : this.document
;
1843 return elem
.queryCommandEnabled(command
);
1849 _createlinkEnabledImpl: function(/*===== argument =====*/){
1851 // This function implements the test for if the create link
1852 // command should be enabled or not.
1854 // arguments to the exec command, if any.
1859 var sel
= this.window
.getSelection();
1860 if(sel
.isCollapsed
){
1863 enabled
= this.document
.queryCommandEnabled("createlink");
1866 enabled
= this._browserQueryCommandEnabled("createlink");
1871 _unlinkEnabledImpl: function(/*===== argument =====*/){
1873 // This function implements the test for if the unlink
1874 // command should be enabled or not.
1876 // arguments to the exec command, if any.
1880 if(has("mozilla") || has("webkit")){
1881 enabled
= this._sCall("hasAncestorElement", ["a"]);
1883 enabled
= this._browserQueryCommandEnabled("unlink");
1888 _inserttableEnabledImpl: function(/*===== argument =====*/){
1890 // This function implements the test for if the inserttable
1891 // command should be enabled or not.
1893 // arguments to the exec command, if any.
1897 if(has("mozilla") || has("webkit")){
1900 enabled
= this._browserQueryCommandEnabled("inserttable");
1905 _cutEnabledImpl: function(/*===== argument =====*/){
1907 // This function implements the test for if the cut
1908 // command should be enabled or not.
1910 // arguments to the exec command, if any.
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(); }
1920 enabled
= this._browserQueryCommandEnabled("cut");
1925 _copyEnabledImpl: function(/*===== argument =====*/){
1927 // This function implements the test for if the copy
1928 // command should be enabled or not.
1930 // arguments to the exec command, if any.
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(); }
1940 enabled
= this._browserQueryCommandEnabled("copy");
1945 _pasteEnabledImpl: function(/*===== argument =====*/){
1947 // This function implements the test for if the paste
1948 // command should be enabled or not.
1950 // arguments to the exec command, if any.
1957 enabled
= this._browserQueryCommandEnabled("paste");
1962 /*** execCommand implementations ***/
1964 _inserthorizontalruleImpl: function(argument
){
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
1970 // arguments to the exec command, if any.
1974 return this._inserthtmlImpl("<hr>");
1976 return this.document
.execCommand("inserthorizontalrule", false, argument
);
1979 _unlinkImpl: function(argument
){
1981 // This function implements the unlink of an 'a' tag.
1983 // arguments to the exec command, if any.
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);
1991 return this.document
.execCommand("unlink", false, argument
);
1994 _hilitecolorImpl: function(argument
){
1996 // This function implements the hilitecolor command
1998 // arguments to the exec command, if any.
2002 var isApplied
= this._handleTextColorOrProperties("hilitecolor", argument
);
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);
2012 returnValue
= this.document
.execCommand("hilitecolor", false, argument
);
2018 _backcolorImpl: function(argument
){
2020 // This function implements the backcolor command
2022 // arguments to the exec command, if any.
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;
2031 var isApplied
= this._handleTextColorOrProperties("backcolor", argument
);
2033 isApplied
= this.document
.execCommand("backcolor", false, argument
);
2038 _forecolorImpl: function(argument
){
2040 // This function implements the forecolor command
2042 // arguments to the exec command, if any.
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;
2051 var isApplied
= false;
2052 isApplied
= this._handleTextColorOrProperties("forecolor", argument
);
2054 isApplied
= this.document
.execCommand("forecolor", false, argument
);
2059 _inserthtmlImpl: function(argument
){
2061 // This function implements the insertion of HTML content into
2062 // a point on the page.
2064 // The content to insert, if any.
2067 argument
= this._preFilterContent(argument
);
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));
2076 n
.outerHTML
=argument
;
2078 insertRange
.pasteHTML(argument
);
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
2087 rv
= this.document
.execCommand("inserthtml", false, argument
);
2092 _boldImpl: function(argument
){
2094 // This function implements an over-ride of the bold command.
2096 // Not used, operates by selection.
2099 var applied
= false;
2101 this._adaptIESelection();
2102 applied
= this._adaptIEFormatAreaAndExec("bold");
2105 applied
= this.document
.execCommand("bold", false, argument
);
2110 _italicImpl: function(argument
){
2112 // This function implements an over-ride of the italic command.
2114 // Not used, operates by selection.
2117 var applied
= false;
2119 this._adaptIESelection();
2120 applied
= this._adaptIEFormatAreaAndExec("italic");
2123 applied
= this.document
.execCommand("italic", false, argument
);
2128 _underlineImpl: function(argument
){
2130 // This function implements an over-ride of the underline command.
2132 // Not used, operates by selection.
2135 var applied
= false;
2137 this._adaptIESelection();
2138 applied
= this._adaptIEFormatAreaAndExec("underline");
2141 applied
= this.document
.execCommand("underline", false, argument
);
2146 _strikethroughImpl: function(argument
){
2148 // This function implements an over-ride of the strikethrough command.
2150 // Not used, operates by selection.
2153 var applied
= false;
2155 this._adaptIESelection();
2156 applied
= this._adaptIEFormatAreaAndExec("strikethrough");
2159 applied
= this.document
.execCommand("strikethrough", false, argument
);
2164 _superscriptImpl: function(argument
){
2166 // This function implements an over-ride of the superscript command.
2168 // Not used, operates by selection.
2171 var applied
= false;
2173 this._adaptIESelection();
2174 applied
= this._adaptIEFormatAreaAndExec("superscript");
2177 applied
= this.document
.execCommand("superscript", false, argument
);
2182 _subscriptImpl: function(argument
){
2184 // This function implements an over-ride of the superscript command.
2186 // Not used, operates by selection.
2189 var applied
= false;
2191 this._adaptIESelection();
2192 applied
= this._adaptIEFormatAreaAndExec("subscript");
2196 applied
= this.document
.execCommand("subscript", false, argument
);
2201 _fontnameImpl: function(argument
){
2203 // This function implements the fontname command
2205 // arguments to the exec command, if any.
2210 isApplied
= this._handleTextColorOrProperties("fontname", argument
);
2213 isApplied
= this.document
.execCommand("fontname", false, argument
);
2218 _fontsizeImpl: function(argument
){
2220 // This function implements the fontsize command
2222 // arguments to the exec command, if any.
2227 isApplied
= this._handleTextColorOrProperties("fontsize", argument
);
2230 isApplied
= this.document
.execCommand("fontsize", false, argument
);
2235 _insertorderedlistImpl: function(argument
){
2237 // This function implements the insertorderedlist command
2239 // arguments to the exec command, if any.
2242 var applied
= false;
2244 applied
= this._adaptIEList("insertorderedlist", argument
);
2247 applied
= this.document
.execCommand("insertorderedlist", false, argument
);
2252 _insertunorderedlistImpl: function(argument
){
2254 // This function implements the insertunorderedlist command
2256 // arguments to the exec command, if any.
2259 var applied
= false;
2261 applied
= this._adaptIEList("insertunorderedlist", argument
);
2264 applied
= this.document
.execCommand("insertunorderedlist", false, argument
);
2269 getHeaderHeight: function(){
2271 // A function for obtaining the height of the header node
2272 return this._getNodeChildrenHeight(this.header
); // Number
2275 getFooterHeight: function(){
2277 // A function for obtaining the height of the footer node
2278 return this._getNodeChildrenHeight(this.footer
); // Number
2281 _getNodeChildrenHeight: function(node
){
2283 // An internal function for computing the cumulative height of all child nodes of 'node'
2285 // The node to process the children of;
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.
2291 for(i
= 0; i
< node
.childNodes
.length
; i
++){
2292 var size
= domGeometry
.position(node
.childNodes
[i
]);
2299 _isNodeEmpty: function(node
, startOffset
){
2301 // Function to test if a node is devoid of real content.
2303 // The node to check.
2306 if(node
.nodeType
=== 1/*element*/){
2307 if(node
.childNodes
.length
> 0){
2308 return this._isNodeEmpty(node
.childNodes
[0], startOffset
);
2311 }else if(node
.nodeType
=== 3/*text*/){
2312 return (node
.nodeValue
.substring(startOffset
) === "");
2317 _removeStartingRangeFromRange: function(node
, range
){
2319 // Function to adjust selection range by removing the current
2322 // The node to remove from the starting range.
2324 // The range to adapt.
2327 if(node
.nextSibling
){
2328 range
.setStart(node
.nextSibling
,0);
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
;
2336 range
.setStart(parent
.nextSibling
,0);
2342 _adaptIESelection: function(){
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
;
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
;
2363 //Remove the starting ranges until the range does not start with an empty node.
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
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
);
2376 _adaptIEFormatAreaAndExec: function(command
){
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.
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
);
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
);
2401 offset
= ret
.offset
;
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){
2416 // Okay, we have a stopping place, time to split things apart.
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
);
2430 endOffset
= ret
.offset
;
2434 startNode
= doc
.createTextNode(txt
.substring(0, endOffset
));
2435 var endText
= txt
.substring(endOffset
, txt
.length
);
2437 endNode
= doc
.createTextNode(endText
);
2439 // Place the split, then remove original nodes.
2440 domConstruct
.place(startNode
, rs
, "before");
2442 breaker
= doc
.createElement("span");
2443 breaker
.className
= "ieFormatBreakerSpan";
2444 domConstruct
.place(breaker
, rs
, "after");
2445 domConstruct
.place(endNode
, breaker
, "after");
2448 domConstruct
.destroy(rs
);
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
;
2456 while(parentC
!== topNode
){
2457 var tg
= parentC
.tagName
;
2458 tagData
= {tagName
: tg
};
2459 tagList
.push(tagData
);
2461 var newTg
= doc
.createElement(tg
);
2462 // Clone over any 'style' data.
2465 if(parentC
.style
.cssText
){
2466 newTg
.style
.cssText
= parentC
.style
.cssText
;
2467 tagData
.cssText
= parentC
.style
.cssText
;
2471 // If font also need to clone over any font data.
2472 if(parentC
.tagName
=== "FONT"){
2474 newTg
.color
= parentC
.color
;
2475 tagData
.color
= parentC
.color
;
2478 newTg
.face
= parentC
.face
;
2479 tagData
.face
= parentC
.face
;
2481 if(parentC
.size
){ // this check was necessary on IE
2482 newTg
.size
= parentC
.size
;
2483 tagData
.size
= parentC
.size
;
2486 if(parentC
.className
){
2487 newTg
.className
= parentC
.className
;
2488 tagData
.className
= parentC
.className
;
2491 // Now move end node and every sibling
2492 // after it over into the new tag.
2494 nodeToMove
= endNode
;
2496 tNode
= nodeToMove
.nextSibling
;
2497 newTg
.appendChild(nodeToMove
);
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");
2507 domConstruct
.place(newTg
, parentC
, "after");
2509 startNode
= parentC
;
2511 parentC
= parentC
.parentNode
;
2514 // Lastly, move the split out all the split tags
2515 // to the new block as they should now be split properly.
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
= "";
2524 tNode
= nodeToMove
.nextSibling
;
2525 newblock
.appendChild(nodeToMove
);
2530 // We had intermediate tags, we have to now recreate them inbetween the split
2531 // and restore what styles, classnames, etc, we can.
2533 tagData
= tagList
.pop();
2534 var newContTag
= doc
.createElement(tagData
.tagName
);
2535 if(tagData
.cssText
&& newContTag
.style
){
2536 newContTag
.style
.cssText
= tagData
.cssText
;
2538 if(tagData
.className
){
2539 newContTag
.className
= tagData
.className
;
2541 if(tagData
.tagName
=== "FONT"){
2543 newContTag
.color
= tagData
.color
;
2546 newContTag
.face
= tagData
.face
;
2549 newContTag
.size
= tagData
.size
;
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
;
2559 if(tagData
.className
){
2560 newTgNode
.className
= tagData
.className
;
2562 if(tagData
.tagName
=== "FONT"){
2564 newTgNode
.color
= tagData
.color
;
2567 newTgNode
.face
= tagData
.face
;
2570 newTgNode
.size
= tagData
.size
;
2573 newContTag
.appendChild(newTgNode
);
2574 newContTag
= newTgNode
;
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
= "";
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
= "";
2610 if(!newblock
.firstChild
){
2611 // Empty, we don't need it. Split was at end or similar
2613 domConstruct
.destroy(newblock
);
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
);
2630 offset
= ret
.offset
;
2633 startNode
= doc
.createTextNode(txt
.substring(0, offset
));
2634 var endText
= txt
.substring(offset
);
2636 endNode
= doc
.createTextNode(txt
.substring(offset
));
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");
2648 domConstruct
.place(breaker
, startNode
, "after");
2650 domConstruct
.place(endNode
, breaker
, "after");
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
= "";
2676 _adaptIEList: function(command
/*===== , argument =====*/){
2678 // This function handles normalizing the IE list behavior as
2679 // much as possible.
2681 // The list command to execute.
2683 // Any additional argument.
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
2706 if(command
=== "insertorderedlist"){
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.
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);
2733 _handleTextColorOrProperties: function(command
, argument
){
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
2742 // Any additional arguments.
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
);
2761 offset
= ret
.offset
;
2764 startNode
= doc
.createTextNode(txt
.substring(0, offset
));
2765 var endText
= txt
.substring(offset
);
2767 endNode
= doc
.createTextNode(txt
.substring(offset
));
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");
2783 domConstruct
.place(breaker
, startNode
, "after");
2785 domConstruct
.place(endNode
, breaker
, "after");
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
);
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";
2801 domStyle
.set(breaker
, style
, argument
);
2802 selectionapi
.remove();
2803 domConstruct
.destroy(extraSpan
);
2804 breaker
.innerHTML
= " "; //
2805 selectionapi
.selectElement(breaker
);
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
);
2826 _adjustNodeAndOffset: function(/*DomNode*/node
, /*Int*/offset
){
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
2832 // The node to check.
2834 // The position to find within the text node
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
;
2842 return {"node": node
, "offset": offset
};
2845 _tagNamesForCommand: function(command
){
2847 // Function to return the tab names that are associated
2848 // with a particular style.
2850 // The command to return tags for.
2853 if(command
=== "bold"){
2854 return ["b", "strong"];
2855 }else if(command
=== "italic"){
2857 }else if(command
=== "strikethrough"){
2858 return ["s", "strike"];
2859 }else if(command
=== "superscript"){
2861 }else if(command
=== "subscript"){
2863 }else if(command
=== "underline"){
2869 _stripBreakerNodes: function(node
){
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
);
2877 for(i
= 0; i
< breakers
.length
; i
++){
2878 var b
= breakers
[i
];
2879 while(b
.firstChild
){
2880 domConstruct
.place(b
.firstChild
, b
, "before");
2882 domConstruct
.destroy(b
);