1 if(!dojo._hasResource["dijit._editor.RichText"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
2 dojo._hasResource["dijit._editor.RichText"] = true;
3 dojo.provide("dijit._editor.RichText");
5 dojo.require("dijit._Widget");
6 dojo.require("dijit._editor.selection");
7 dojo.require("dijit._editor.html");
8 dojo.require("dojo.i18n");
9 dojo.requireLocalization("dijit.form", "Textarea", null, "zh,pt,da,tr,ru,de,ROOT,sv,ja,he,fi,nb,el,ar,pt-pt,cs,fr,es,ko,nl,zh-tw,pl,it,hu");
11 // used to restore content when user leaves this page then comes back
12 // but do not try doing dojo.doc.write if we are using xd loading.
13 // dojo.doc.write will only work if RichText.js is included in the dojo.js
14 // file. If it is included in dojo.js and you want to allow rich text saving
15 // for back/forward actions, then set dojo.config.allowXdRichTextSave = true.
16 if(!dojo.config["useXDomain"] || dojo.config["allowXdRichTextSave"]){
19 var savetextarea = dojo.doc.createElement('textarea');
20 savetextarea.id = dijit._scopeName + "._editor.RichText.savedContent";
21 var s = savetextarea.style;
23 s.position='absolute';
28 dojo.body().appendChild(savetextarea);
31 //dojo.body() is not available before onLoad is fired
33 dojo.doc.write('<textarea id="' + dijit._scopeName + '._editor.RichText.savedContent" ' +
34 'style="display:none;position:absolute;top:-100px;left:-100px;height:3px;width:3px;overflow:hidden;"></textarea>');
38 dojo.declare("dijit._editor.RichText", dijit._Widget, {
39 constructor: function(){
41 // dijit._editor.RichText is the core of the WYSIWYG editor in dojo, which
42 // provides the basic editing features. It also encapsulates the differences
43 // of different js engines for various browsers
45 // contentPreFilters: Array
46 // pre content filter function register array.
47 // these filters will be executed before the actual
48 // editing area get the html content
49 this.contentPreFilters = [];
51 // contentPostFilters: Array
52 // post content filter function register array.
53 // these will be used on the resulting html
54 // from contentDomPostFilters. The resuling
55 // content is the final html (returned by getValue())
56 this.contentPostFilters = [];
58 // contentDomPreFilters: Array
59 // pre content dom filter function register array.
60 // these filters are applied after the result from
61 // contentPreFilters are set to the editing area
62 this.contentDomPreFilters = [];
64 // contentDomPostFilters: Array
65 // post content dom filter function register array.
66 // these filters are executed on the editing area dom
67 // the result from these will be passed to contentPostFilters
68 this.contentDomPostFilters = [];
70 // editingAreaStyleSheets: Array
71 // array to store all the stylesheets applied to the editing area
72 this.editingAreaStyleSheets=[];
74 this._keyHandlers = {};
75 this.contentPreFilters.push(dojo.hitch(this, "_preFixUrlAttributes"));
77 this.contentPreFilters.push(this._fixContentForMoz);
78 this.contentPostFilters.push(this._removeMozBogus);
79 }else if(dojo.isSafari){
80 this.contentPostFilters.push(this._removeSafariBogus);
82 //this.contentDomPostFilters.push(this._postDomFixUrlAttributes);
84 this.onLoadDeferred = new dojo.Deferred();
87 // inheritWidth: Boolean
88 // whether to inherit the parent's width or simply use 100%
91 // focusOnLoad: Boolean
92 // whether focusing into this instance of richtext when page onload
96 // If a save name is specified the content is saved and restored when the user
97 // leave this page can come back, or if the editor is not properly closed after
98 // editing has started.
101 // styleSheets: String
102 // semicolon (";") separated list of css files for the editing area
106 // temporary content storage
110 // set height to fix the editor at a specific height, with scrolling.
111 // By default, this is 300px. If you want to have the editor always
112 // resizes to accommodate the content, use AlwaysShowToolbar plugin
117 // The minimum height that the editor should have
126 // _SEPARATOR: String
127 // used to concat contents from multiple textareas into a single string
128 _SEPARATOR: "@@**%%__RICHTEXTBOUNDRY__%%**@@",
130 // onLoadDeferred: dojo.Deferred
131 // deferred which is fired when the editor finishes loading
132 onLoadDeferred: null,
134 postCreate: function(){
136 dojo.publish(dijit._scopeName + "._editor.RichText::init", [this]);
138 this.setupDefaultShortcuts();
141 setupDefaultShortcuts: function(){
142 // summary: add some default key handlers
144 // Overwrite this to setup your own handlers. The default
145 // implementation does not use Editor commands, but directly
146 // executes the builtin commands within the underlying browser
148 var exec = function(cmd, arg){
149 return arguments.length == 1 ? function(){ this.execCommand(cmd); } :
150 function(){ this.execCommand(cmd, arg); };
153 var ctrlKeyHandlers = { b: exec("bold"),
155 u: exec("underline"),
156 a: exec("selectall"),
157 s: function(){ this.save(true); },
159 "1": exec("formatblock", "h1"),
160 "2": exec("formatblock", "h2"),
161 "3": exec("formatblock", "h3"),
162 "4": exec("formatblock", "h4"),
164 "\\": exec("insertunorderedlist") };
167 ctrlKeyHandlers.Z = exec("redo"); //FIXME: undo?
170 for(var key in ctrlKeyHandlers){
171 this.addKeyHandler(key, this.KEY_CTRL, ctrlKeyHandlers[key]);
176 // events which should be connected to the underlying editing area
177 events: ["onKeyPress", "onKeyDown", "onKeyUp", "onClick"],
180 // events which should be connected to the underlying editing
181 // area, events in this array will be addListener with
185 _editorCommandsLocalized: false,
186 _localizeEditorCommands: function(){
187 if(this._editorCommandsLocalized){
190 this._editorCommandsLocalized = true;
192 //in IE, names for blockformat is locale dependent, so we cache the values here
194 //if the normal way fails, we try the hard way to get the list
196 //do not use _cacheLocalBlockFormatNames here, as it will
197 //trigger security warning in IE7
199 //in the array below, ul can not come directly after ol,
200 //otherwise the queryCommandValue returns Normal for it
201 var formats = ['p', 'pre', 'address', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'div', 'ul'];
202 var localhtml = "", format, i=0;
203 while((format=formats[i++])){
204 if(format.charAt(1) != 'l'){
205 localhtml += "<"+format+"><span>content</span></"+format+">";
207 localhtml += "<"+format+"><li>content</li></"+format+">";
210 //queryCommandValue returns empty if we hide editNode, so move it out of screen temporary
211 var div=dojo.doc.createElement('div');
212 div.style.position = "absolute";
213 div.style.left = "-2000px";
214 div.style.top = "-2000px";
215 dojo.doc.body.appendChild(div);
216 div.innerHTML = localhtml;
217 var node = div.firstChild;
219 dijit._editor.selection.selectElement(node.firstChild);
220 dojo.withGlobal(this.window, "selectElement", dijit._editor.selection, [node.firstChild]);
221 var nativename = node.tagName.toLowerCase();
222 this._local2NativeFormatNames[nativename] = dojo.doc.queryCommandValue("formatblock");//this.queryCommandValue("formatblock");
223 this._native2LocalFormatNames[this._local2NativeFormatNames[nativename]] = nativename;
224 node = node.nextSibling;
226 dojo.doc.body.removeChild(div);
229 open: function(/*DomNode?*/element){
231 // Transforms the node referenced in this.domNode into a rich text editing
232 // node. This will result in the creation and replacement with an <iframe>
233 // if designMode(FF)/contentEditable(IE) is used.
235 if((!this.onLoadDeferred)||(this.onLoadDeferred.fired >= 0)){
236 this.onLoadDeferred = new dojo.Deferred();
239 if(!this.isClosed){ this.close(); }
240 dojo.publish(dijit._scopeName + "._editor.RichText::open", [ this ]);
243 if((arguments.length == 1)&&(element["nodeName"])){ this.domNode = element; } // else unchanged
246 if( (this.domNode["nodeName"])&&
247 (this.domNode.nodeName.toLowerCase() == "textarea")){
248 // if we were created from a textarea, then we need to create a
249 // new editing harness node.
250 this.textarea = this.domNode;
251 this.name=this.textarea.name;
252 html = this._preFilterContent(this.textarea.value);
253 this.domNode = dojo.doc.createElement("div");
254 this.domNode.setAttribute('widgetId',this.id);
255 this.textarea.removeAttribute('widgetId');
256 this.domNode.cssText = this.textarea.cssText;
257 this.domNode.className += " "+this.textarea.className;
258 dojo.place(this.domNode, this.textarea, "before");
259 var tmpFunc = dojo.hitch(this, function(){
260 //some browsers refuse to submit display=none textarea, so
261 //move the textarea out of screen instead
262 dojo.attr(this.textarea, 'tabIndex', '-1');
263 with(this.textarea.style){
265 position = "absolute";
266 left = top = "-1000px";
268 if(dojo.isIE){ //nasty IE bug: abnormal formatting if overflow is not hidden
269 this.__overflow = overflow;
275 setTimeout(tmpFunc, 10);
280 // this.domNode.innerHTML = html;
282 // if(this.textarea.form){
283 // // FIXME: port: this used to be before advice!!!
284 // dojo.connect(this.textarea.form, "onsubmit", this, function(){
285 // // FIXME: should we be calling close() here instead?
286 // this.textarea.value = this.getValue();
290 html = this._preFilterContent(dijit._editor.getChildrenHtml(this.domNode));
291 this.domNode.innerHTML = '';
293 if(html == ""){ html = " "; }
295 var content = dojo.contentBox(this.domNode);
296 // var content = dojo.contentBox(this.srcNodeRef);
297 this._oldHeight = content.h;
298 this._oldWidth = content.w;
300 // If we're a list item we have to put in a blank line to force the
301 // bullet to nicely align at the top of text
302 if( (this.domNode["nodeName"]) &&
303 (this.domNode.nodeName == "LI") ){
304 this.domNode.innerHTML = " <br>";
307 this.editingArea = dojo.doc.createElement("div");
308 this.domNode.appendChild(this.editingArea);
310 if(this.name != "" && (!dojo.config["useXDomain"] || dojo.config["allowXdRichTextSave"])){
311 var saveTextarea = dojo.byId(dijit._scopeName + "._editor.RichText.savedContent");
312 if(saveTextarea.value != ""){
313 var datas = saveTextarea.value.split(this._SEPARATOR), i=0, dat;
314 while((dat=datas[i++])){
315 var data = dat.split(":");
316 if(data[0] == this.name){
324 // FIXME: need to do something different for Opera/Safari
325 this.connect(window, "onbeforeunload", "_saveContent");
326 // dojo.connect(window, "onunload", this, "_saveContent");
329 this.isClosed = false;
330 // Safari's selections go all out of whack if we do it inline,
331 // so for now IE is our only hero
332 //if(typeof dojo.doc.body.contentEditable != "undefined"){
333 if(dojo.isIE || dojo.isSafari || dojo.isOpera){ // contentEditable, easy
335 if(dojo.config["useXDomain"] && !dojo.config["dojoBlankHtmlUrl"]){
336 console.debug("dijit._editor.RichText: When using cross-domain Dojo builds,"
337 + " please save dojo/resources/blank.html to your domain and set djConfig.dojoBlankHtmlUrl"
338 + " to the path on your domain to blank.html");
341 var burl = dojo.config["dojoBlankHtmlUrl"] || (dojo.moduleUrl("dojo", "resources/blank.html")+"");
342 var ifr = this.editorObject = this.iframe = dojo.doc.createElement('iframe');
343 ifr.id = this.id+"_iframe";
345 ifr.style.border = "none";
346 ifr.style.width = "100%";
348 // ifr.style.scrolling = this.height ? "auto" : "vertical";
349 this.editingArea.appendChild(ifr);
350 var h = null; // set later in non-ie6 branch
351 var loadFunc = dojo.hitch( this, function(){
352 if(h){ dojo.disconnect(h); h = null; }
353 this.window = ifr.contentWindow;
354 var d = this.document = this.window.document;
356 d.write(this._getIframeDocTxt(html));
361 ifr.style.height = this.height;
364 ifr.style.minHeight = this.minHeight;
367 ifr.style.height = this.height ? this.height : this.minHeight;
371 this._localizeEditorCommands();
375 this.savedContent = this.getValue(true);
377 if(dojo.isIE && dojo.isIE < 7){ // IE 6 is a steaming pile...
378 var t = setInterval(function(){
379 if(ifr.contentWindow.isLoaded){
384 }else{ // blissful sanity!
386 ((dojo.isIE) ? ifr.contentWindow : ifr), "onload", loadFunc
389 }else{ // designMode in iframe
390 this._drawIframe(html);
391 this.savedContent = this.getValue(true);
394 // TODO: this is a guess at the default line-height, kinda works
395 if(this.domNode.nodeName == "LI"){ this.domNode.lastChild.style.marginTop = "-1.2em"; }
396 this.domNode.className += " RichTextEditable";
399 //static cache variables shared among all instance of this class
400 _local2NativeFormatNames: {},
401 _native2LocalFormatNames: {},
402 _localizedIframeTitles: null,
404 _getIframeDocTxt: function(/* String */ html){
405 var _cs = dojo.getComputedStyle(this.domNode);
406 if(dojo.isIE || (!this.height && !dojo.isMoz)){
407 html="<div>"+html+"</div>";
409 var font = [ _cs.fontWeight, _cs.fontSize, _cs.fontFamily ].join(" ");
411 // line height is tricky - applying a units value will mess things up.
412 // if we can't get a non-units value, bail out.
413 var lineHeight = _cs.lineHeight;
414 if(lineHeight.indexOf("px") >= 0){
415 lineHeight = parseFloat(lineHeight)/parseFloat(_cs.fontSize);
416 // console.debug(lineHeight);
417 }else if(lineHeight.indexOf("em")>=0){
418 lineHeight = parseFloat(lineHeight);
423 this.isLeftToRight() ? "<html><head>" : "<html dir='rtl'><head>",
424 (dojo.isMoz ? "<title>" + this._localizedIframeTitles.iframeEditTitle + "</title>" : ""),
427 " background:transparent;",
429 " padding: 1em 0 0 0;",
430 " margin: -1em 0 0 0;", // remove extraneous vertical scrollbar on safari and firefox
433 // TODO: left positioning will cause contents to disappear out of view
434 // if it gets too wide for the visible area
436 " top:0px; left:0px; right:0px;",
437 ((this.height||dojo.isOpera) ? "" : "position: fixed;"),
438 // FIXME: IE 6 won't understand min-height?
439 " min-height:", this.minHeight, ";",
440 " line-height:", lineHeight,
442 "p{ margin: 1em 0 !important; }",
443 (this.height ? // height:auto undoes the height:100%
444 "" : "body,html{height:auto;overflow-y:hidden;/*for IE*/} body > div {overflow-x:auto;/*for FF to show vertical scrollbar*/}"
446 "li > ul:-moz-first-node, li > ol:-moz-first-node{ padding-top: 1.2em; } ",
447 "li{ min-height:1.2em; }",
449 this._applyEditingAreaStyleSheets(),
450 "</head><body>"+html+"</body></html>"
451 ].join(""); // String
454 _drawIframe: function(/*String*/html){
456 // Draws an iFrame using the existing one if one exists.
457 // Used by Mozilla, Safari, and Opera
460 var ifr = this.iframe = dojo.doc.createElement("iframe");
462 // this.iframe.src = "about:blank";
463 // dojo.doc.body.appendChild(this.iframe);
464 // console.debug(this.iframe.contentDocument.open());
465 // dojo.body().appendChild(this.iframe);
466 var ifrs = ifr.style;
467 // ifrs.border = "1px solid black";
468 ifrs.border = "none";
469 ifrs.lineHeight = "0"; // squash line height
470 ifrs.verticalAlign = "bottom";
471 // ifrs.scrolling = this.height ? "auto" : "vertical";
472 this.editorObject = this.iframe;
473 // get screen reader text for mozilla here, too
474 this._localizedIframeTitles = dojo.i18n.getLocalization("dijit.form", "Textarea");
475 // need to find any associated label element and update iframe document title
476 var label=dojo.query('label[for="'+this.id+'"]');
478 this._localizedIframeTitles.iframeEditTitle = label[0].innerHTML + " " + this._localizedIframeTitles.iframeEditTitle;
481 // opera likes this to be outside the with block
482 // this.iframe.src = "javascript:void(0)";//dojo.uri.dojoUri("src/widget/templates/richtextframe.html") + ((dojo.doc.domain != currentDomain) ? ("#"+dojo.doc.domain) : "");
483 this.iframe.style.width = this.inheritWidth ? this._oldWidth : "100%";
486 this.iframe.style.height = this.height;
488 this.iframe.height = this._oldHeight;
493 tmpContent = this.srcNodeRef;
495 tmpContent = dojo.doc.createElement('div');
496 tmpContent.style.display="none";
497 tmpContent.innerHTML = html;
498 //append tmpContent to under the current domNode so that the margin
499 //calculation below is correct
500 this.editingArea.appendChild(tmpContent);
503 this.editingArea.appendChild(this.iframe);
505 //do we want to show the content before the editing area finish loading here?
506 //if external style sheets are used for the editing area, the appearance now
507 //and after loading of the editing area won't be the same (and padding/margin
508 //calculation above may not be accurate)
509 // tmpContent.style.display = "none";
510 // this.editingArea.appendChild(this.iframe);
512 var _iframeInitialized = false;
513 // console.debug(this.iframe);
514 // var contentDoc = this.iframe.contentWindow.document;
517 // note that on Safari lower than 420+, we have to get the iframe
518 // by ID in order to get something w/ a contentDocument property
520 var contentDoc = this.iframe.contentDocument;
523 contentDoc.body.innerHTML = html;
525 contentDoc.write(this._getIframeDocTxt(html));
529 // now we wait for onload. Janky hack!
530 var ifrFunc = dojo.hitch(this, function(){
531 if(!_iframeInitialized){
532 _iframeInitialized = true;
536 if(this.iframe.contentWindow){
537 this.window = this.iframe.contentWindow;
538 this.document = this.iframe.contentWindow.document
539 }else if(this.iframe.contentDocument){
541 this.window = this.iframe.contentDocument.window;
542 this.document = this.iframe.contentDocument;
544 if(!this.document.body){
548 setTimeout(ifrFunc,500);
549 _iframeInitialized = false;
553 dojo._destroyElement(tmpContent);
556 dojo._destroyElement(tmpContent);
557 this.editNode.innerHTML = html;
558 this.onDisplayChanged();
560 this._preDomFilterContent(this.editNode);
566 _applyEditingAreaStyleSheets: function(){
568 // apply the specified css files in styleSheets
570 if(this.styleSheets){
571 files = this.styleSheets.split(';');
572 this.styleSheets = '';
575 //empty this.editingAreaStyleSheets here, as it will be filled in addStyleSheet
576 files = files.concat(this.editingAreaStyleSheets);
577 this.editingAreaStyleSheets = [];
579 var text='', i=0, url;
580 while((url=files[i++])){
581 var abstring = (new dojo._Url(dojo.global.location, url)).toString();
582 this.editingAreaStyleSheets.push(abstring);
583 text += '<link rel="stylesheet" type="text/css" href="'+abstring+'"/>'
588 addStyleSheet: function(/*dojo._Url*/uri){
590 // add an external stylesheet for the editing area
591 // uri: a dojo.uri.Uri pointing to the url of the external css file
592 var url=uri.toString();
594 //if uri is relative, then convert it to absolute so that it can be resolved correctly in iframe
595 if(url.charAt(0) == '.' || (url.charAt(0) != '/' && !uri.host)){
596 url = (new dojo._Url(dojo.global.location, url)).toString();
599 if(dojo.indexOf(this.editingAreaStyleSheets, url) > -1){
600 // console.debug("dijit._editor.RichText.addStyleSheet: Style sheet "+url+" is already applied");
604 this.editingAreaStyleSheets.push(url);
605 if(this.document.createStyleSheet){ //IE
606 this.document.createStyleSheet(url);
607 }else{ //other browser
608 var head = this.document.getElementsByTagName("head")[0];
609 var stylesheet = this.document.createElement("link");
615 head.appendChild(stylesheet);
619 removeStyleSheet: function(/*dojo._Url*/uri){
621 // remove an external stylesheet for the editing area
622 var url=uri.toString();
623 //if uri is relative, then convert it to absolute so that it can be resolved correctly in iframe
624 if(url.charAt(0) == '.' || (url.charAt(0) != '/' && !uri.host)){
625 url = (new dojo._Url(dojo.global.location, url)).toString();
627 var index = dojo.indexOf(this.editingAreaStyleSheets, url);
629 // console.debug("dijit._editor.RichText.removeStyleSheet: Style sheet "+url+" has not been applied");
632 delete this.editingAreaStyleSheets[index];
633 dojo.withGlobal(this.window,'query', dojo, ['link:[href="'+url+'"]']).orphan()
637 _mozSettingProps: ['styleWithCSS','insertBrOnReturn'],
638 setDisabled: function(/*Boolean*/ disabled){
639 if(dojo.isIE || dojo.isSafari || dojo.isOpera){
640 if(dojo.isIE){ this.editNode.unselectable = "on"; } // prevent IE from setting focus
641 this.editNode.contentEditable = !disabled;
644 setTimeout(function(){ _this.editNode.unselectable = "off"; }, 0);
648 //AP: why isn't this set in the constructor, or put in mozSettingProps as a hash?
649 this._mozSettings=[false,this.blockNodeForEnter==='BR'];
651 this.document.designMode=(disabled?'off':'on');
652 if(!disabled && this._mozSettings){
653 dojo.forEach(this._mozSettingProps, function(s,i){
654 this.document.execCommand(s,false,this._mozSettings[i]);
657 // this.document.execCommand('contentReadOnly', false, disabled);
659 // this.blur(); //to remove the blinking caret
662 this.disabled = disabled;
668 _isResized: function(){ return false; },
670 onLoad: function(/* Event */ e){
671 // summary: handler after the content of the document finishes loading
672 this.isLoaded = true;
673 if(!this.window.__registeredWindow){
674 this.window.__registeredWindow=true;
675 dijit.registerWin(this.window);
677 if(!dojo.isIE && (this.height || dojo.isMoz)){
678 this.editNode=this.document.body;
680 this.editNode=this.document.body.firstChild;
682 if(dojo.isIE){ // #4996 IE wants to focus the BODY tag
683 var tabStop = this.tabStop = dojo.doc.createElement('<div tabIndex=-1>');
684 this.editingArea.appendChild(tabStop);
685 this.iframe.onfocus = function(){ _this.editNode.setActive(); }
690 this.setDisabled(false);
692 // Firefox throws an exception if the editor is initially hidden
693 // so, if this fails, try again onClick by adding "once" advice
694 var handle = dojo.connect(this, "onClick", this, function(){
695 this.setDisabled(false);
696 dojo.disconnect(handle);
700 this._preDomFilterContent(this.editNode);
702 var events=this.events.concat(this.captureEvents),i=0,et;
703 while((et=events[i++])){
704 this.connect(this.document, et.toLowerCase(), et);
707 try{ // sanity check for Mozilla
708 //AP: what's the point of this?
709 // this.document.execCommand("useCSS", false, true); // old moz call
710 this.document.execCommand("styleWithCSS", false, false); // new moz call
711 //this.document.execCommand("insertBrOnReturn", false, false); // new moz call
713 // FIXME: when scrollbars appear/disappear this needs to be fired
714 }else{ // IE contentEditable
715 // give the node Layout on IE
716 this.connect(this.document, "onmousedown", "_onMouseDown"); // #4996 fix focus
717 this.editNode.style.zoom = 1.0;
720 if(this.focusOnLoad){
721 setTimeout(dojo.hitch(this, "focus"), 0); // have to wait for IE to set unselectable=off
724 this.onDisplayChanged(e);
725 if(this.onLoadDeferred){
726 this.onLoadDeferred.callback(true);
730 onKeyDown: function(/* Event */ e){
731 // summary: Fired on keydown
733 // we need this event at the moment to get the events from control keys
734 // such as the backspace. It might be possible to add this to Dojo, so that
735 // keyPress events can be emulated by the keyDown and keyUp detection.
737 if(e.keyCode == dojo.keys.TAB && e.shiftKey && !e.ctrlKey && !e.altKey){
738 // focus the BODY so the browser will tab away from it instead
740 }else if(e.keyCode == dojo.keys.TAB && !e.shiftKey && !e.ctrlKey && !e.altKey){
741 // focus the BODY so the browser will tab away from it instead
742 this.tabStop.focus();
743 }else if(e.keyCode === dojo.keys.BACKSPACE && this.document.selection.type === "Control"){
744 // IE has a bug where if a non-text object is selected in the editor,
745 // hitting backspace would act as if the browser's back button was
746 // clicked instead of deleting the object. see #1069
748 this.execCommand("delete");
749 }else if((65 <= e.keyCode&&e.keyCode <= 90) ||
750 (e.keyCode>=37&&e.keyCode<=40) // FIXME: get this from connect() instead!
752 e.charCode = e.keyCode;
755 }else if(dojo.isMoz){
756 if(e.keyCode == dojo.keys.TAB && !e.shiftKey && !e.ctrlKey && !e.altKey && this.iframe){
757 // update iframe document title for screen reader
758 this.iframe.contentDocument.title = this._localizedIframeTitles.iframeFocusTitle;
760 // Place focus on the iframe. A subsequent tab or shift tab will put focus
761 // on the correct control.
762 this.iframe.focus(); // this.focus(); won't work
764 }else if(e.keyCode == dojo.keys.TAB && e.shiftKey){
765 // if there is a toolbar, set focus to it, otherwise ignore
767 this.toolbar.focus();
774 onKeyUp: function(e){
775 // summary: Fired on keyup
782 onKeyPress: function(e){
783 // summary: Fired on keypress
785 // handle the various key events
786 var modifiers = (e.ctrlKey && !e.altKey) ? this.KEY_CTRL : 0 | e.shiftKey ? this.KEY_SHIFT : 0;
788 var key = e.keyChar || e.keyCode;
789 if(this._keyHandlers[key]){
790 // console.debug("char:", e.key);
791 var handlers = this._keyHandlers[key], i = 0, h;
792 while((h = handlers[i++])){
793 if(modifiers == h.modifiers){
794 if(!h.handler.apply(this,arguments)){
802 // function call after the character has been inserted
803 setTimeout(dojo.hitch(this, function(){
804 this.onKeyPressed(e);
808 addKeyHandler: function(/*String*/key, /*Int*/modifiers, /*Function*/handler){
809 // summary: add a handler for a keyboard shortcut
810 if(!dojo.isArray(this._keyHandlers[key])){ this._keyHandlers[key] = []; }
811 this._keyHandlers[key].push({
812 modifiers: modifiers || 0,
817 onKeyPressed: function(/*Event*/e){
818 this.onDisplayChanged(/*e*/); // can't pass in e
821 onClick: function(/*Event*/e){
822 // console.info('onClick',this._tryDesignModeOn);
823 this.onDisplayChanged(e);
826 _onMouseDown: function(/*Event*/e){ // IE only to prevent 2 clicks to focus
827 if(!this._focused && !this.disabled){
832 _onBlur: function(e){
833 this.inherited(arguments);
834 var _c=this.getValue(true);
835 if(_c!=this.savedContent){
837 this.savedContent=_c;
839 if(dojo.isMoz && this.iframe){
840 this.iframe.contentDocument.title = this._localizedIframeTitles.iframeEditTitle;
844 _onFocus: function(/*Event*/e){
845 // summary: Fired on focus
846 this.inherited(arguments);
847 if(dojo.isMoz && this._initialFocus){
848 this._initialFocus = false;
849 if(this.editNode.innerHTML.replace(/^\s+|\s+$/g, "") == " "){
850 this.placeCursorAtStart();
851 // this.execCommand("selectall");
852 // this.window.getSelection().collapseToStart();
857 // TODO: why is this needed - should we deprecate this ?
859 // summary: remove focus from this instance
860 if(!dojo.isIE && this.window.document.documentElement && this.window.document.documentElement.focus){
861 this.window.document.documentElement.focus();
862 }else if(dojo.doc.body.focus){
863 dojo.doc.body.focus();
868 // summary: move focus to this instance
870 dijit.focus(this.iframe);
871 }else if(this.editNode && this.editNode.focus){
872 // editNode may be hidden in display:none div, lets just punt in this case
873 //this.editNode.focus(); -> causes IE to scroll always (strict and quirks mode) to the top the Iframe
874 // if we fire the event manually and let the browser handle the focusing, the latest
875 // cursor position is focused like in FF
876 this.iframe.fireEvent('onfocus', document.createEventObject()); // createEventObject only in IE
878 // TODO: should we throw here?
879 // console.debug("Have no idea how to focus into the editor!");
886 onDisplayChanged: function(/*Event*/e){
888 // This event will be fired everytime the display context
889 // changes and the result needs to be reflected in the UI.
891 // If you don't want to have update too often,
892 // onNormalizedDisplayChanged should be used instead
894 // var _t=new Date();
895 if(!this._updateTimer){
896 // this._lastUpdate=_t;
897 if(this._updateTimer){
898 clearTimeout(this._updateTimer);
900 this._updateTimer=setTimeout(dojo.hitch(this,this.onNormalizedDisplayChanged),this.updateInterval);
903 onNormalizedDisplayChanged: function(){
905 // This event is fired every updateInterval ms or more
907 // If something needs to happen immidiately after a
908 // user change, please use onDisplayChanged instead
909 this._updateTimer=null;
911 onChange: function(newContent){
913 // this is fired if and only if the editor loses focus and
914 // the content is changed
916 // console.log('onChange',newContent);
918 _normalizeCommand: function(/*String*/cmd){
920 // Used as the advice function by dojo.connect to map our
921 // normalized set of commands to those supported by the target
924 var command = cmd.toLowerCase();
925 if(command == "hilitecolor" && !dojo.isMoz){
926 command = "backcolor";
932 queryCommandAvailable: function(/*String*/command){
934 // Tests whether a command is supported by the host. Clients SHOULD check
935 // whether a command is supported before attempting to use it, behaviour
936 // for unsupported commands is undefined.
937 // command: The command to test for
939 var mozilla = 1 << 1;
942 var safari420 = 1 << 4;
944 var gt420 = dojo.isSafari;
946 function isSupportedBy(browsers){
948 ie: Boolean(browsers & ie),
949 mozilla: Boolean(browsers & mozilla),
950 safari: Boolean(browsers & safari),
951 safari420: Boolean(browsers & safari420),
952 opera: Boolean(browsers & opera)
956 var supportedBy = null;
958 switch(command.toLowerCase()){
959 case "bold": case "italic": case "underline":
960 case "subscript": case "superscript":
961 case "fontname": case "fontsize":
962 case "forecolor": case "hilitecolor":
963 case "justifycenter": case "justifyfull": case "justifyleft":
964 case "justifyright": case "delete": case "selectall": case "toggledir":
965 supportedBy = isSupportedBy(mozilla | ie | safari | opera);
968 case "createlink": case "unlink": case "removeformat":
969 case "inserthorizontalrule": case "insertimage":
970 case "insertorderedlist": case "insertunorderedlist":
971 case "indent": case "outdent": case "formatblock":
972 case "inserthtml": case "undo": case "redo": case "strikethrough":
973 supportedBy = isSupportedBy(mozilla | ie | opera | safari420);
976 case "blockdirltr": case "blockdirrtl":
977 case "dirltr": case "dirrtl":
978 case "inlinedirltr": case "inlinedirrtl":
979 supportedBy = isSupportedBy(ie);
981 case "cut": case "copy": case "paste":
982 supportedBy = isSupportedBy( ie | mozilla | safari420);
986 supportedBy = isSupportedBy(mozilla | ie);
989 case "insertcell": case "insertcol": case "insertrow":
990 case "deletecells": case "deletecols": case "deleterows":
991 case "mergecells": case "splitcell":
992 supportedBy = isSupportedBy(ie | mozilla);
995 default: return false;
998 return (dojo.isIE && supportedBy.ie) ||
999 (dojo.isMoz && supportedBy.mozilla) ||
1000 (dojo.isSafari && supportedBy.safari) ||
1001 (gt420 && supportedBy.safari420) ||
1002 (dojo.isOpera && supportedBy.opera); // Boolean return true if the command is supported, false otherwise
1005 execCommand: function(/*String*/command, argument){
1006 // summary: Executes a command in the Rich Text area
1007 // command: The command to execute
1008 // argument: An optional argument to the command
1011 //focus() is required for IE to work
1012 //In addition, focus() makes sure after the execution of
1013 //the command, the editor receives the focus as expected
1016 command = this._normalizeCommand(command);
1017 if(argument != undefined){
1018 if(command == "heading"){
1019 throw new Error("unimplemented");
1020 }else if((command == "formatblock") && dojo.isIE){
1021 argument = '<'+argument+'>';
1024 if(command == "inserthtml"){
1025 argument=this._preFilterContent(argument);
1027 var insertRange = this.document.selection.createRange();
1028 if(this.document.selection.type.toUpperCase()=='CONTROL'){
1029 var n=insertRange.item(0);
1030 while(insertRange.length){
1031 insertRange.remove(insertRange.item(0));
1033 n.outerHTML=argument;
1035 insertRange.pasteHTML(argument);
1037 insertRange.select();
1038 //insertRange.collapse(true);
1040 }else if(dojo.isMoz && !argument.length){
1041 //mozilla can not inserthtml an empty html to delete current selection
1042 //so we delete the selection instead in this case
1043 dojo.withGlobal(this.window,'remove',dijit._editor.selection);
1046 returnValue=this.document.execCommand(command, false, argument);
1049 (command == "unlink")&&
1050 (this.queryCommandEnabled("unlink"))&&
1051 (dojo.isMoz || dojo.isSafari)
1053 // fix up unlink in Mozilla to unlink the link and not just the selection
1056 // Mozilla gets upset if we just store the range so we have to
1057 // get the basic properties and recreate to save the selection
1058 var selection = this.window.getSelection();
1059 // var selectionRange = selection.getRangeAt(0);
1060 // var selectionStartContainer = selectionRange.startContainer;
1061 // var selectionStartOffset = selectionRange.startOffset;
1062 // var selectionEndContainer = selectionRange.endContainer;
1063 // var selectionEndOffset = selectionRange.endOffset;
1065 // select our link and unlink
1066 var a = dojo.withGlobal(this.window, "getAncestorElement",dijit._editor.selection, ['a']);
1067 dojo.withGlobal(this.window, "selectElement", dijit._editor.selection, [a]);
1069 returnValue=this.document.execCommand("unlink", false, null);
1070 }else if((command == "hilitecolor")&&(dojo.isMoz)){
1071 // // mozilla doesn't support hilitecolor properly when useCSS is
1072 // // set to false (bugzilla #279330)
1074 this.document.execCommand("styleWithCSS", false, true);
1075 returnValue = this.document.execCommand(command, false, argument);
1076 this.document.execCommand("styleWithCSS", false, false);
1078 }else if((dojo.isIE)&&( (command == "backcolor")||(command == "forecolor") )){
1079 // Tested under IE 6 XP2, no problem here, comment out
1080 // IE weirdly collapses ranges when we exec these commands, so prevent it
1081 // var tr = this.document.selection.createRange();
1082 argument = arguments.length > 1 ? argument : null;
1083 returnValue = this.document.execCommand(command, false, argument);
1085 // timeout is workaround for weird IE behavior were the text
1086 // selection gets correctly re-created, but subsequent input
1087 // apparently isn't bound to it
1088 // setTimeout(function(){tr.select();}, 1);
1090 argument = arguments.length > 1 ? argument : null;
1092 // this.document = this.iframe.contentWindow.document
1095 if(argument || command!="createlink"){
1096 returnValue = this.document.execCommand(command, false, argument);
1100 this.onDisplayChanged();
1104 queryCommandEnabled: function(/*String*/command){
1105 // summary: check whether a command is enabled or not
1107 if(this.disabled){ return false; }
1108 command = this._normalizeCommand(command);
1109 if(dojo.isMoz || dojo.isSafari){
1110 if(command == "unlink"){ // mozilla returns true always
1111 // console.debug(dojo.withGlobal(this.window, "hasAncestorElement",dijit._editor.selection, ['a']));
1112 return dojo.withGlobal(this.window, "hasAncestorElement",dijit._editor.selection, ['a']);
1113 }else if(command == "inserttable"){
1119 if(command == "copy"){
1121 }else if(command == "paste"){
1126 // return this.document.queryCommandEnabled(command);
1127 var elem = dojo.isIE ? this.document.selection.createRange() : this.document;
1128 return elem.queryCommandEnabled(command);
1131 queryCommandState: function(command){
1132 // summary: check the state of a given command
1134 if(this.disabled){ return false; }
1135 command = this._normalizeCommand(command);
1136 return this.document.queryCommandState(command);
1139 queryCommandValue: function(command){
1140 // summary: check the value of a given command
1142 if(this.disabled){ return false; }
1143 command = this._normalizeCommand(command);
1144 if(dojo.isIE && command == "formatblock"){
1145 return this._local2NativeFormatNames[this.document.queryCommandValue(command)];
1147 return this.document.queryCommandValue(command);
1152 placeCursorAtStart: function(){
1154 // place the cursor at the start of the editing area
1157 //see comments in placeCursorAtEnd
1160 var first=this.editNode.firstChild;
1162 if(first.nodeType == 3){
1163 if(first.nodeValue.replace(/^\s+|\s+$/g, "").length>0){
1165 dojo.withGlobal(this.window, "selectElement", dijit._editor.selection, [first]);
1168 }else if(first.nodeType == 1){
1170 dojo.withGlobal(this.window, "selectElementChildren",dijit._editor.selection, [first]);
1173 first = first.nextSibling;
1177 dojo.withGlobal(this.window, "selectElementChildren",dijit._editor.selection, [this.editNode]);
1180 dojo.withGlobal(this.window, "collapse", dijit._editor.selection, [true]);
1184 placeCursorAtEnd: function(){
1186 // place the cursor at the end of the editing area
1189 //In mozilla, if last child is not a text node, we have to use selectElementChildren on this.editNode.lastChild
1190 //otherwise the cursor would be placed at the end of the closing tag of this.editNode.lastChild
1193 var last=this.editNode.lastChild;
1195 if(last.nodeType == 3){
1196 if(last.nodeValue.replace(/^\s+|\s+$/g, "").length>0){
1198 dojo.withGlobal(this.window, "selectElement",dijit._editor.selection, [last]);
1201 }else if(last.nodeType == 1){
1204 dojo.withGlobal(this.window, "selectElement",dijit._editor.selection, [last.lastChild]);
1206 dojo.withGlobal(this.window, "selectElement",dijit._editor.selection, [last]);
1210 last = last.previousSibling;
1214 dojo.withGlobal(this.window, "selectElementChildren",dijit._editor.selection, [this.editNode]);
1217 dojo.withGlobal(this.window, "collapse", dijit._editor.selection, [false]);
1221 getValue: function(/*Boolean?*/nonDestructive){
1223 // return the current content of the editing area (post filters are applied)
1225 if(this.isClosed || !this.isLoaded){
1226 return this.textarea.value;
1230 return this._postFilterContent(null, nonDestructive);
1233 setValue: function(/*String*/html){
1235 // this function set the content. No undo history is preserved
1238 // try again after the editor is finished loading
1239 this.onLoadDeferred.addCallback(dojo.hitch(this, function(){
1240 this.setValue(html);
1245 if(this.textarea && (this.isClosed || !this.isLoaded)){
1246 this.textarea.value=html;
1248 html = this._preFilterContent(html);
1249 var node = this.isClosed ? this.domNode : this.editNode;
1250 node.innerHTML = html;
1251 this._preDomFilterContent(node);
1254 this.onDisplayChanged();
1257 replaceValue: function(/*String*/html){
1259 // this function set the content while trying to maintain the undo stack
1260 // (now only works fine with Moz, this is identical to setValue in all
1263 this.setValue(html);
1264 }else if(this.window && this.window.getSelection && !dojo.isMoz){ // Safari
1265 // look ma! it's a totally f'd browser!
1266 this.setValue(html);
1267 }else if(this.window && this.window.getSelection){ // Moz
1268 html = this._preFilterContent(html);
1269 this.execCommand("selectall");
1270 if(dojo.isMoz && !html){ html = " " }
1271 this.execCommand("inserthtml", html);
1272 this._preDomFilterContent(this.editNode);
1273 }else if(this.document && this.document.selection){//IE
1274 //In IE, when the first element is not a text node, say
1275 //an <a> tag, when replacing the content of the editing
1276 //area, the <a> tag will be around all the content
1277 //so for now, use setValue for IE too
1278 this.setValue(html);
1282 _preFilterContent: function(/*String*/html){
1284 // filter the input before setting the content of the editing area
1286 dojo.forEach(this.contentPreFilters, function(ef){ if(ef){ ec = ef(ec); } });
1289 _preDomFilterContent: function(/*DomNode*/dom){
1292 dom = dom || this.editNode;
1293 dojo.forEach(this.contentDomPreFilters, function(ef){
1294 if(ef && dojo.isFunction(ef)){
1300 _postFilterContent: function(/*DomNode|DomNode[]|String?*/dom,/*Boolean?*/nonDestructive){
1302 // filter the output after getting the content of the editing area
1304 if(!dojo.isString(dom)){
1305 dom = dom || this.editNode;
1306 if(this.contentDomPostFilters.length){
1307 if(nonDestructive && dom['cloneNode']){
1308 dom = dom.cloneNode(true);
1310 dojo.forEach(this.contentDomPostFilters, function(ef){
1314 ec = dijit._editor.getChildrenHtml(dom);
1319 if(!ec.replace(/^(?:\s|\xA0)+/g, "").replace(/(?:\s|\xA0)+$/g,"").length){ ec = ""; }
1322 // //removing appended <P> </P> for IE
1323 // ec = ec.replace(/(?:<p> </p>[\n\r]*)+$/i,"");
1325 dojo.forEach(this.contentPostFilters, function(ef){
1332 _saveContent: function(/*Event*/e){
1334 // Saves the content in an onunload event if the editor has not been closed
1335 var saveTextarea = dojo.byId(dijit._scopeName + "._editor.RichText.savedContent");
1336 saveTextarea.value += this._SEPARATOR + this.name + ":" + this.getValue();
1339 escapeXml: function(/*String*/str, /*Boolean*/noSingleQuotes){
1340 dojo.deprecated('dijit.Editor::escapeXml is deprecated','use dijit._editor.escapeXml instead', 2);
1341 return dijit._editor.escapeXml(str,noSingleQuotes);
1344 getNodeHtml: function(/* DomNode */node){
1345 dojo.deprecated('dijit.Editor::getNodeHtml is deprecated','use dijit._editor.getNodeHtml instead', 2);
1346 return dijit._editor.getNodeHtml(node);
1349 getNodeChildrenHtml: function(/* DomNode */dom){
1350 dojo.deprecated('dijit.Editor::getNodeChildrenHtml is deprecated','use dijit._editor.getChildrenHtml instead', 2);
1351 return dijit._editor.getChildrenHtml(dom);
1354 close: function(/*Boolean*/save, /*Boolean*/force){
1356 // Kills the editor and optionally writes back the modified contents to the
1357 // element from which it originated.
1359 // Whether or not to save the changes. If false, the changes are discarded.
1361 if(this.isClosed){return false; }
1363 if(!arguments.length){ save = true; }
1364 this._content = this.getValue();
1365 var changed = (this.savedContent != this._content);
1367 // line height is squashed for iframes
1368 // FIXME: why was this here? if(this.iframe){ this.domNode.style.lineHeight = null; }
1370 if(this.interval){ clearInterval(this.interval); }
1373 with(this.textarea.style){
1377 overflow = this.__overflow;
1378 this.__overflow = null;
1381 this.textarea.value = save ? this._content : this.savedContent;
1382 dojo._destroyElement(this.domNode);
1383 this.domNode = this.textarea;
1386 //why we treat moz differently? comment out to fix #1061
1388 // var nc = dojo.doc.createElement("span");
1389 // this.domNode.appendChild(nc);
1390 // nc.innerHTML = this.editNode.innerHTML;
1392 // this.domNode.innerHTML = this._content;
1395 this.domNode.innerHTML = save ? this._content : this.savedContent;
1398 dojo.removeClass(this.domNode, "RichTextEditable");
1399 this.isClosed = true;
1400 this.isLoaded = false;
1401 // FIXME: is this always the right thing to do?
1402 delete this.editNode;
1404 if(this.window && this.window._frameElement){
1405 this.window._frameElement = null;
1409 this.document = null;
1410 this.editingArea = null;
1411 this.editorObject = null;
1413 return changed; // Boolean: whether the content has been modified
1416 destroyRendering: function(){
1420 destroy: function(){
1421 this.destroyRendering();
1422 if(!this.isClosed){ this.close(false); }
1423 this.inherited("destroy",arguments);
1424 //dijit._editor.RichText.superclass.destroy.call(this);
1427 _removeMozBogus: function(/* String */ html){
1428 return html.replace(/\stype="_moz"/gi, '').replace(/\s_moz_dirty=""/gi, ''); // String
1430 _removeSafariBogus: function(/* String */ html){
1431 return html.replace(/\sclass="webkit-block-placeholder"/gi, ''); // String
1433 _fixContentForMoz: function(/* String */ html){
1435 // Moz can not handle strong/em tags correctly, convert them to b/i
1436 return html.replace(/<(\/)?strong([ \>])/gi, '<$1b$2')
1437 .replace(/<(\/)?em([ \>])/gi, '<$1i$2' ); // String
1440 _srcInImgRegex : /(?:(<img(?=\s).*?\ssrc=)("|')(.*?)\2)|(?:(<img\s.*?src=)([^"'][^ >]+))/gi ,
1441 _hrefInARegex : /(?:(<a(?=\s).*?\shref=)("|')(.*?)\2)|(?:(<a\s.*?href=)([^"'][^ >]+))/gi ,
1443 _preFixUrlAttributes: function(/* String */ html){
1444 return html.replace(this._hrefInARegex, '$1$4$2$3$5$2 _djrealurl=$2$3$5$2')
1445 .replace(this._srcInImgRegex, '$1$4$2$3$5$2 _djrealurl=$2$3$5$2'); // String