| /* |
| Copyright (c) 2004-2006, The Dojo Foundation |
| All Rights Reserved. |
| |
| Licensed under the Academic Free License version 2.1 or above OR the |
| modified BSD license. For more information on Dojo licensing, see: |
| |
| http://dojotoolkit.org/community/licensing.shtml |
| */ |
| |
| /* -*- tab-width: 4 -*- */ |
| dojo.provide("dojo.widget.RichText"); |
| |
| dojo.require("dojo.widget.*"); |
| dojo.require("dojo.html.*"); |
| dojo.require("dojo.html.layout"); |
| dojo.require("dojo.html.selection"); |
| dojo.require("dojo.event.*"); |
| dojo.require("dojo.string.extras"); |
| dojo.require("dojo.uri.Uri"); |
| dojo.require("dojo.Deferred"); |
| |
| // used to save content |
| if(dojo.hostenv.post_load_){ |
| (function(){ |
| var savetextarea = dojo.doc().createElement('textarea'); |
| savetextarea.id = "dojo.widget.RichText.savedContent"; |
| savetextarea.style = "display:none;position:absolute;top:-100px;left:-100px;height:3px;width:3px;overflow:hidden;"; |
| dojo.body().appendChild(savetextarea); |
| })(); |
| }else{ |
| //dojo.body() is not available before onLoad is fired |
| try { |
| dojo.doc().write('<textarea id="dojo.widget.RichText.savedContent" ' + |
| 'style="display:none;position:absolute;top:-100px;left:-100px;height:3px;width:3px;overflow:hidden;"></textarea>'); |
| }catch(e){ } |
| } |
| |
| dojo.widget.defineWidget( |
| "dojo.widget.RichText", |
| dojo.widget.HtmlWidget, |
| function(){ |
| // summary: |
| // dojo.widget.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 |
| this.contentPreFilters = []; |
| |
| // contentPostFilters: Array |
| // post content filter function register array |
| this.contentPostFilters = []; |
| |
| // contentDomPreFilters: Array |
| // pre content dom filter function register array |
| this.contentDomPreFilters = []; |
| |
| // contentDomPostFilters: Array |
| // post content dom filter function register array |
| this.contentDomPostFilters = []; |
| |
| // editingAreaStyleSheets: Array |
| // array to store all the stylesheets applied to the editing area |
| this.editingAreaStyleSheets=[]; |
| |
| if(dojo.render.html.moz){ |
| this.contentPreFilters.push(this._fixContentForMoz); |
| } |
| |
| this._keyHandlers = {}; |
| |
| if(dojo.Deferred){ |
| 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, |
| |
| // saveName: 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. |
| saveName: "", |
| |
| // 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 |
| height: "", |
| |
| // minHeight: String |
| // The minimum height that the editor should have |
| minHeight: "1em", |
| |
| // isClosed: Boolean |
| isClosed: true, |
| |
| // isLoaded: Boolean |
| isLoaded: false, |
| |
| // useActiveX: Boolean |
| // whether to use the active-x object in IE |
| useActiveX: false, |
| |
| // relativeImageUrls: Boolean |
| // whether to use relative URLs for images - if this is enabled |
| // images will be given absolute URLs when inside the editor but |
| // will be changed to use relative URLs (to the current page) on save |
| relativeImageUrls: false, |
| |
| // _SEPARATOR: String |
| // used to concat contents from multiple textareas into a single string |
| _SEPARATOR: "@@**%%__RICHTEXTBOUNDRY__%%**@@", |
| |
| // onLoadDeferred: dojo.Deferred |
| // deferred that can be used to connect to the onLoad function. This |
| // will only be set if dojo.Deferred is required |
| onLoadDeferred: null, |
| |
| /* Init |
| *******/ |
| |
| fillInTemplate: function(){ |
| // summary: see dojo.widget.DomWidget |
| dojo.event.topic.publish("dojo.widget.RichText::init", this); |
| this.open(); |
| |
| |
| // backwards compatibility, needs to be removed |
| dojo.event.connect(this, "onKeyPressed", this, "afterKeyPress"); |
| dojo.event.connect(this, "onKeyPress", this, "keyPress"); |
| dojo.event.connect(this, "onKeyDown", this, "keyDown"); |
| dojo.event.connect(this, "onKeyUp", this, "keyUp"); |
| |
| this.setupDefaultShortcuts(); |
| }, |
| |
| setupDefaultShortcuts: function(){ |
| // summary: add some default key handlers |
| // description: |
| // Overwrite this to setup your own handlers. The default |
| // implementation does not use Editor2 commands, but directly |
| // executes the builtin commands within the underlying browser |
| // support. |
| var ctrl = this.KEY_CTRL; |
| var exec = function (cmd, arg) { |
| return arguments.length == 1 ? function () { this.execCommand(cmd); } : |
| function () { this.execCommand(cmd, arg); } |
| } |
| this.addKeyHandler("b", ctrl, exec("bold")); |
| this.addKeyHandler("i", ctrl, exec("italic")); |
| this.addKeyHandler("u", ctrl, exec("underline")); |
| this.addKeyHandler("a", ctrl, exec("selectall")); |
| this.addKeyHandler("s", ctrl, function () { this.save(true); }); |
| |
| this.addKeyHandler("1", ctrl, exec("formatblock", "h1")); |
| this.addKeyHandler("2", ctrl, exec("formatblock", "h2")); |
| this.addKeyHandler("3", ctrl, exec("formatblock", "h3")); |
| this.addKeyHandler("4", ctrl, exec("formatblock", "h4")); |
| |
| this.addKeyHandler("\\", ctrl, exec("insertunorderedlist")); |
| if(!dojo.render.html.ie){ |
| this.addKeyHandler("Z", ctrl, exec("redo")); |
| } |
| }, |
| |
| // events: Array |
| // events which should be connected to the underlying editing area |
| events: ["onBlur", "onFocus", "onKeyPress", "onKeyDown", "onKeyUp", "onClick"], |
| |
| /** |
| * Transforms the node referenced in this.domNode into a rich text editing |
| * node. This can result in the creation and replacement with an <iframe> if |
| * designMode is used, an <object> and active-x component if inside of IE or |
| * a reguler element if contentEditable is available. |
| */ |
| open: function (/*DomNode, optional*/element) { |
| // summary: |
| // Transforms the node referenced in this.domNode into a rich text editing |
| // node. This can result in the creation and replacement with an <iframe> if |
| // designMode is used, an <object> and active-x component if inside of IE or |
| // a reguler element if contentEditable is available. |
| |
| if(this.onLoadDeferred.fired >= 0){ |
| this.onLoadDeferred = new dojo.Deferred(); |
| } |
| |
| var h = dojo.render.html; |
| if (!this.isClosed) { this.close(); } |
| dojo.event.topic.publish("dojo.widget.RichText::open", this); |
| |
| this._content = ""; |
| if((arguments.length == 1)&&(element["nodeName"])){ this.domNode = element; } // else unchanged |
| |
| if( (this.domNode["nodeName"])&& |
| (this.domNode.nodeName.toLowerCase() == "textarea")){ |
| this.textarea = this.domNode; |
| var html = dojo.string.trim(this.textarea.value); |
| this.domNode = dojo.doc().createElement("div"); |
| dojo.html.copyStyle(this.domNode, this.textarea); |
| var tmpFunc = dojo.lang.hitch(this, function(){ |
| //some browsers refuse to submit display=none textarea, so |
| //move the textarea out of screen instead |
| with(this.textarea.style){ |
| display = "block"; |
| position = "absolute"; |
| left = top = "-1000px"; |
| |
| if(h.ie){ //nasty IE bug: abnormal formatting if overflow is not hidden |
| this.__overflow = overflow; |
| overflow = "hidden"; |
| } |
| } |
| }); |
| if(h.ie){ |
| setTimeout(tmpFunc, 10); |
| }else{ |
| tmpFunc(); |
| } |
| if(!h.safari){ |
| // FIXME: VERY STRANGE safari 2.0.4 behavior here caused by |
| // moving the textarea. Often crashed the browser!!! Seems |
| // fixed on webkit nightlies. |
| dojo.html.insertBefore(this.domNode, this.textarea); |
| } |
| // this.domNode.innerHTML = html; |
| |
| if(this.textarea.form){ |
| dojo.event.connect('before', this.textarea.form, "onsubmit", |
| // FIXME: should we be calling close() here instead? |
| dojo.lang.hitch(this, function(){ |
| this.textarea.value = this.getEditorContent(); |
| }) |
| ); |
| } |
| |
| // dojo plucks our original domNode from the document so we need |
| // to go back and put ourselves back in |
| var editor = this; |
| dojo.event.connect(this, "postCreate", function (){ |
| dojo.html.insertAfter(editor.textarea, editor.domNode); |
| }); |
| }else{ |
| var html = this._preFilterContent(dojo.string.trim(this.domNode.innerHTML)); |
| } |
| if(html == ""){ html = " "; } |
| var content = dojo.html.getContentBox(this.domNode); |
| this._oldHeight = content.height; |
| this._oldWidth = content.width; |
| |
| this._firstChildContributingMargin = this._getContributingMargin(this.domNode, "top"); |
| this._lastChildContributingMargin = this._getContributingMargin(this.domNode, "bottom"); |
| |
| this.savedContent = this.domNode.innerHTML; |
| this.domNode.innerHTML = ''; |
| |
| this.editingArea = dojo.doc().createElement("div"); |
| this.domNode.appendChild(this.editingArea); |
| |
| // 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>"; |
| } |
| |
| if(this.saveName != ""){ |
| var saveTextarea = dojo.doc().getElementById("dojo.widget.RichText.savedContent"); |
| if (saveTextarea.value != "") { |
| var datas = saveTextarea.value.split(this._SEPARATOR); |
| for (var i = 0; i < datas.length; i++) { |
| var data = datas[i].split(":"); |
| if (data[0] == this.saveName) { |
| html = data[1]; |
| datas.splice(i, 1); |
| break; |
| } |
| } |
| } |
| dojo.event.connect("before", window, "onunload", this, "_saveContent"); |
| // dojo.event.connect(window, "onunload", this, "_saveContent"); |
| } |
| |
| if(h.ie70 && this.useActiveX){ |
| dojo.debug("activeX in ie70 is not currently supported, useActiveX is ignored for now."); |
| this.useActiveX = false; |
| } |
| // Safari's selections go all out of whack if we do it inline, |
| // so for now IE is our only hero |
| //if (typeof document.body.contentEditable != "undefined") { |
| if(this.useActiveX && h.ie){ // active-x |
| var self = this; |
| //if call _drawObject directly here, textarea replacement |
| //won't work: no content is shown. However, add a delay |
| //can workaround this. No clue why. |
| setTimeout(function(){self._drawObject(html);}, 0); |
| }else if(h.ie || this._safariIsLeopard() || h.opera){ // contentEditable, easy |
| this.iframe = dojo.doc().createElement( 'iframe' ) ; |
| this.iframe.src = 'javascript:void(0)'; |
| this.editorObject = this.iframe; |
| with(this.iframe.style){ |
| border = '0'; |
| width = "100%"; |
| } |
| this.iframe.frameBorder = 0; |
| this.editingArea.appendChild(this.iframe) |
| this.window = this.iframe.contentWindow; |
| this.document = this.window.document; |
| this.document.open(); |
| this.document.write("<html><head><style>body{margin:0;padding:0;border:0;overflow:hidden;}</style></head><body><div></div></body></html>"); |
| this.document.close(); |
| this.editNode = this.document.body.firstChild;//document.createElement("div"); |
| this.editNode.contentEditable = true; |
| with (this.iframe.style) { |
| if(h.ie70){ |
| if(this.height){ |
| height = this.height; |
| } |
| if(this.minHeight){ |
| minHeight = this.minHeight; |
| } |
| }else{ |
| height = this.height ? this.height : this.minHeight; |
| } |
| } |
| |
| // FIXME: setting contentEditable on switches this element to |
| // IE's hasLayout mode, triggering weird margin collapsing |
| // behavior. It's particularly bad if the element you're editing |
| // contains childnodes that don't have margin: defined in local |
| // css rules. It would be nice if it was possible to hack around |
| // this. Sadly _firstChildContributingMargin and |
| // _lastChildContributingMargin don't work on IE unless all |
| // elements have margins set in CSS :-( |
| |
| //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 = ""; |
| for(var i in formats){ |
| if(formats[i].charAt(1) != 'l'){ |
| localhtml += "<"+formats[i]+"><span>content</span></"+formats[i]+">"; |
| }else{ |
| localhtml += "<"+formats[i]+"><li>content</li></"+formats[i]+">"; |
| } |
| } |
| //queryCommandValue returns empty if we hide editNode, so move it out of screen temporary |
| with(this.editNode.style){ |
| position = "absolute"; |
| left = "-2000px"; |
| top = "-2000px"; |
| } |
| this.editNode.innerHTML = localhtml; |
| var node = this.editNode.firstChild; |
| while(node){ |
| dojo.withGlobal(this.window, "selectElement", dojo.html.selection, [node.firstChild]); |
| var nativename = node.tagName.toLowerCase(); |
| this._local2NativeFormatNames[nativename] = this.queryCommandValue("formatblock"); |
| // dojo.debug([nativename,this._local2NativeFormatNames[nativename]]); |
| this._native2LocalFormatNames[this._local2NativeFormatNames[nativename]] = nativename; |
| node = node.nextSibling; |
| } |
| with(this.editNode.style){ |
| position = ""; |
| left = ""; |
| top = ""; |
| } |
| |
| this.editNode.innerHTML = html; |
| if(this.height){ this.document.body.style.overflowY="scroll"; } |
| |
| dojo.lang.forEach(this.events, function(e){ |
| dojo.event.connect(this.editNode, e.toLowerCase(), this, e); |
| }, this); |
| |
| this.onLoad(); |
| } else { // designMode in iframe |
| this._drawIframe(html); |
| this.editorObject = this.iframe; |
| } |
| |
| // TODO: this is a guess at the default line-height, kinda works |
| if (this.domNode.nodeName == "LI") { this.domNode.lastChild.style.marginTop = "-1.2em"; } |
| dojo.html.addClass(this.domNode, "RichTextEditable"); |
| |
| this.isClosed = false; |
| }, |
| |
| _hasCollapseableMargin: function(/*DomNode*/element, /*String*/side) { |
| // summary: |
| // check if an element has padding or borders on the given side |
| // which would prevent it from collapsing margins |
| if (dojo.html.getPixelValue(element, |
| 'border-'+side+'-width', |
| false)) { |
| return false; |
| } else if (dojo.html.getPixelValue(element, |
| 'padding-'+side, |
| false)) { |
| return false; |
| } else { |
| return true; |
| } |
| }, |
| |
| _getContributingMargin: function(/*DomNode*/element, /*String*/topOrBottom) { |
| // summary: |
| // calculate how much margin this element and its first or last |
| // child are contributing to the total margin between this element |
| // and the adjacent node. CSS border collapsing makes this |
| // necessary. |
| |
| if (topOrBottom == "top") { |
| var siblingAttr = "previousSibling"; |
| var childSiblingAttr = "nextSibling"; |
| var childAttr = "firstChild"; |
| var marginProp = "margin-top"; |
| var siblingMarginProp = "margin-bottom"; |
| } else { |
| var siblingAttr = "nextSibling"; |
| var childSiblingAttr = "previousSibling"; |
| var childAttr = "lastChild"; |
| var marginProp = "margin-bottom"; |
| var siblingMarginProp = "margin-top"; |
| } |
| |
| var elementMargin = dojo.html.getPixelValue(element, marginProp, false); |
| |
| function isSignificantNode(element) { |
| // see if an node is significant in the current context |
| // for calulating margins |
| return !(element.nodeType==3 && dojo.string.isBlank(element.data)) |
| && dojo.html.getStyle(element, "display") != "none" |
| && !dojo.html.isPositionAbsolute(element); |
| } |
| |
| // walk throuh first/last children to find total collapsed margin size |
| var childMargin = 0; |
| var child = element[childAttr]; |
| while (child) { |
| // skip over insignificant elements (whitespace, etc) |
| while ((!isSignificantNode(child)) && child[childSiblingAttr]) { |
| child = child[childSiblingAttr]; |
| } |
| |
| childMargin = Math.max(childMargin, dojo.html.getPixelValue(child, marginProp, false)); |
| // stop if we hit a bordered/padded element |
| if (!this._hasCollapseableMargin(child, topOrBottom)) break; |
| child = child[childAttr]; |
| } |
| |
| // if this element has a border, return full child margin immediately |
| // as there won't be any margin collapsing |
| if (!this._hasCollapseableMargin(element, topOrBottom)){ return parseInt(childMargin); } |
| |
| // find margin supplied by nearest sibling |
| var contextMargin = 0; |
| var sibling = element[siblingAttr]; |
| while (sibling) { |
| if (isSignificantNode(sibling)) { |
| contextMargin = dojo.html.getPixelValue(sibling, |
| siblingMarginProp, |
| false); |
| break; |
| } |
| sibling = sibling[siblingAttr]; |
| } |
| if (!sibling) { // no sibling, look at parent's margin instead |
| contextMargin = dojo.html.getPixelValue(element.parentNode, |
| marginProp, false); |
| } |
| |
| if (childMargin > elementMargin) { |
| return parseInt(Math.max((childMargin-elementMargin)-contextMargin, 0)); |
| } else { |
| return 0; |
| } |
| |
| }, |
| |
| _drawIframe: function (/*String*/html){ |
| // summary: |
| // Draws an iFrame using the existing one if one exists. |
| // Used by Mozilla, Safari, and Opera |
| |
| // detect firefox < 1.5, which has some iframe loading issues |
| var oldMoz = Boolean(dojo.render.html.moz && ( |
| typeof window.XML == 'undefined')) |
| |
| if(!this.iframe){ |
| var currentDomain = (new dojo.uri.Uri(dojo.doc().location)).host; |
| this.iframe = dojo.doc().createElement("iframe"); |
| // dojo.body().appendChild(this.iframe); |
| with(this.iframe){ |
| style.border = "none"; |
| style.lineHeight = "0"; // squash line height |
| style.verticalAlign = "bottom"; |
| scrolling = this.height ? "auto" : "no"; |
| } |
| } |
| // opera likes this to be outside the with block |
| this.iframe.src = dojo.uri.dojoUri("src/widget/templates/richtextframe.html") + ((dojo.doc().domain != currentDomain) ? ("#"+dojo.doc().domain) : ""); |
| this.iframe.width = this.inheritWidth ? this._oldWidth : "100%"; |
| if(this.height){ |
| this.iframe.style.height = this.height; |
| }else{ |
| var height = this._oldHeight; |
| if(this._hasCollapseableMargin(this.domNode, 'top')){ |
| height += this._firstChildContributingMargin; |
| } |
| if(this._hasCollapseableMargin(this.domNode, 'bottom')){ |
| height += this._lastChildContributingMargin; |
| } |
| this.iframe.height = height; |
| } |
| |
| var tmpContent = dojo.doc().createElement('div'); |
| tmpContent.innerHTML = html; |
| //append tmpContent to under the current domNode so that the margin |
| //calculation below is correct |
| this.editingArea.appendChild(tmpContent); |
| |
| // make relative image urls absolute |
| if(this.relativeImageUrls){ |
| var imgs = tmpContent.getElementsByTagName('img'); |
| for(var i=0; i<imgs.length; i++){ |
| imgs[i].src = (new dojo.uri.Uri(dojo.global().location, imgs[i].src)).toString(); |
| } |
| html = tmpContent.innerHTML; |
| } |
| |
| // fix margins on tmpContent |
| var firstChild = dojo.html.firstElement(tmpContent); |
| var lastChild = dojo.html.lastElement(tmpContent); |
| if(firstChild){ |
| firstChild.style.marginTop = this._firstChildContributingMargin+"px"; |
| } |
| if(lastChild){ |
| lastChild.style.marginBottom = this._lastChildContributingMargin+"px"; |
| } |
| //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); |
| if(dojo.render.html.safari){ |
| this.iframe.src = this.iframe.src; |
| } |
| |
| var _iframeInitialized = false; |
| |
| // now we wait for onload. Janky hack! |
| var ifrFunc = dojo.lang.hitch(this, function(){ |
| if(!_iframeInitialized){ |
| _iframeInitialized = true; |
| }else{ return; } |
| if(!this.editNode){ |
| 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; |
| } |
| |
| // curry the getStyle function |
| var getStyle = (function (domNode) { return function (style) { |
| return dojo.html.getStyle(domNode, style); |
| }; })(this.domNode); |
| |
| var font = |
| getStyle('font-weight') + " " + |
| getStyle('font-size') + " " + |
| getStyle('font-family'); |
| |
| // 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 = "1.0"; |
| var lineHeightStyle = dojo.html.getUnitValue(this.domNode, 'line-height'); |
| if (lineHeightStyle.value && lineHeightStyle.units=="") { |
| lineHeight = lineHeightStyle.value; |
| } |
| |
| dojo.html.insertCssText( |
| 'body,html{background:transparent;padding:0;margin:0;}' + |
| // TODO: left positioning will case contents to disappear out of view |
| // if it gets too wide for the visible area |
| 'body{top:0;left:0;right:0;' + |
| (((this.height)||(dojo.render.html.opera)) ? '' : 'position:fixed;') + |
| 'font:' + font + ';' + |
| 'min-height:' + this.minHeight + ';' + |
| 'line-height:' + lineHeight + '}' + |
| 'p{margin: 1em 0 !important;}' + |
| 'body > *:first-child{padding-top:0 !important;margin-top:' + this._firstChildContributingMargin + 'px !important;}' + // FIXME: test firstChild nodeType |
| 'body > *:last-child{padding-bottom:0 !important;margin-bottom:' + this._lastChildContributingMargin + 'px !important;}' + |
| 'li > ul:-moz-first-node, li > ol:-moz-first-node{padding-top:1.2em;}\n' + |
| 'li{min-height:1.2em;}' + |
| //' p,ul,li { padding-top: 0; padding-bottom: 0; margin-top:0; margin-bottom: 0; }\n' + |
| '', this.document); |
| |
| dojo.html.removeNode(tmpContent); |
| this.document.body.innerHTML = html; |
| if(oldMoz||dojo.render.html.safari){ |
| this.document.designMode = "on"; |
| } |
| this.onLoad(); |
| }else{ |
| dojo.html.removeNode(tmpContent); |
| this.editNode.innerHTML = html; |
| this.onDisplayChanged(); |
| } |
| }); |
| |
| if(this.editNode){ |
| ifrFunc(); // iframe already exists, just set content |
| }else if(dojo.render.html.moz){ |
| // FIXME: if we put this on a delay, we get a height of 20px. |
| // Otherwise we get the correctly specified minHeight value. |
| this.iframe.onload = function(){ |
| setTimeout(ifrFunc, 250); |
| } |
| }else{ // new mozillas, opera, safari |
| this.iframe.onload = 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 = []; |
| |
| if(files.length>0){ |
| for(var i=0;i<files.length;i++){ |
| var url = files[i]; |
| if(url){ |
| this.addStyleSheet(dojo.uri.dojoUri(url)); |
| } |
| } |
| } |
| }, |
| |
| addStyleSheet: function(/*dojo.uri.Uri*/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(dojo.lang.find(this.editingAreaStyleSheets, url) > -1){ |
| dojo.debug("dojo.widget.RichText.addStyleSheet: Style sheet "+url+" is already applied to the editing area!"); |
| return; |
| } |
| |
| //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.uri.Uri(dojo.global().location, url)).toString(); |
| } |
| |
| 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.uri.Uri*/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.uri.Uri(dojo.global().location, url)).toString(); |
| } |
| var index = dojo.lang.find(this.editingAreaStyleSheets, url); |
| if(index == -1){ |
| dojo.debug("dojo.widget.RichText.removeStyleSheet: Style sheet "+url+" is not applied to the editing area so it can not be removed!"); |
| return; |
| } |
| delete this.editingAreaStyleSheets[index]; |
| |
| var links = this.document.getElementsByTagName("link"); |
| for(var i=0;i<links.length;i++){ |
| if(links[i].href == url){ |
| if(dojo.render.html.ie){//we need to empty the href first, to get IE to remove the rendered styles |
| links[i].href=""; |
| } |
| dojo.html.removeNode(links[i]); |
| break; |
| } |
| } |
| }, |
| |
| _drawObject: function (/*String*/html) { |
| // summary: |
| // Draws an active x object, used by IE |
| this.object = dojo.html.createExternalElement(dojo.doc(), "object"); |
| |
| with (this.object) { |
| classid = "clsid:2D360201-FFF5-11D1-8D03-00A0C959BC0A"; |
| width = this.inheritWidth ? this._oldWidth : "100%"; |
| style.height = this.height ? this.height : (this._oldHeight+"px"); |
| Scrollbars = this.height ? true : false; |
| Appearance = this._activeX.appearance.flat; |
| } |
| this.editorObject = this.object; |
| this.editingArea.appendChild(this.object); |
| |
| this.object.attachEvent("DocumentComplete", dojo.lang.hitch(this, "onLoad")); |
| //DisplayChanged is fired too often even no change is made, so we ignore it |
| //and call onDisplayChanged manually in execCommand instead |
| // this.object.attachEvent("DisplayChanged", dojo.lang.hitch(this, "onDisplayChanged")); |
| |
| dojo.lang.forEach(this.events, function(e){ |
| this.object.attachEvent(e.toLowerCase(), dojo.lang.hitch(this, e)); |
| }, this); |
| |
| this.object.DocumentHTML = '<!doctype HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">' + |
| '<html><title></title>' + |
| '<style type="text/css">' + |
| ' body,html { padding: 0; margin: 0; }' + //font: ' + font + '; }' + |
| (this.height ? '' : ' body, { overflow: hidden; }') + |
| '</style>' + |
| //'<base href="' + dojo.global().location + '">' + |
| '<body><div>' + html + '<div></body></html>'; |
| |
| this._cacheLocalBlockFormatNames(); |
| }, |
| |
| //static cache variables shared among all instance of this class |
| _local2NativeFormatNames: {}, |
| _native2LocalFormatNames: {}, |
| //in IE, names for blockformat is locale dependent, so we cache the values here |
| //we use activeX to obtain the list, if success or the names are already cached, |
| //return true |
| _cacheLocalBlockFormatNames: function(){ |
| // summary: |
| // in IE, names for blockformat is locale dependent, so we cache the values here |
| // we use activeX to obtain the list, if success or the names are already cached, |
| // return true |
| if(!this._native2LocalFormatNames['p']){ |
| var obj = this.object; |
| var error = false; |
| if(!obj){ |
| //create obj temporarily |
| try{ |
| obj = dojo.html.createExternalElement(dojo.doc(), "object"); |
| obj.classid = "clsid:2D360201-FFF5-11D1-8D03-00A0C959BC0A"; |
| dojo.body().appendChild(obj); |
| obj.DocumentHTML = "<html><head></head><body></body></html>"; |
| }catch(e){ error = true; } |
| } |
| try{ |
| var oNamesParm = new ActiveXObject("DEGetBlockFmtNamesParam.DEGetBlockFmtNamesParam"); |
| obj.ExecCommand(this._activeX.command['getblockformatnames'], 0, oNamesParm); |
| var vbNamesArray = new VBArray(oNamesParm.Names); |
| var localFormats = vbNamesArray.toArray(); |
| var nativeFormats = ['p', 'pre', 'address', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'ul', '', '', '','','div']; |
| for(var i=0;i<nativeFormats.length;++i){ |
| if(nativeFormats[i].length>0){ |
| this._local2NativeFormatNames[localFormats[i]] = nativeFormats[i]; |
| this._native2LocalFormatNames[nativeFormats[i]] = localFormats[i]; |
| } |
| } |
| }catch(e){ error = true; } |
| if(obj && !this.object){ |
| //delete the temporary obj |
| dojo.body().removeChild(obj); |
| } |
| } |
| return !error; |
| }, |
| /* Event handlers |
| *****************/ |
| |
| _isResized: function(){ return false; }, |
| |
| onLoad: function(e){ |
| // summary: handler after the content of the document finishes loading |
| this.isLoaded = true; |
| if (this.object){ |
| this.document = this.object.DOM; |
| this.window = this.document.parentWindow; |
| this.editNode = this.document.body.firstChild; |
| this.editingArea.style.height = this.height ? this.height : this.minHeight; |
| if(!this.height){ |
| this.connect(this, "onDisplayChanged", "_updateHeight"); |
| } |
| //pretend the object as an iframe, so that the context menu for the |
| //editor can be placed correctly when shown |
| this.window._frameElement = this.object; |
| }else if (this.iframe && !dojo.render.html.ie){ |
| this.editNode = this.document.body; |
| if(!this.height){ |
| this.connect(this, "onDisplayChanged", "_updateHeight"); |
| } |
| |
| try { // sanity check for Mozilla |
| 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){ } |
| |
| if (dojo.render.html.safari) { |
| /* |
| this.iframe.style.visiblity = "visible"; |
| this.iframe.style.border = "1px solid black"; |
| this.editNode.style.visiblity = "visible"; |
| this.editNode.style.border = "1px solid black"; |
| */ |
| // this.onDisplayChanged(); |
| this.connect(this.editNode, "onblur", "onBlur"); |
| this.connect(this.editNode, "onfocus", "onFocus"); |
| this.connect(this.editNode, "onclick", "onFocus"); |
| |
| this.interval = setInterval(dojo.lang.hitch(this, "onDisplayChanged"), 750); |
| // dojo.raise("onload"); |
| // dojo.debug(this.editNode.parentNode.parentNode.parentNode.nodeName); |
| } else if (dojo.render.html.mozilla || dojo.render.html.opera) { |
| var doc = this.document; |
| var addListener = dojo.event.browser.addListener; |
| var self = this; |
| dojo.lang.forEach(this.events, function(e){ |
| var l = addListener(self.document, e.substr(2).toLowerCase(), dojo.lang.hitch(self, e)); |
| if(e=="onBlur"){ |
| // We need to unhook the blur event listener on close as we |
| // can encounter a garunteed crash in FF if another event is |
| // also fired |
| var unBlur = { unBlur: function(e){ |
| dojo.event.browser.removeListener(doc, "blur", l); |
| } }; |
| dojo.event.connect("before", self, "close", unBlur, "unBlur"); |
| } |
| }); |
| } |
| // FIXME: when scrollbars appear/disappear this needs to be fired |
| }else if(dojo.render.html.ie){ |
| // IE contentEditable |
| if(!this.height){ |
| this.connect(this, "onDisplayChanged", "_updateHeight"); |
| } |
| this.editNode.style.zoom = 1.0; |
| } |
| |
| this._applyEditingAreaStyleSheets(); |
| |
| if(this.focusOnLoad){ |
| this.focus(); |
| } |
| this.onDisplayChanged(e); |
| if(this.onLoadDeferred){ |
| this.onLoadDeferred.callback(true); |
| } |
| }, |
| |
| onKeyDown: function(e){ |
| // summary: Fired on keydown |
| if((!e)&&(this.object)){ |
| e = dojo.event.browser.fixEvent(this.window.event); |
| } |
| // dojo.debug("onkeydown:", e.keyCode); |
| // 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.render.html.ie)&&(e.keyCode == e.KEY_TAB)){ |
| e.preventDefault(); |
| e.stopPropagation(); |
| // FIXME: this is a poor-man's indent/outdent. It would be |
| // better if it added 4 " " chars in an undoable way. |
| // Unfortuantly pasteHTML does not prove to be undoable |
| this.execCommand((e.shiftKey ? "outdent" : "indent")); |
| }else if(dojo.render.html.ie){ |
| if((65 <= e.keyCode)&&(e.keyCode <= 90)){ |
| e.charCode = e.keyCode; |
| this.onKeyPress(e); |
| } |
| // dojo.debug(e.ctrlKey); |
| // dojo.debug(e.keyCode); |
| // dojo.debug(e.charCode); |
| // this.onKeyPress(e); |
| } |
| }, |
| |
| onKeyUp: function(e){ |
| // summary: Fired on keyup |
| return; |
| }, |
| |
| KEY_CTRL: 1, |
| |
| onKeyPress: function(e){ |
| // summary: Fired on keypress |
| if((!e)&&(this.object)){ |
| e = dojo.event.browser.fixEvent(this.window.event); |
| } |
| // handle the various key events |
| |
| var modifiers = e.ctrlKey ? this.KEY_CTRL : 0; |
| |
| if (this._keyHandlers[e.key]) { |
| // dojo.debug("char:", e.key); |
| var handlers = this._keyHandlers[e.key], i = 0, handler; |
| while (handler = handlers[i++]) { |
| if (modifiers == handler.modifiers) { |
| e.preventDefault(); |
| handler.handler.call(this); |
| break; |
| } |
| } |
| } |
| |
| // function call after the character has been inserted |
| dojo.lang.setTimeout(this, this.onKeyPressed, 1, e); |
| }, |
| |
| addKeyHandler: function (/*String*/key, /*Int*/modifiers, /*Function*/handler) { |
| // summary: add a handler for a keyboard shortcut |
| if (!(this._keyHandlers[key] instanceof Array)) { this._keyHandlers[key] = []; } |
| this._keyHandlers[key].push({ |
| modifiers: modifiers || 0, |
| handler: handler |
| }); |
| }, |
| |
| onKeyPressed: function (e) { |
| // summary: |
| // Fired after a keypress event has occured and it's action taken. This |
| // is useful if action needs to be taken after text operations have finished |
| |
| // Mozilla adds a single <p> with an embedded <br> when you hit enter once: |
| // <p><br>\n</p> |
| // when you hit enter again it adds another <br> inside your enter |
| // <p><br>\n<br>\n</p> |
| // and if you hit enter again it splits the <br>s over 2 <p>s |
| // <p><br>\n</p>\n<p><br>\n</p> |
| // now this assumes that <p>s have double the line-height of <br>s to work |
| // and so we need to remove the <p>s to ensure the position of the cursor |
| // changes from the users perspective when they hit enter, as the second two |
| // html snippets render the same when margins are set to 0. |
| |
| // TODO: doesn't really work; is this really needed? |
| //if (dojo.render.html.moz) { |
| // for (var i = 0; i < this.document.getElementsByTagName("p").length; i++) { |
| // var p = this.document.getElementsByTagName("p")[i]; |
| // if (p.innerHTML.match(/^<br>\s$/m)) { |
| // while (p.hasChildNodes()) { p.parentNode.insertBefore(p.firstChild, p); } |
| // p.parentNode.removeChild(p); |
| // } |
| // } |
| //} |
| this.onDisplayChanged(/*e*/); // can't pass in e |
| }, |
| |
| onClick: function(e){ this.onDisplayChanged(e); }, |
| onBlur: function(e){ }, |
| _initialFocus: true, |
| onFocus: function(e){ |
| // summary: Fired on focus |
| if( (dojo.render.html.mozilla)&&(this._initialFocus) ){ |
| this._initialFocus = false; |
| if(dojo.string.trim(this.editNode.innerHTML) == " "){ |
| this.placeCursorAtStart(); |
| // this.execCommand("selectall"); |
| // this.window.getSelection().collapseToStart(); |
| } |
| } |
| /* |
| // FIXME: attempted (but ultimately unworkable) solution for #2066 |
| if(dojo.render.html.safari){ |
| var sel = this.window.getSelection(); |
| dojo.debug(dojo.dom.isDescendantOf(sel.focusNode, this.document.body)); |
| |
| var isLastNode = (sel.focusNode == this.document.body.lastChild); |
| if(!isLastNode){ return; } |
| var isText = (sel.focusNode.nodeType == 3); |
| if(!isText){ return; } |
| var isLastChar = (sel.focusOffset == sel.focusNode.nodeValue.length); |
| if(!isLastChar){ return; } |
| dojo.debug(sel.focusOffset, sel.focusNode.nodeValue.length); |
| if(isLastNode && isText && isLastChar){ |
| this.document.body.appendChild(this.document.createTextNode(" ")); |
| this.document.body.appendChild(this.document.createElement("br")); |
| this.document.body.appendChild(this.document.createElement("p")); |
| this.document.body.appendChild(this.document.createElement("p")); |
| sel.collapse(sel.focusNode, sel.focusOffset-1); |
| sel.collapse(sel.focusNode, sel.focusOffset+1); |
| } |
| } |
| */ |
| }, |
| |
| blur: function () { |
| // summary: remove focus from this instance |
| if(this.iframe) { this.window.blur(); } |
| else if(this.object) { this.document.body.blur(); } |
| else if(this.editNode) { this.editNode.blur(); } |
| }, |
| |
| focus: function () { |
| // summary: move focus to this instance |
| if(this.iframe && !dojo.render.html.ie) { this.window.focus(); } |
| else if(this.object) { this.document.focus(); } |
| // editNode may be hidden in display:none div, lets just punt in this case |
| else if(this.editNode && this.editNode.focus) { this.editNode.focus(); } |
| else{ |
| dojo.debug("Have no idea how to focus into the editor!"); |
| } |
| }, |
| |
| /** this event will be fired everytime the display context changes and the |
| result needs to be reflected in the UI */ |
| onDisplayChanged: function (e){ }, |
| |
| |
| /* Formatting commands |
| **********************/ |
| |
| // Object: IE's Active X codes: see http://www.computerbytesman.com/js/activex/dhtmledit.htm |
| _activeX: { |
| command: { |
| bold: 5000, |
| italic: 5023, |
| underline: 5048, |
| |
| justifycenter: 5024, |
| justifyleft: 5025, |
| justifyright: 5026, |
| |
| cut: 5003, |
| copy: 5002, |
| paste: 5032, |
| "delete": 5004, |
| |
| undo: 5049, |
| redo: 5033, |
| |
| removeformat: 5034, |
| selectall: 5035, |
| unlink: 5050, |
| |
| indent: 5018, |
| outdent: 5031, |
| |
| insertorderedlist: 5030, |
| insertunorderedlist: 5051, |
| |
| // table commands |
| inserttable: 5022, |
| insertcell: 5019, |
| insertcol: 5020, |
| insertrow: 5021, |
| deletecells: 5005, |
| deletecols: 5006, |
| deleterows: 5007, |
| mergecells: 5029, |
| splitcell: 5047, |
| |
| // the command need mapping, they don't translate directly |
| // to the contentEditable commands |
| setblockformat: 5043, |
| getblockformat: 5011, |
| getblockformatnames: 5012, |
| setfontname: 5044, |
| getfontname: 5013, |
| setfontsize: 5045, |
| getfontsize: 5014, |
| setbackcolor: 5042, |
| getbackcolor: 5010, |
| setforecolor: 5046, |
| getforecolor: 5015, |
| |
| findtext: 5008, |
| font: 5009, |
| hyperlink: 5016, |
| image: 5017, |
| |
| lockelement: 5027, |
| makeabsolute: 5028, |
| sendbackward: 5036, |
| bringforward: 5037, |
| sendbelowtext: 5038, |
| bringabovetext: 5039, |
| sendtoback: 5040, |
| bringtofront: 5041, |
| |
| properties: 5052 |
| }, |
| |
| ui: { |
| "default": 0, |
| prompt: 1, |
| noprompt: 2 |
| }, |
| |
| status: { |
| notsupported: 0, |
| disabled: 1, |
| enabled: 3, |
| latched: 7, |
| ninched: 11 |
| }, |
| |
| appearance: { |
| flat: 0, |
| inset: 1 |
| }, |
| |
| state: { |
| unchecked: 0, |
| checked: 1, |
| gray: 2 |
| } |
| }, |
| |
| _normalizeCommand: function (/*String*/cmd){ |
| // summary: |
| // Used as the advice function by dojo.event.connect to map our |
| // normalized set of commands to those supported by the target |
| // browser |
| var drh = dojo.render.html; |
| |
| var command = cmd.toLowerCase(); |
| if(command == "formatblock"){ |
| if(drh.safari){ command = "heading"; } |
| }else if(this.object){ |
| switch(command){ |
| case "createlink": |
| command = "hyperlink"; |
| break; |
| case "insertimage": |
| command = "image"; |
| break; |
| } |
| }else if(command == "hilitecolor" && !drh.mozilla){ |
| command = "backcolor"; |
| } |
| |
| return command; |
| }, |
| |
| _safariIsLeopard: function(){ |
| var gt420 = false; |
| if(dojo.render.html.safari){ |
| var tmp = dojo.render.html.UA.split("AppleWebKit/")[1]; |
| var ver = parseFloat(tmp.split(" ")[0]); |
| if(ver >= 420){ gt420 = true; } |
| } |
| return gt420; |
| }, |
| |
| 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 = this._safariIsLeopard(); |
| |
| 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": |
| 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 | (this.object ? ie : 0)); |
| break; |
| |
| case "insertcell": case "insertcol": case "insertrow": |
| case "deletecells": case "deletecols": case "deleterows": |
| case "mergecells": case "splitcell": |
| supportedBy = isSupportedBy(this.object ? ie : 0); |
| break; |
| |
| default: return false; |
| } |
| |
| return (dojo.render.html.ie && supportedBy.ie) || |
| (dojo.render.html.mozilla && supportedBy.mozilla) || |
| (dojo.render.html.safari && supportedBy.safari) || |
| (gt420 && supportedBy.safari420) || |
| (dojo.render.html.opera && 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 (none-activeX mode) 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"){ |
| if(this.object){ //IE activeX mode |
| argument = this._native2LocalFormatNames[argument]; |
| } |
| else if(dojo.render.html.ie){ argument = '<'+argument+'>'; } |
| } |
| } |
| if(this.object){ |
| switch (command) { |
| case "hilitecolor": |
| command = "setbackcolor"; |
| break; |
| case "forecolor": |
| case "backcolor": |
| case "fontsize": |
| case "fontname": |
| command = "set" + command; |
| break; |
| case "formatblock": |
| command = "setblockformat"; |
| } |
| |
| if(command == "strikethrough"){ |
| command = "inserthtml"; |
| var range = this.document.selection.createRange(); |
| if(!range.htmlText){ |
| return; |
| } |
| argument=range.htmlText.strike(); |
| }else if(command == "inserthorizontalrule"){ |
| command = "inserthtml"; |
| argument="<hr>"; |
| } |
| |
| if(command == "inserthtml"){ |
| var range = this.document.selection.createRange(); |
| if(this.document.selection.type.toUpperCase() == "CONTROL"){ |
| //if selection is controlrange, no pasteHTML is available, |
| //we replace the outerHTML directly |
| for(var i=0;i<range.length;i++){ |
| range.item(i).outerHTML = argument; |
| } |
| }else{ |
| // on IE, we can use the pasteHTML method of the textRange object |
| // to get an undo-able innerHTML modification |
| range.pasteHTML(argument); |
| range.select(); |
| } |
| returnValue = true; |
| }else if(arguments.length == 1){ |
| returnValue = this.object.ExecCommand(this._activeX.command[command], |
| this._activeX.ui.noprompt); |
| }else{ |
| returnValue = this.object.ExecCommand(this._activeX.command[command], |
| this._activeX.ui.noprompt, argument); |
| } |
| }else if(command == "inserthtml"){ |
| if(dojo.render.html.ie){ |
| //dojo.debug("inserthtml breaks the undo stack when not using the ActiveX version of the control!"); |
| var insertRange = this.document.selection.createRange(); |
| insertRange.pasteHTML(argument); |
| insertRange.select(); |
| //insertRange.collapse(true); |
| return true; |
| }else{ |
| return this.document.execCommand(command, false, argument); |
| } |
| /* */ |
| // fix up unlink in Mozilla to unlink the link and not just the selection |
| }else if((command == "unlink")&& |
| (this.queryCommandEnabled("unlink"))&& |
| (dojo.render.html.mozilla)){ |
| // 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", dojo.html.selection, ['a']); |
| dojo.withGlobal(this.window, "selectElement", dojo.html.selection, [a]); |
| |
| returnValue = this.document.execCommand("unlink", false, null); |
| |
| // restore original selection |
| var selectionRange = this.document.createRange(); |
| selectionRange.setStart(selectionStartContainer, selectionStartOffset); |
| selectionRange.setEnd(selectionEndContainer, selectionEndOffset); |
| selection.removeAllRanges(); |
| selection.addRange(selectionRange); |
| |
| return returnValue; |
| }else if((command == "hilitecolor")&&(dojo.render.html.mozilla)){ |
| // mozilla doesn't support hilitecolor properly when useCSS is |
| // set to false (bugzilla #279330) |
| |
| this.document.execCommand("useCSS", false, false); |
| returnValue = this.document.execCommand(command, false, argument); |
| this.document.execCommand("useCSS", false, true); |
| |
| }else if((dojo.render.html.ie)&&( (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{ |
| // dojo.debug("command:", command, "arg:", argument); |
| |
| argument = arguments.length > 1 ? argument : null; |
| // if(dojo.render.html.moz){ |
| // 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 |
| command = this._normalizeCommand(command); |
| if(this.object){ |
| switch (command) { |
| case "hilitecolor": |
| command = "setbackcolor"; |
| break; |
| case "forecolor": |
| case "backcolor": |
| case "fontsize": |
| case "fontname": |
| command = "set" + command; |
| break; |
| case "formatblock": |
| command = "setblockformat"; |
| break; |
| //below are not natively supported commands, we fake them |
| case "strikethrough": |
| command = "bold"; //whenever bold is enabled, strikethrough should be so as well |
| break; |
| case "inserthorizontalrule": |
| return true; |
| } |
| |
| if(typeof this._activeX.command[command] == "undefined"){ return false; } |
| var status = this.object.QueryStatus(this._activeX.command[command]); |
| return ((status != this._activeX.status.notsupported)&& |
| (status != this._activeX.status.disabled)); |
| }else{ |
| if(dojo.render.html.mozilla){ |
| if(command == "unlink"){ // mozilla returns true always |
| return dojo.withGlobal(this.window, "hasAncestorElement", dojo.html.selection, ['a']); |
| } else if (command == "inserttable") { |
| return true; |
| } |
| } |
| |
| // return this.document.queryCommandEnabled(command); |
| var elem = (dojo.render.html.ie) ? this.document.selection.createRange() : this.document; |
| return elem.queryCommandEnabled(command); |
| } |
| }, |
| |
| queryCommandState: function(command){ |
| // summary: check the state of a given command |
| command = this._normalizeCommand(command); |
| if(this.object){ |
| if(command == "forecolor"){ |
| command = "setforecolor"; |
| }else if(command == "backcolor"){ |
| command = "setbackcolor"; |
| }else if(command == "strikethrough"){ |
| //check whether we are under a <strike> |
| return dojo.withGlobal(this.window, "hasAncestorElement", dojo.html.selection, ['strike']); |
| }else if(command == "inserthorizontalrule"){ |
| return false; |
| } |
| |
| if(typeof this._activeX.command[command] == "undefined"){ return null; } |
| var status = this.object.QueryStatus(this._activeX.command[command]); |
| return ((status == this._activeX.status.latched)|| |
| (status == this._activeX.status.ninched)); |
| }else{ |
| return this.document.queryCommandState(command); |
| } |
| }, |
| |
| queryCommandValue: function (command) { |
| // summary: check the value of a given command |
| command = this._normalizeCommand(command); |
| if (this.object) { |
| switch (command) { |
| case "forecolor": |
| case "backcolor": |
| case "fontsize": |
| case "fontname": |
| command = "get" + command; |
| return this.object.execCommand( |
| this._activeX.command[command], |
| this._activeX.ui.noprompt); |
| case "formatblock": |
| var retvalue = this.object.execCommand( |
| this._activeX.command["getblockformat"], |
| this._activeX.ui.noprompt); |
| if(retvalue){ |
| return this._local2NativeFormatNames[retvalue]; |
| } |
| } |
| } else { |
| if(dojo.render.html.ie && command == "formatblock"){ |
| return this._local2NativeFormatNames[this.document.queryCommandValue(command)] || 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 |
| if(dojo.render.html.moz && this.editNode.firstChild && |
| this.editNode.firstChild.nodeType != dojo.dom.TEXT_NODE){ |
| dojo.withGlobal(this.window, "selectElementChildren", dojo.html.selection, [this.editNode.firstChild]); |
| }else{ |
| dojo.withGlobal(this.window, "selectElementChildren", dojo.html.selection, [this.editNode]); |
| } |
| dojo.withGlobal(this.window, "collapse", dojo.html.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 |
| if(dojo.render.html.moz && this.editNode.lastChild && |
| this.editNode.lastChild.nodeType != dojo.dom.TEXT_NODE){ |
| dojo.withGlobal(this.window, "selectElementChildren", dojo.html.selection, [this.editNode.lastChild]); |
| }else{ |
| dojo.withGlobal(this.window, "selectElementChildren", dojo.html.selection, [this.editNode]); |
| } |
| dojo.withGlobal(this.window, "collapse", dojo.html.selection, [false]); |
| }, |
| |
| replaceEditorContent: function(/*String*/html){ |
| // summary: |
| // this function set the content while trying to maintain the undo stack |
| html = this._preFilterContent(html); |
| if(this.isClosed){ |
| this.domNode.innerHTML = html; |
| }else if(this.window && this.window.getSelection && !dojo.render.html.moz){ // Safari |
| // look ma! it's a totally f'd browser! |
| this.editNode.innerHTML = html; |
| }else if((this.window && this.window.getSelection) || (this.document && this.document.selection)){ // Moz/IE |
| this.execCommand("selectall"); |
| if(dojo.render.html.moz && !html){ html = " " } |
| this.execCommand("inserthtml", html); |
| } |
| }, |
| |
| _preFilterContent: function(/*String*/html){ |
| // summary: |
| // filter the input before setting the content of the editing area |
| var ec = html; |
| dojo.lang.forEach(this.contentPreFilters, function(ef){ |
| ec = ef(ec); |
| }); |
| if(this.contentDomPreFilters.length>0){ |
| var dom = dojo.doc().createElement('div'); |
| dom.style.display = "none"; |
| dojo.body().appendChild(dom); |
| dom.innerHTML = ec; |
| dojo.lang.forEach(this.contentDomPreFilters, function(ef){ |
| dom = ef(dom); |
| }); |
| ec = dom.innerHTML; |
| dojo.body().removeChild(dom); |
| } |
| return ec; |
| }, |
| _postFilterContent: function(/*String*/html){ |
| // summary: |
| // filter the output after getting the content of the editing area |
| var ec = html; |
| if(this.contentDomPostFilters.length>0){ |
| var dom = this.document.createElement('div'); |
| dom.innerHTML = ec; |
| dojo.lang.forEach(this.contentDomPostFilters, function(ef){ |
| dom = ef(dom); |
| }); |
| ec = dom.innerHTML; |
| } |
| dojo.lang.forEach(this.contentPostFilters, function(ef){ |
| ec = ef(ec); |
| }); |
| return ec; |
| }, |
| |
| //Int: stored last time height |
| _lastHeight: 0, |
| |
| _updateHeight: function(){ |
| // summary: |
| // Updates the height of the editor area to fit the contents. |
| if(!this.isLoaded){ return; } |
| if(this.height){ return; } |
| |
| var height = dojo.html.getBorderBox(this.editNode).height; |
| //height maybe zero in some cases even though the content is not empty, |
| //we try the height of body instead |
| if(!height){ |
| height = dojo.html.getBorderBox(this.document.body).height; |
| } |
| if(height == 0){ |
| dojo.debug("Can not figure out the height of the editing area!"); |
| return; //prevent setting height to 0 |
| } |
| this._lastHeight = height; |
| this.editorObject.style.height = this._lastHeight + "px"; |
| this.window.scrollTo(0, 0); |
| }, |
| |
| _saveContent: function(e){ |
| // summary: |
| // Saves the content in an onunload event if the editor has not been closed |
| var saveTextarea = dojo.doc().getElementById("dojo.widget.RichText.savedContent"); |
| saveTextarea.value += this._SEPARATOR + this.saveName + ":" + this.getEditorContent(); |
| }, |
| |
| getEditorContent: function(){ |
| // summary: |
| // return the current content of the editing area (post filters are applied) |
| var ec = ""; |
| try{ |
| ec = (this._content.length > 0) ? this._content : this.editNode.innerHTML; |
| if(dojo.string.trim(ec) == " "){ ec = ""; } |
| }catch(e){ /* squelch */ } |
| |
| if(dojo.render.html.ie && !this.object){ |
| //removing appended <P> </P> for IE in none-activeX mode |
| var re = new RegExp("(?:<p> </p>[\n\r]*)+$", "i"); |
| ec = ec.replace(re,""); |
| } |
| |
| ec = this._postFilterContent(ec); |
| |
| if (this.relativeImageUrls) { |
| // why use a regexp instead of dom? because IE is stupid |
| // and won't let us set img.src to a relative URL |
| // this comes after contentPostFilters because once content |
| // gets innerHTML'd img urls will be fully qualified |
| var siteBase = dojo.global().location.protocol + "//" + dojo.global().location.host; |
| var pathBase = dojo.global().location.pathname; |
| if (pathBase.match(/\/$/)) { |
| // ends with slash, match full path |
| } else { |
| // match parent path to find siblings |
| var pathParts = pathBase.split("/"); |
| if (pathParts.length) { |
| pathParts.pop(); |
| } |
| pathBase = pathParts.join("/") + "/"; |
| |
| } |
| |
| var sameSite = new RegExp("(<img[^>]*\ src=[\"'])("+siteBase+"("+pathBase+")?)", "ig"); |
| ec = ec.replace(sameSite, "$1"); |
| } |
| return ec; |
| }, |
| |
| 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 == 0) { save = true; } |
| this._content = this._postFilterContent(this.editNode.innerHTML); |
| 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(dojo.render.html.ie && !this.object){ |
| dojo.event.browser.clean(this.editNode); |
| } |
| |
| if (this.iframe) { |
| // FIXME: should keep iframe around for later re-use |
| delete this.iframe; |
| } |
| |
| if(this.textarea){ |
| with(this.textarea.style){ |
| position = ""; |
| left = top = ""; |
| if(dojo.render.html.ie){ |
| overflow = this.__overflow; |
| this.__overflow = null; |
| } |
| } |
| if(save){ |
| this.textarea.value = this._content; |
| }else{ |
| this.textarea.value = this.savedContent; |
| } |
| dojo.html.removeNode(this.domNode); |
| this.domNode = this.textarea; |
| }else{ |
| if(save){ |
| if(dojo.render.html.moz){ |
| var nc = dojo.doc().createElement("span"); |
| this.domNode.appendChild(nc); |
| nc.innerHTML = this.editNode.innerHTML; |
| }else{ |
| this.domNode.innerHTML = this._content; |
| } |
| }else{ |
| this.domNode.innerHTML = this.savedContent; |
| } |
| } |
| |
| dojo.html.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._frameElement){ |
| this.window._frameElement = null; |
| } |
| |
| this.window = null; |
| this.document = null; |
| this.object = null; |
| this.editingArea = null; |
| this.editorObject = null; |
| |
| return changed; // Boolean: whether the content has been modified |
| }, |
| |
| destroyRendering: function(){}, // stub! |
| |
| destroy: function (){ |
| this.destroyRendering(); |
| if(!this.isClosed){ this.close(false); } |
| |
| dojo.widget.RichText.superclass.destroy.call(this); |
| }, |
| |
| connect: function (targetObj, targetFunc, thisFunc) { |
| // summary: convenient method for dojo.event.connect |
| dojo.event.connect(targetObj, targetFunc, this, thisFunc); |
| }, |
| |
| disconnect: function (targetObj, targetFunc, thisFunc) { |
| // summary: convenient method for dojo.event.disconnect |
| dojo.event.disconnect(targetObj, targetFunc, this, thisFunc); |
| }, |
| |
| disconnectAllWithRoot: function (targetObj) { |
| dojo.deprecated("disconnectAllWithRoot", "is deprecated. No need to disconnect manually", "0.5"); |
| }, |
| |
| _fixContentForMoz: function(html){ |
| // summary: |
| // Moz can not handle strong/em tags correctly, correct them here |
| html = html.replace(/<strong([ \>])/gi, '<b$1' ); |
| html = html.replace(/<\/strong>/gi, '<\/b>' ); |
| html = html.replace(/<em([ \>])/gi, '<i$1' ); |
| html = html.replace(/<\/em>/gi, '<\/i>' ); |
| return html; |
| } |
| } |
| ); |