diff options
Diffstat (limited to 'includes/js/dijit/_editor/RichText.js')
-rw-r--r-- | includes/js/dijit/_editor/RichText.js | 1449 |
1 files changed, 1449 insertions, 0 deletions
diff --git a/includes/js/dijit/_editor/RichText.js b/includes/js/dijit/_editor/RichText.js new file mode 100644 index 0000000..9ece365 --- /dev/null +++ b/includes/js/dijit/_editor/RichText.js @@ -0,0 +1,1449 @@ +if(!dojo._hasResource["dijit._editor.RichText"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit._editor.RichText"] = true; +dojo.provide("dijit._editor.RichText"); + +dojo.require("dijit._Widget"); +dojo.require("dijit._editor.selection"); +dojo.require("dijit._editor.html"); +dojo.require("dojo.i18n"); +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"); + +// used to restore content when user leaves this page then comes back +// but do not try doing dojo.doc.write if we are using xd loading. +// dojo.doc.write will only work if RichText.js is included in the dojo.js +// file. If it is included in dojo.js and you want to allow rich text saving +// for back/forward actions, then set dojo.config.allowXdRichTextSave = true. +if(!dojo.config["useXDomain"] || dojo.config["allowXdRichTextSave"]){ + if(dojo._postLoad){ + (function(){ + var savetextarea = dojo.doc.createElement('textarea'); + savetextarea.id = dijit._scopeName + "._editor.RichText.savedContent"; + var s = savetextarea.style; + s.display='none'; + s.position='absolute'; + s.top="-100px"; + s.left="-100px"; + s.height="3px"; + s.width="3px"; + dojo.body().appendChild(savetextarea); + })(); + }else{ + //dojo.body() is not available before onLoad is fired + try{ + dojo.doc.write('<textarea id="' + dijit._scopeName + '._editor.RichText.savedContent" ' + + 'style="display:none;position:absolute;top:-100px;left:-100px;height:3px;width:3px;overflow:hidden;"></textarea>'); + }catch(e){ } + } +} +dojo.declare("dijit._editor.RichText", dijit._Widget, { + constructor: function(){ + // summary: + // dijit._editor.RichText is the core of the WYSIWYG editor in dojo, which + // provides the basic editing features. It also encapsulates the differences + // of different js engines for various browsers + // + // contentPreFilters: Array + // pre content filter function register array. + // these filters will be executed before the actual + // editing area get the html content + this.contentPreFilters = []; + + // contentPostFilters: Array + // post content filter function register array. + // these will be used on the resulting html + // from contentDomPostFilters. The resuling + // content is the final html (returned by getValue()) + this.contentPostFilters = []; + + // contentDomPreFilters: Array + // pre content dom filter function register array. + // these filters are applied after the result from + // contentPreFilters are set to the editing area + this.contentDomPreFilters = []; + + // contentDomPostFilters: Array + // post content dom filter function register array. + // these filters are executed on the editing area dom + // the result from these will be passed to contentPostFilters + this.contentDomPostFilters = []; + + // editingAreaStyleSheets: Array + // array to store all the stylesheets applied to the editing area + this.editingAreaStyleSheets=[]; + + this._keyHandlers = {}; + this.contentPreFilters.push(dojo.hitch(this, "_preFixUrlAttributes")); + if(dojo.isMoz){ + this.contentPreFilters.push(this._fixContentForMoz); + this.contentPostFilters.push(this._removeMozBogus); + }else if(dojo.isSafari){ + this.contentPostFilters.push(this._removeSafariBogus); + } + //this.contentDomPostFilters.push(this._postDomFixUrlAttributes); + + this.onLoadDeferred = new dojo.Deferred(); + }, + + // inheritWidth: Boolean + // whether to inherit the parent's width or simply use 100% + inheritWidth: false, + + // focusOnLoad: Boolean + // whether focusing into this instance of richtext when page onload + focusOnLoad: false, + + // name: String + // If a save name is specified the content is saved and restored when the user + // leave this page can come back, or if the editor is not properly closed after + // editing has started. + name: "", + + // styleSheets: String + // semicolon (";") separated list of css files for the editing area + styleSheets: "", + + // _content: String + // temporary content storage + _content: "", + + // height: String + // set height to fix the editor at a specific height, with scrolling. + // By default, this is 300px. If you want to have the editor always + // resizes to accommodate the content, use AlwaysShowToolbar plugin + // and set height="" + height: "300px", + + // minHeight: String + // The minimum height that the editor should have + minHeight: "1em", + + // isClosed: Boolean + isClosed: true, + + // isLoaded: Boolean + isLoaded: false, + + // _SEPARATOR: String + // used to concat contents from multiple textareas into a single string + _SEPARATOR: "@@**%%__RICHTEXTBOUNDRY__%%**@@", + + // onLoadDeferred: dojo.Deferred + // deferred which is fired when the editor finishes loading + onLoadDeferred: null, + + postCreate: function(){ + // summary: init + dojo.publish(dijit._scopeName + "._editor.RichText::init", [this]); + this.open(); + this.setupDefaultShortcuts(); + }, + + setupDefaultShortcuts: function(){ + // summary: add some default key handlers + // description: + // Overwrite this to setup your own handlers. The default + // implementation does not use Editor commands, but directly + // executes the builtin commands within the underlying browser + // support. + var exec = function(cmd, arg){ + return arguments.length == 1 ? function(){ this.execCommand(cmd); } : + function(){ this.execCommand(cmd, arg); }; + }; + + var ctrlKeyHandlers = { b: exec("bold"), + i: exec("italic"), + u: exec("underline"), + a: exec("selectall"), + s: function(){ this.save(true); }, + + "1": exec("formatblock", "h1"), + "2": exec("formatblock", "h2"), + "3": exec("formatblock", "h3"), + "4": exec("formatblock", "h4"), + + "\\": exec("insertunorderedlist") }; + + if(!dojo.isIE){ + ctrlKeyHandlers.Z = exec("redo"); //FIXME: undo? + } + + for(var key in ctrlKeyHandlers){ + this.addKeyHandler(key, this.KEY_CTRL, ctrlKeyHandlers[key]); + } + }, + + // events: Array + // events which should be connected to the underlying editing area + events: ["onKeyPress", "onKeyDown", "onKeyUp", "onClick"], + + // events: Array + // events which should be connected to the underlying editing + // area, events in this array will be addListener with + // capture=true + captureEvents: [], + + _editorCommandsLocalized: false, + _localizeEditorCommands: function(){ + if(this._editorCommandsLocalized){ + return; + } + this._editorCommandsLocalized = true; + + //in IE, names for blockformat is locale dependent, so we cache the values here + + //if the normal way fails, we try the hard way to get the list + + //do not use _cacheLocalBlockFormatNames here, as it will + //trigger security warning in IE7 + + //in the array below, ul can not come directly after ol, + //otherwise the queryCommandValue returns Normal for it + var formats = ['p', 'pre', 'address', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'div', 'ul']; + var localhtml = "", format, i=0; + while((format=formats[i++])){ + if(format.charAt(1) != 'l'){ + localhtml += "<"+format+"><span>content</span></"+format+">"; + }else{ + localhtml += "<"+format+"><li>content</li></"+format+">"; + } + } + //queryCommandValue returns empty if we hide editNode, so move it out of screen temporary + var div=dojo.doc.createElement('div'); + div.style.position = "absolute"; + div.style.left = "-2000px"; + div.style.top = "-2000px"; + dojo.doc.body.appendChild(div); + div.innerHTML = localhtml; + var node = div.firstChild; + while(node){ + dijit._editor.selection.selectElement(node.firstChild); + dojo.withGlobal(this.window, "selectElement", dijit._editor.selection, [node.firstChild]); + var nativename = node.tagName.toLowerCase(); + this._local2NativeFormatNames[nativename] = dojo.doc.queryCommandValue("formatblock");//this.queryCommandValue("formatblock"); + this._native2LocalFormatNames[this._local2NativeFormatNames[nativename]] = nativename; + node = node.nextSibling; + } + dojo.doc.body.removeChild(div); + }, + + open: function(/*DomNode?*/element){ + // summary: + // Transforms the node referenced in this.domNode into a rich text editing + // node. This will result in the creation and replacement with an <iframe> + // if designMode(FF)/contentEditable(IE) is used. + + if((!this.onLoadDeferred)||(this.onLoadDeferred.fired >= 0)){ + this.onLoadDeferred = new dojo.Deferred(); + } + + if(!this.isClosed){ this.close(); } + dojo.publish(dijit._scopeName + "._editor.RichText::open", [ this ]); + + this._content = ""; + if((arguments.length == 1)&&(element["nodeName"])){ this.domNode = element; } // else unchanged + + var html; + if( (this.domNode["nodeName"])&& + (this.domNode.nodeName.toLowerCase() == "textarea")){ + // if we were created from a textarea, then we need to create a + // new editing harness node. + this.textarea = this.domNode; + this.name=this.textarea.name; + html = this._preFilterContent(this.textarea.value); + this.domNode = dojo.doc.createElement("div"); + this.domNode.setAttribute('widgetId',this.id); + this.textarea.removeAttribute('widgetId'); + this.domNode.cssText = this.textarea.cssText; + this.domNode.className += " "+this.textarea.className; + dojo.place(this.domNode, this.textarea, "before"); + var tmpFunc = dojo.hitch(this, function(){ + //some browsers refuse to submit display=none textarea, so + //move the textarea out of screen instead + dojo.attr(this.textarea, 'tabIndex', '-1'); + with(this.textarea.style){ + display = "block"; + position = "absolute"; + left = top = "-1000px"; + + if(dojo.isIE){ //nasty IE bug: abnormal formatting if overflow is not hidden + this.__overflow = overflow; + overflow = "hidden"; + } + } + }); + if(dojo.isIE){ + setTimeout(tmpFunc, 10); + }else{ + tmpFunc(); + } + + // this.domNode.innerHTML = html; + +// if(this.textarea.form){ +// // FIXME: port: this used to be before advice!!! +// dojo.connect(this.textarea.form, "onsubmit", this, function(){ +// // FIXME: should we be calling close() here instead? +// this.textarea.value = this.getValue(); +// }); +// } + }else{ + html = this._preFilterContent(dijit._editor.getChildrenHtml(this.domNode)); + this.domNode.innerHTML = ''; + } + if(html == ""){ html = " "; } + + var content = dojo.contentBox(this.domNode); + // var content = dojo.contentBox(this.srcNodeRef); + this._oldHeight = content.h; + this._oldWidth = content.w; + + // If we're a list item we have to put in a blank line to force the + // bullet to nicely align at the top of text + if( (this.domNode["nodeName"]) && + (this.domNode.nodeName == "LI") ){ + this.domNode.innerHTML = " <br>"; + } + + this.editingArea = dojo.doc.createElement("div"); + this.domNode.appendChild(this.editingArea); + + if(this.name != "" && (!dojo.config["useXDomain"] || dojo.config["allowXdRichTextSave"])){ + var saveTextarea = dojo.byId(dijit._scopeName + "._editor.RichText.savedContent"); + if(saveTextarea.value != ""){ + var datas = saveTextarea.value.split(this._SEPARATOR), i=0, dat; + while((dat=datas[i++])){ + var data = dat.split(":"); + if(data[0] == this.name){ + html = data[1]; + datas.splice(i, 1); + break; + } + } + } + + // FIXME: need to do something different for Opera/Safari + this.connect(window, "onbeforeunload", "_saveContent"); + // dojo.connect(window, "onunload", this, "_saveContent"); + } + + this.isClosed = false; + // Safari's selections go all out of whack if we do it inline, + // so for now IE is our only hero + //if(typeof dojo.doc.body.contentEditable != "undefined"){ + if(dojo.isIE || dojo.isSafari || dojo.isOpera){ // contentEditable, easy + + if(dojo.config["useXDomain"] && !dojo.config["dojoBlankHtmlUrl"]){ + console.debug("dijit._editor.RichText: When using cross-domain Dojo builds," + + " please save dojo/resources/blank.html to your domain and set djConfig.dojoBlankHtmlUrl" + + " to the path on your domain to blank.html"); + } + + var burl = dojo.config["dojoBlankHtmlUrl"] || (dojo.moduleUrl("dojo", "resources/blank.html")+""); + var ifr = this.editorObject = this.iframe = dojo.doc.createElement('iframe'); + ifr.id = this.id+"_iframe"; + ifr.src = burl; + ifr.style.border = "none"; + ifr.style.width = "100%"; + ifr.frameBorder = 0; + // ifr.style.scrolling = this.height ? "auto" : "vertical"; + this.editingArea.appendChild(ifr); + var h = null; // set later in non-ie6 branch + var loadFunc = dojo.hitch( this, function(){ + if(h){ dojo.disconnect(h); h = null; } + this.window = ifr.contentWindow; + var d = this.document = this.window.document; + d.open(); + d.write(this._getIframeDocTxt(html)); + d.close(); + + if(dojo.isIE >= 7){ + if(this.height){ + ifr.style.height = this.height; + } + if(this.minHeight){ + ifr.style.minHeight = this.minHeight; + } + }else{ + ifr.style.height = this.height ? this.height : this.minHeight; + } + + if(dojo.isIE){ + this._localizeEditorCommands(); + } + + this.onLoad(); + this.savedContent = this.getValue(true); + }); + if(dojo.isIE && dojo.isIE < 7){ // IE 6 is a steaming pile... + var t = setInterval(function(){ + if(ifr.contentWindow.isLoaded){ + clearInterval(t); + loadFunc(); + } + }, 100); + }else{ // blissful sanity! + h = dojo.connect( + ((dojo.isIE) ? ifr.contentWindow : ifr), "onload", loadFunc + ); + } + }else{ // designMode in iframe + this._drawIframe(html); + this.savedContent = this.getValue(true); + } + + // TODO: this is a guess at the default line-height, kinda works + if(this.domNode.nodeName == "LI"){ this.domNode.lastChild.style.marginTop = "-1.2em"; } + this.domNode.className += " RichTextEditable"; + }, + + //static cache variables shared among all instance of this class + _local2NativeFormatNames: {}, + _native2LocalFormatNames: {}, + _localizedIframeTitles: null, + + _getIframeDocTxt: function(/* String */ html){ + var _cs = dojo.getComputedStyle(this.domNode); + if(dojo.isIE || (!this.height && !dojo.isMoz)){ + html="<div>"+html+"</div>"; + } + var font = [ _cs.fontWeight, _cs.fontSize, _cs.fontFamily ].join(" "); + + // line height is tricky - applying a units value will mess things up. + // if we can't get a non-units value, bail out. + var lineHeight = _cs.lineHeight; + if(lineHeight.indexOf("px") >= 0){ + lineHeight = parseFloat(lineHeight)/parseFloat(_cs.fontSize); + // console.debug(lineHeight); + }else if(lineHeight.indexOf("em")>=0){ + lineHeight = parseFloat(lineHeight); + }else{ + lineHeight = "1.0"; + } + return [ + this.isLeftToRight() ? "<html><head>" : "<html dir='rtl'><head>", + (dojo.isMoz ? "<title>" + this._localizedIframeTitles.iframeEditTitle + "</title>" : ""), + "<style>", + "body,html {", + " background:transparent;", + " font:", font, ";", + " padding: 1em 0 0 0;", + " margin: -1em 0 0 0;", // remove extraneous vertical scrollbar on safari and firefox + " height: 100%;", + "}", + // TODO: left positioning will cause contents to disappear out of view + // if it gets too wide for the visible area + "body{", + " top:0px; left:0px; right:0px;", + ((this.height||dojo.isOpera) ? "" : "position: fixed;"), + // FIXME: IE 6 won't understand min-height? + " min-height:", this.minHeight, ";", + " line-height:", lineHeight, + "}", + "p{ margin: 1em 0 !important; }", + (this.height ? // height:auto undoes the height:100% + "" : "body,html{height:auto;overflow-y:hidden;/*for IE*/} body > div {overflow-x:auto;/*for FF to show vertical scrollbar*/}" + ), + "li > ul:-moz-first-node, li > ol:-moz-first-node{ padding-top: 1.2em; } ", + "li{ min-height:1.2em; }", + "</style>", + this._applyEditingAreaStyleSheets(), + "</head><body>"+html+"</body></html>" + ].join(""); // String + }, + + _drawIframe: function(/*String*/html){ + // summary: + // Draws an iFrame using the existing one if one exists. + // Used by Mozilla, Safari, and Opera + + if(!this.iframe){ + var ifr = this.iframe = dojo.doc.createElement("iframe"); + ifr.id=this.id; + // this.iframe.src = "about:blank"; + // dojo.doc.body.appendChild(this.iframe); + // console.debug(this.iframe.contentDocument.open()); + // dojo.body().appendChild(this.iframe); + var ifrs = ifr.style; + // ifrs.border = "1px solid black"; + ifrs.border = "none"; + ifrs.lineHeight = "0"; // squash line height + ifrs.verticalAlign = "bottom"; +// ifrs.scrolling = this.height ? "auto" : "vertical"; + this.editorObject = this.iframe; + // get screen reader text for mozilla here, too + this._localizedIframeTitles = dojo.i18n.getLocalization("dijit.form", "Textarea"); + // need to find any associated label element and update iframe document title + var label=dojo.query('label[for="'+this.id+'"]'); + if(label.length){ + this._localizedIframeTitles.iframeEditTitle = label[0].innerHTML + " " + this._localizedIframeTitles.iframeEditTitle; + } + } + // opera likes this to be outside the with block + // this.iframe.src = "javascript:void(0)";//dojo.uri.dojoUri("src/widget/templates/richtextframe.html") + ((dojo.doc.domain != currentDomain) ? ("#"+dojo.doc.domain) : ""); + this.iframe.style.width = this.inheritWidth ? this._oldWidth : "100%"; + + if(this.height){ + this.iframe.style.height = this.height; + }else{ + this.iframe.height = this._oldHeight; + } + + var tmpContent; + if(this.textarea){ + tmpContent = this.srcNodeRef; + }else{ + tmpContent = dojo.doc.createElement('div'); + tmpContent.style.display="none"; + tmpContent.innerHTML = html; + //append tmpContent to under the current domNode so that the margin + //calculation below is correct + this.editingArea.appendChild(tmpContent); + } + + this.editingArea.appendChild(this.iframe); + + //do we want to show the content before the editing area finish loading here? + //if external style sheets are used for the editing area, the appearance now + //and after loading of the editing area won't be the same (and padding/margin + //calculation above may not be accurate) + // tmpContent.style.display = "none"; + // this.editingArea.appendChild(this.iframe); + + var _iframeInitialized = false; + // console.debug(this.iframe); + // var contentDoc = this.iframe.contentWindow.document; + + + // note that on Safari lower than 420+, we have to get the iframe + // by ID in order to get something w/ a contentDocument property + + var contentDoc = this.iframe.contentDocument; + contentDoc.open(); + if(dojo.isAIR){ + contentDoc.body.innerHTML = html; + }else{ + contentDoc.write(this._getIframeDocTxt(html)); + } + contentDoc.close(); + + // now we wait for onload. Janky hack! + var ifrFunc = dojo.hitch(this, function(){ + if(!_iframeInitialized){ + _iframeInitialized = true; + }else{ return; } + if(!this.editNode){ + try{ + if(this.iframe.contentWindow){ + this.window = this.iframe.contentWindow; + this.document = this.iframe.contentWindow.document + }else if(this.iframe.contentDocument){ + // for opera + this.window = this.iframe.contentDocument.window; + this.document = this.iframe.contentDocument; + } + if(!this.document.body){ + throw 'Error'; + } + }catch(e){ + setTimeout(ifrFunc,500); + _iframeInitialized = false; + return; + } + + dojo._destroyElement(tmpContent); + this.onLoad(); + }else{ + dojo._destroyElement(tmpContent); + this.editNode.innerHTML = html; + this.onDisplayChanged(); + } + this._preDomFilterContent(this.editNode); + }); + + ifrFunc(); + }, + + _applyEditingAreaStyleSheets: function(){ + // summary: + // apply the specified css files in styleSheets + var files = []; + if(this.styleSheets){ + files = this.styleSheets.split(';'); + this.styleSheets = ''; + } + + //empty this.editingAreaStyleSheets here, as it will be filled in addStyleSheet + files = files.concat(this.editingAreaStyleSheets); + this.editingAreaStyleSheets = []; + + var text='', i=0, url; + while((url=files[i++])){ + var abstring = (new dojo._Url(dojo.global.location, url)).toString(); + this.editingAreaStyleSheets.push(abstring); + text += '<link rel="stylesheet" type="text/css" href="'+abstring+'"/>' + } + return text; + }, + + addStyleSheet: function(/*dojo._Url*/uri){ + // summary: + // add an external stylesheet for the editing area + // uri: a dojo.uri.Uri pointing to the url of the external css file + var url=uri.toString(); + + //if uri is relative, then convert it to absolute so that it can be resolved correctly in iframe + if(url.charAt(0) == '.' || (url.charAt(0) != '/' && !uri.host)){ + url = (new dojo._Url(dojo.global.location, url)).toString(); + } + + if(dojo.indexOf(this.editingAreaStyleSheets, url) > -1){ +// console.debug("dijit._editor.RichText.addStyleSheet: Style sheet "+url+" is already applied"); + return; + } + + this.editingAreaStyleSheets.push(url); + if(this.document.createStyleSheet){ //IE + this.document.createStyleSheet(url); + }else{ //other browser + var head = this.document.getElementsByTagName("head")[0]; + var stylesheet = this.document.createElement("link"); + with(stylesheet){ + rel="stylesheet"; + type="text/css"; + href=url; + } + head.appendChild(stylesheet); + } + }, + + removeStyleSheet: function(/*dojo._Url*/uri){ + // summary: + // remove an external stylesheet for the editing area + var url=uri.toString(); + //if uri is relative, then convert it to absolute so that it can be resolved correctly in iframe + if(url.charAt(0) == '.' || (url.charAt(0) != '/' && !uri.host)){ + url = (new dojo._Url(dojo.global.location, url)).toString(); + } + var index = dojo.indexOf(this.editingAreaStyleSheets, url); + if(index == -1){ +// console.debug("dijit._editor.RichText.removeStyleSheet: Style sheet "+url+" has not been applied"); + return; + } + delete this.editingAreaStyleSheets[index]; + dojo.withGlobal(this.window,'query', dojo, ['link:[href="'+url+'"]']).orphan() + }, + + disabled: true, + _mozSettingProps: ['styleWithCSS','insertBrOnReturn'], + setDisabled: function(/*Boolean*/ disabled){ + if(dojo.isIE || dojo.isSafari || dojo.isOpera){ + if(dojo.isIE){ this.editNode.unselectable = "on"; } // prevent IE from setting focus + this.editNode.contentEditable = !disabled; + if(dojo.isIE){ + var _this = this; + setTimeout(function(){ _this.editNode.unselectable = "off"; }, 0); + } + }else{ //moz + if(disabled){ + //AP: why isn't this set in the constructor, or put in mozSettingProps as a hash? + this._mozSettings=[false,this.blockNodeForEnter==='BR']; + } + this.document.designMode=(disabled?'off':'on'); + if(!disabled && this._mozSettings){ + dojo.forEach(this._mozSettingProps, function(s,i){ + this.document.execCommand(s,false,this._mozSettings[i]); + },this); + } +// this.document.execCommand('contentReadOnly', false, disabled); +// if(disabled){ +// this.blur(); //to remove the blinking caret +// } + } + this.disabled = disabled; + }, + +/* Event handlers + *****************/ + + _isResized: function(){ return false; }, + + onLoad: function(/* Event */ e){ + // summary: handler after the content of the document finishes loading + this.isLoaded = true; + if(!this.window.__registeredWindow){ + this.window.__registeredWindow=true; + dijit.registerWin(this.window); + } + if(!dojo.isIE && (this.height || dojo.isMoz)){ + this.editNode=this.document.body; + }else{ + this.editNode=this.document.body.firstChild; + var _this = this; + if(dojo.isIE){ // #4996 IE wants to focus the BODY tag + var tabStop = this.tabStop = dojo.doc.createElement('<div tabIndex=-1>'); + this.editingArea.appendChild(tabStop); + this.iframe.onfocus = function(){ _this.editNode.setActive(); } + } + } + + try{ + this.setDisabled(false); + }catch(e){ + // Firefox throws an exception if the editor is initially hidden + // so, if this fails, try again onClick by adding "once" advice + var handle = dojo.connect(this, "onClick", this, function(){ + this.setDisabled(false); + dojo.disconnect(handle); + }); + } + + this._preDomFilterContent(this.editNode); + + var events=this.events.concat(this.captureEvents),i=0,et; + while((et=events[i++])){ + this.connect(this.document, et.toLowerCase(), et); + } + if(!dojo.isIE){ + try{ // sanity check for Mozilla + //AP: what's the point of this? +// this.document.execCommand("useCSS", false, true); // old moz call + this.document.execCommand("styleWithCSS", false, false); // new moz call + //this.document.execCommand("insertBrOnReturn", false, false); // new moz call + }catch(e2){ } + // FIXME: when scrollbars appear/disappear this needs to be fired + }else{ // IE contentEditable + // give the node Layout on IE + this.connect(this.document, "onmousedown", "_onMouseDown"); // #4996 fix focus + this.editNode.style.zoom = 1.0; + } + + if(this.focusOnLoad){ + setTimeout(dojo.hitch(this, "focus"), 0); // have to wait for IE to set unselectable=off + } + + this.onDisplayChanged(e); + if(this.onLoadDeferred){ + this.onLoadDeferred.callback(true); + } + }, + + onKeyDown: function(/* Event */ e){ + // summary: Fired on keydown + + // we need this event at the moment to get the events from control keys + // such as the backspace. It might be possible to add this to Dojo, so that + // keyPress events can be emulated by the keyDown and keyUp detection. + if(dojo.isIE){ + if(e.keyCode == dojo.keys.TAB && e.shiftKey && !e.ctrlKey && !e.altKey){ + // focus the BODY so the browser will tab away from it instead + this.iframe.focus(); + }else if(e.keyCode == dojo.keys.TAB && !e.shiftKey && !e.ctrlKey && !e.altKey){ + // focus the BODY so the browser will tab away from it instead + this.tabStop.focus(); + }else if(e.keyCode === dojo.keys.BACKSPACE && this.document.selection.type === "Control"){ + // IE has a bug where if a non-text object is selected in the editor, + // hitting backspace would act as if the browser's back button was + // clicked instead of deleting the object. see #1069 + dojo.stopEvent(e); + this.execCommand("delete"); + }else if((65 <= e.keyCode&&e.keyCode <= 90) || + (e.keyCode>=37&&e.keyCode<=40) // FIXME: get this from connect() instead! + ){ //arrow keys + e.charCode = e.keyCode; + this.onKeyPress(e); + } + }else if(dojo.isMoz){ + if(e.keyCode == dojo.keys.TAB && !e.shiftKey && !e.ctrlKey && !e.altKey && this.iframe){ + // update iframe document title for screen reader + this.iframe.contentDocument.title = this._localizedIframeTitles.iframeFocusTitle; + + // Place focus on the iframe. A subsequent tab or shift tab will put focus + // on the correct control. + this.iframe.focus(); // this.focus(); won't work + dojo.stopEvent(e); + }else if(e.keyCode == dojo.keys.TAB && e.shiftKey){ + // if there is a toolbar, set focus to it, otherwise ignore + if(this.toolbar){ + this.toolbar.focus(); + } + dojo.stopEvent(e); + } + } + }, + + onKeyUp: function(e){ + // summary: Fired on keyup + return; + }, + + KEY_CTRL: 1, + KEY_SHIFT: 2, + + onKeyPress: function(e){ + // summary: Fired on keypress + + // handle the various key events + var modifiers = (e.ctrlKey && !e.altKey) ? this.KEY_CTRL : 0 | e.shiftKey ? this.KEY_SHIFT : 0; + + var key = e.keyChar || e.keyCode; + if(this._keyHandlers[key]){ + // console.debug("char:", e.key); + var handlers = this._keyHandlers[key], i = 0, h; + while((h = handlers[i++])){ + if(modifiers == h.modifiers){ + if(!h.handler.apply(this,arguments)){ + e.preventDefault(); + } + break; + } + } + } + + // function call after the character has been inserted + setTimeout(dojo.hitch(this, function(){ + this.onKeyPressed(e); + }), 1); + }, + + addKeyHandler: function(/*String*/key, /*Int*/modifiers, /*Function*/handler){ + // summary: add a handler for a keyboard shortcut + if(!dojo.isArray(this._keyHandlers[key])){ this._keyHandlers[key] = []; } + this._keyHandlers[key].push({ + modifiers: modifiers || 0, + handler: handler + }); + }, + + onKeyPressed: function(/*Event*/e){ + this.onDisplayChanged(/*e*/); // can't pass in e + }, + + onClick: function(/*Event*/e){ +// console.info('onClick',this._tryDesignModeOn); + this.onDisplayChanged(e); + }, + + _onMouseDown: function(/*Event*/e){ // IE only to prevent 2 clicks to focus + if(!this._focused && !this.disabled){ + this.focus(); + } + }, + + _onBlur: function(e){ + this.inherited(arguments); + var _c=this.getValue(true); + if(_c!=this.savedContent){ + this.onChange(_c); + this.savedContent=_c; + } + if(dojo.isMoz && this.iframe){ + this.iframe.contentDocument.title = this._localizedIframeTitles.iframeEditTitle; + } + }, + _initialFocus: true, + _onFocus: function(/*Event*/e){ + // summary: Fired on focus + this.inherited(arguments); + if(dojo.isMoz && this._initialFocus){ + this._initialFocus = false; + if(this.editNode.innerHTML.replace(/^\s+|\s+$/g, "") == " "){ + this.placeCursorAtStart(); +// this.execCommand("selectall"); +// this.window.getSelection().collapseToStart(); + } + } + }, + + // TODO: why is this needed - should we deprecate this ? + blur: function(){ + // summary: remove focus from this instance + if(!dojo.isIE && this.window.document.documentElement && this.window.document.documentElement.focus){ + this.window.document.documentElement.focus(); + }else if(dojo.doc.body.focus){ + dojo.doc.body.focus(); + } + }, + + focus: function(){ + // summary: move focus to this instance + if(!dojo.isIE){ + dijit.focus(this.iframe); + }else if(this.editNode && this.editNode.focus){ + // editNode may be hidden in display:none div, lets just punt in this case + //this.editNode.focus(); -> causes IE to scroll always (strict and quirks mode) to the top the Iframe + // if we fire the event manually and let the browser handle the focusing, the latest + // cursor position is focused like in FF + this.iframe.fireEvent('onfocus', document.createEventObject()); // createEventObject only in IE +// }else{ +// TODO: should we throw here? +// console.debug("Have no idea how to focus into the editor!"); + } + }, + +// _lastUpdate: 0, + updateInterval: 200, + _updateTimer: null, + onDisplayChanged: function(/*Event*/e){ + // summary: + // This event will be fired everytime the display context + // changes and the result needs to be reflected in the UI. + // description: + // If you don't want to have update too often, + // onNormalizedDisplayChanged should be used instead + +// var _t=new Date(); + if(!this._updateTimer){ +// this._lastUpdate=_t; + if(this._updateTimer){ + clearTimeout(this._updateTimer); + } + this._updateTimer=setTimeout(dojo.hitch(this,this.onNormalizedDisplayChanged),this.updateInterval); + } + }, + onNormalizedDisplayChanged: function(){ + // summary: + // This event is fired every updateInterval ms or more + // description: + // If something needs to happen immidiately after a + // user change, please use onDisplayChanged instead + this._updateTimer=null; + }, + onChange: function(newContent){ + // summary: + // this is fired if and only if the editor loses focus and + // the content is changed + +// console.log('onChange',newContent); + }, + _normalizeCommand: function(/*String*/cmd){ + // summary: + // Used as the advice function by dojo.connect to map our + // normalized set of commands to those supported by the target + // browser + + var command = cmd.toLowerCase(); + if(command == "hilitecolor" && !dojo.isMoz){ + command = "backcolor"; + } + + return command; + }, + + queryCommandAvailable: function(/*String*/command){ + // summary: + // Tests whether a command is supported by the host. Clients SHOULD check + // whether a command is supported before attempting to use it, behaviour + // for unsupported commands is undefined. + // command: The command to test for + var ie = 1; + var mozilla = 1 << 1; + var safari = 1 << 2; + var opera = 1 << 3; + var safari420 = 1 << 4; + + var gt420 = dojo.isSafari; + + function isSupportedBy(browsers){ + return { + ie: Boolean(browsers & ie), + mozilla: Boolean(browsers & mozilla), + safari: Boolean(browsers & safari), + safari420: Boolean(browsers & safari420), + opera: Boolean(browsers & opera) + } + } + + var supportedBy = null; + + switch(command.toLowerCase()){ + case "bold": case "italic": case "underline": + case "subscript": case "superscript": + case "fontname": case "fontsize": + case "forecolor": case "hilitecolor": + case "justifycenter": case "justifyfull": case "justifyleft": + case "justifyright": case "delete": case "selectall": case "toggledir": + supportedBy = isSupportedBy(mozilla | ie | safari | opera); + break; + + case "createlink": case "unlink": case "removeformat": + case "inserthorizontalrule": case "insertimage": + case "insertorderedlist": case "insertunorderedlist": + case "indent": case "outdent": case "formatblock": + case "inserthtml": case "undo": case "redo": case "strikethrough": + supportedBy = isSupportedBy(mozilla | ie | opera | safari420); + break; + + case "blockdirltr": case "blockdirrtl": + case "dirltr": case "dirrtl": + case "inlinedirltr": case "inlinedirrtl": + supportedBy = isSupportedBy(ie); + break; + case "cut": case "copy": case "paste": + supportedBy = isSupportedBy( ie | mozilla | safari420); + break; + + case "inserttable": + supportedBy = isSupportedBy(mozilla | ie); + break; + + case "insertcell": case "insertcol": case "insertrow": + case "deletecells": case "deletecols": case "deleterows": + case "mergecells": case "splitcell": + supportedBy = isSupportedBy(ie | mozilla); + break; + + default: return false; + } + + return (dojo.isIE && supportedBy.ie) || + (dojo.isMoz && supportedBy.mozilla) || + (dojo.isSafari && supportedBy.safari) || + (gt420 && supportedBy.safari420) || + (dojo.isOpera && supportedBy.opera); // Boolean return true if the command is supported, false otherwise + }, + + execCommand: function(/*String*/command, argument){ + // summary: Executes a command in the Rich Text area + // command: The command to execute + // argument: An optional argument to the command + var returnValue; + + //focus() is required for IE to work + //In addition, focus() makes sure after the execution of + //the command, the editor receives the focus as expected + this.focus(); + + command = this._normalizeCommand(command); + if(argument != undefined){ + if(command == "heading"){ + throw new Error("unimplemented"); + }else if((command == "formatblock") && dojo.isIE){ + argument = '<'+argument+'>'; + } + } + if(command == "inserthtml"){ + argument=this._preFilterContent(argument); + if(dojo.isIE){ + var insertRange = this.document.selection.createRange(); + if(this.document.selection.type.toUpperCase()=='CONTROL'){ + var n=insertRange.item(0); + while(insertRange.length){ + insertRange.remove(insertRange.item(0)); + } + n.outerHTML=argument; + }else{ + insertRange.pasteHTML(argument); + } + insertRange.select(); + //insertRange.collapse(true); + returnValue=true; + }else if(dojo.isMoz && !argument.length){ + //mozilla can not inserthtml an empty html to delete current selection + //so we delete the selection instead in this case + dojo.withGlobal(this.window,'remove',dijit._editor.selection); + returnValue=true; + }else{ + returnValue=this.document.execCommand(command, false, argument); + } + }else if( + (command == "unlink")&& + (this.queryCommandEnabled("unlink"))&& + (dojo.isMoz || dojo.isSafari) + ){ + // fix up unlink in Mozilla to unlink the link and not just the selection + + // grab selection + // Mozilla gets upset if we just store the range so we have to + // get the basic properties and recreate to save the selection + var selection = this.window.getSelection(); + // var selectionRange = selection.getRangeAt(0); + // var selectionStartContainer = selectionRange.startContainer; + // var selectionStartOffset = selectionRange.startOffset; + // var selectionEndContainer = selectionRange.endContainer; + // var selectionEndOffset = selectionRange.endOffset; + + // select our link and unlink + var a = dojo.withGlobal(this.window, "getAncestorElement",dijit._editor.selection, ['a']); + dojo.withGlobal(this.window, "selectElement", dijit._editor.selection, [a]); + + returnValue=this.document.execCommand("unlink", false, null); + }else if((command == "hilitecolor")&&(dojo.isMoz)){ +// // mozilla doesn't support hilitecolor properly when useCSS is +// // set to false (bugzilla #279330) + + this.document.execCommand("styleWithCSS", false, true); + returnValue = this.document.execCommand(command, false, argument); + this.document.execCommand("styleWithCSS", false, false); + + }else if((dojo.isIE)&&( (command == "backcolor")||(command == "forecolor") )){ + // Tested under IE 6 XP2, no problem here, comment out + // IE weirdly collapses ranges when we exec these commands, so prevent it +// var tr = this.document.selection.createRange(); + argument = arguments.length > 1 ? argument : null; + returnValue = this.document.execCommand(command, false, argument); + + // timeout is workaround for weird IE behavior were the text + // selection gets correctly re-created, but subsequent input + // apparently isn't bound to it +// setTimeout(function(){tr.select();}, 1); + }else{ + argument = arguments.length > 1 ? argument : null; +// if(dojo.isMoz){ +// this.document = this.iframe.contentWindow.document +// } + + if(argument || command!="createlink"){ + returnValue = this.document.execCommand(command, false, argument); + } + } + + this.onDisplayChanged(); + return returnValue; + }, + + queryCommandEnabled: function(/*String*/command){ + // summary: check whether a command is enabled or not + + if(this.disabled){ return false; } + command = this._normalizeCommand(command); + if(dojo.isMoz || dojo.isSafari){ + if(command == "unlink"){ // mozilla returns true always + // console.debug(dojo.withGlobal(this.window, "hasAncestorElement",dijit._editor.selection, ['a'])); + return dojo.withGlobal(this.window, "hasAncestorElement",dijit._editor.selection, ['a']); + }else if(command == "inserttable"){ + return true; + } + } + //see #4109 + if(dojo.isSafari){ + if(command == "copy"){ + command = "cut"; + }else if(command == "paste"){ + return true; + } + } + + // return this.document.queryCommandEnabled(command); + var elem = dojo.isIE ? this.document.selection.createRange() : this.document; + return elem.queryCommandEnabled(command); + }, + + queryCommandState: function(command){ + // summary: check the state of a given command + + if(this.disabled){ return false; } + command = this._normalizeCommand(command); + return this.document.queryCommandState(command); + }, + + queryCommandValue: function(command){ + // summary: check the value of a given command + + if(this.disabled){ return false; } + command = this._normalizeCommand(command); + if(dojo.isIE && command == "formatblock"){ + return this._local2NativeFormatNames[this.document.queryCommandValue(command)]; + } + return this.document.queryCommandValue(command); + }, + + // Misc. + + placeCursorAtStart: function(){ + // summary: + // place the cursor at the start of the editing area + this.focus(); + + //see comments in placeCursorAtEnd + var isvalid=false; + if(dojo.isMoz){ + var first=this.editNode.firstChild; + while(first){ + if(first.nodeType == 3){ + if(first.nodeValue.replace(/^\s+|\s+$/g, "").length>0){ + isvalid=true; + dojo.withGlobal(this.window, "selectElement", dijit._editor.selection, [first]); + break; + } + }else if(first.nodeType == 1){ + isvalid=true; + dojo.withGlobal(this.window, "selectElementChildren",dijit._editor.selection, [first]); + break; + } + first = first.nextSibling; + } + }else{ + isvalid=true; + dojo.withGlobal(this.window, "selectElementChildren",dijit._editor.selection, [this.editNode]); + } + if(isvalid){ + dojo.withGlobal(this.window, "collapse", dijit._editor.selection, [true]); + } + }, + + placeCursorAtEnd: function(){ + // summary: + // place the cursor at the end of the editing area + this.focus(); + + //In mozilla, if last child is not a text node, we have to use selectElementChildren on this.editNode.lastChild + //otherwise the cursor would be placed at the end of the closing tag of this.editNode.lastChild + var isvalid=false; + if(dojo.isMoz){ + var last=this.editNode.lastChild; + while(last){ + if(last.nodeType == 3){ + if(last.nodeValue.replace(/^\s+|\s+$/g, "").length>0){ + isvalid=true; + dojo.withGlobal(this.window, "selectElement",dijit._editor.selection, [last]); + break; + } + }else if(last.nodeType == 1){ + isvalid=true; + if(last.lastChild){ + dojo.withGlobal(this.window, "selectElement",dijit._editor.selection, [last.lastChild]); + }else{ + dojo.withGlobal(this.window, "selectElement",dijit._editor.selection, [last]); + } + break; + } + last = last.previousSibling; + } + }else{ + isvalid=true; + dojo.withGlobal(this.window, "selectElementChildren",dijit._editor.selection, [this.editNode]); + } + if(isvalid){ + dojo.withGlobal(this.window, "collapse", dijit._editor.selection, [false]); + } + }, + + getValue: function(/*Boolean?*/nonDestructive){ + // summary: + // return the current content of the editing area (post filters are applied) + if(this.textarea){ + if(this.isClosed || !this.isLoaded){ + return this.textarea.value; + } + } + + return this._postFilterContent(null, nonDestructive); + }, + + setValue: function(/*String*/html){ + // summary: + // this function set the content. No undo history is preserved + + if(!this.isLoaded){ + // try again after the editor is finished loading + this.onLoadDeferred.addCallback(dojo.hitch(this, function(){ + this.setValue(html); + })); + return; + } + + if(this.textarea && (this.isClosed || !this.isLoaded)){ + this.textarea.value=html; + }else{ + html = this._preFilterContent(html); + var node = this.isClosed ? this.domNode : this.editNode; + node.innerHTML = html; + this._preDomFilterContent(node); + } + + this.onDisplayChanged(); + }, + + replaceValue: function(/*String*/html){ + // summary: + // this function set the content while trying to maintain the undo stack + // (now only works fine with Moz, this is identical to setValue in all + // other browsers) + if(this.isClosed){ + this.setValue(html); + }else if(this.window && this.window.getSelection && !dojo.isMoz){ // Safari + // look ma! it's a totally f'd browser! + this.setValue(html); + }else if(this.window && this.window.getSelection){ // Moz + html = this._preFilterContent(html); + this.execCommand("selectall"); + if(dojo.isMoz && !html){ html = " " } + this.execCommand("inserthtml", html); + this._preDomFilterContent(this.editNode); + }else if(this.document && this.document.selection){//IE + //In IE, when the first element is not a text node, say + //an <a> tag, when replacing the content of the editing + //area, the <a> tag will be around all the content + //so for now, use setValue for IE too + this.setValue(html); + } + }, + + _preFilterContent: function(/*String*/html){ + // summary: + // filter the input before setting the content of the editing area + var ec = html; + dojo.forEach(this.contentPreFilters, function(ef){ if(ef){ ec = ef(ec); } }); + return ec; + }, + _preDomFilterContent: function(/*DomNode*/dom){ + // summary: + // filter the input + dom = dom || this.editNode; + dojo.forEach(this.contentDomPreFilters, function(ef){ + if(ef && dojo.isFunction(ef)){ + ef(dom); + } + }, this); + }, + + _postFilterContent: function(/*DomNode|DomNode[]|String?*/dom,/*Boolean?*/nonDestructive){ + // summary: + // filter the output after getting the content of the editing area + var ec; + if(!dojo.isString(dom)){ + dom = dom || this.editNode; + if(this.contentDomPostFilters.length){ + if(nonDestructive && dom['cloneNode']){ + dom = dom.cloneNode(true); + } + dojo.forEach(this.contentDomPostFilters, function(ef){ + dom = ef(dom); + }); + } + ec = dijit._editor.getChildrenHtml(dom); + }else{ + ec = dom; + } + + if(!ec.replace(/^(?:\s|\xA0)+/g, "").replace(/(?:\s|\xA0)+$/g,"").length){ ec = ""; } + + // if(dojo.isIE){ + // //removing appended <P> </P> for IE + // ec = ec.replace(/(?:<p> </p>[\n\r]*)+$/i,""); + // } + dojo.forEach(this.contentPostFilters, function(ef){ + ec = ef(ec); + }); + + return ec; + }, + + _saveContent: function(/*Event*/e){ + // summary: + // Saves the content in an onunload event if the editor has not been closed + var saveTextarea = dojo.byId(dijit._scopeName + "._editor.RichText.savedContent"); + saveTextarea.value += this._SEPARATOR + this.name + ":" + this.getValue(); + }, + + escapeXml: function(/*String*/str, /*Boolean*/noSingleQuotes){ + dojo.deprecated('dijit.Editor::escapeXml is deprecated','use dijit._editor.escapeXml instead', 2); + return dijit._editor.escapeXml(str,noSingleQuotes); + }, + + getNodeHtml: function(/* DomNode */node){ + dojo.deprecated('dijit.Editor::getNodeHtml is deprecated','use dijit._editor.getNodeHtml instead', 2); + return dijit._editor.getNodeHtml(node); + }, + + getNodeChildrenHtml: function(/* DomNode */dom){ + dojo.deprecated('dijit.Editor::getNodeChildrenHtml is deprecated','use dijit._editor.getChildrenHtml instead', 2); + return dijit._editor.getChildrenHtml(dom); + }, + + close: function(/*Boolean*/save, /*Boolean*/force){ + // summary: + // Kills the editor and optionally writes back the modified contents to the + // element from which it originated. + // save: + // Whether or not to save the changes. If false, the changes are discarded. + // force: + if(this.isClosed){return false; } + + if(!arguments.length){ save = true; } + this._content = this.getValue(); + var changed = (this.savedContent != this._content); + + // line height is squashed for iframes + // FIXME: why was this here? if(this.iframe){ this.domNode.style.lineHeight = null; } + + if(this.interval){ clearInterval(this.interval); } + + if(this.textarea){ + with(this.textarea.style){ + position = ""; + left = top = ""; + if(dojo.isIE){ + overflow = this.__overflow; + this.__overflow = null; + } + } + this.textarea.value = save ? this._content : this.savedContent; + dojo._destroyElement(this.domNode); + this.domNode = this.textarea; + }else{ +// if(save){ + //why we treat moz differently? comment out to fix #1061 +// if(dojo.isMoz){ +// var nc = dojo.doc.createElement("span"); +// this.domNode.appendChild(nc); +// nc.innerHTML = this.editNode.innerHTML; +// }else{ +// this.domNode.innerHTML = this._content; +// } +// } + this.domNode.innerHTML = save ? this._content : this.savedContent; + } + + dojo.removeClass(this.domNode, "RichTextEditable"); + this.isClosed = true; + this.isLoaded = false; + // FIXME: is this always the right thing to do? + delete this.editNode; + + if(this.window && this.window._frameElement){ + this.window._frameElement = null; + } + + this.window = null; + this.document = null; + this.editingArea = null; + this.editorObject = null; + + return changed; // Boolean: whether the content has been modified + }, + + destroyRendering: function(){ + // summary: stub + }, + + destroy: function(){ + this.destroyRendering(); + if(!this.isClosed){ this.close(false); } + this.inherited("destroy",arguments); + //dijit._editor.RichText.superclass.destroy.call(this); + }, + + _removeMozBogus: function(/* String */ html){ + return html.replace(/\stype="_moz"/gi, '').replace(/\s_moz_dirty=""/gi, ''); // String + }, + _removeSafariBogus: function(/* String */ html){ + return html.replace(/\sclass="webkit-block-placeholder"/gi, ''); // String + }, + _fixContentForMoz: function(/* String */ html){ + // summary: + // Moz can not handle strong/em tags correctly, convert them to b/i + return html.replace(/<(\/)?strong([ \>])/gi, '<$1b$2') + .replace(/<(\/)?em([ \>])/gi, '<$1i$2' ); // String + }, + + _srcInImgRegex : /(?:(<img(?=\s).*?\ssrc=)("|')(.*?)\2)|(?:(<img\s.*?src=)([^"'][^ >]+))/gi , + _hrefInARegex : /(?:(<a(?=\s).*?\shref=)("|')(.*?)\2)|(?:(<a\s.*?href=)([^"'][^ >]+))/gi , + + _preFixUrlAttributes: function(/* String */ html){ + return html.replace(this._hrefInARegex, '$1$4$2$3$5$2 _djrealurl=$2$3$5$2') + .replace(this._srcInImgRegex, '$1$4$2$3$5$2 _djrealurl=$2$3$5$2'); // String + } +}); + +} |