diff options
Diffstat (limited to 'lib/dijit/Editor.js')
-rw-r--r-- | lib/dijit/Editor.js | 1231 |
1 files changed, 776 insertions, 455 deletions
diff --git a/lib/dijit/Editor.js b/lib/dijit/Editor.js index 69258eade..b77dd7165 100644 --- a/lib/dijit/Editor.js +++ b/lib/dijit/Editor.js @@ -1,12 +1,12 @@ /* - Copyright (c) 2004-2010, The Dojo Foundation All Rights Reserved. + Copyright (c) 2004-2011, The Dojo Foundation All Rights Reserved. Available via Academic Free License >= 2.1 OR the modified BSD license. see: http://dojotoolkit.org/license for details */ -if(!dojo._hasResource["dijit.Editor"]){ -dojo._hasResource["dijit.Editor"]=true; +if(!dojo._hasResource["dijit.Editor"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.Editor"] = true; dojo.provide("dijit.Editor"); dojo.require("dijit._editor.RichText"); dojo.require("dijit.Toolbar"); @@ -17,457 +17,778 @@ dojo.require("dijit._editor.range"); dojo.require("dijit._Container"); dojo.require("dojo.i18n"); dojo.require("dijit.layout._LayoutWidget"); -dojo.require("dijit._editor.range"); -dojo.requireLocalization("dijit._editor","commands",null,"ROOT,ar,ca,cs,da,de,el,es,fi,fr,he,hu,it,ja,ko,nb,nl,pl,pt,pt-pt,ro,ru,sk,sl,sv,th,tr,zh,zh-tw"); -dojo.declare("dijit.Editor",dijit._editor.RichText,{plugins:null,extraPlugins:null,constructor:function(){ -if(!dojo.isArray(this.plugins)){ -this.plugins=["undo","redo","|","cut","copy","paste","|","bold","italic","underline","strikethrough","|","insertOrderedList","insertUnorderedList","indent","outdent","|","justifyLeft","justifyRight","justifyCenter","justifyFull","dijit._editor.plugins.EnterKeyHandling"]; -} -this._plugins=[]; -this._editInterval=this.editActionInterval*1000; -if(dojo.isIE){ -this.events.push("onBeforeDeactivate"); -this.events.push("onBeforeActivate"); -} -},postCreate:function(){ -this._steps=this._steps.slice(0); -this._undoedSteps=this._undoedSteps.slice(0); -if(dojo.isArray(this.extraPlugins)){ -this.plugins=this.plugins.concat(this.extraPlugins); -} -this.setValueDeferred=new dojo.Deferred(); -this.inherited(arguments); -this.commands=dojo.i18n.getLocalization("dijit._editor","commands",this.lang); -if(!this.toolbar){ -this.toolbar=new dijit.Toolbar({dir:this.dir,lang:this.lang}); -this.header.appendChild(this.toolbar.domNode); -} -dojo.forEach(this.plugins,this.addPlugin,this); -this.setValueDeferred.callback(true); -dojo.addClass(this.iframe.parentNode,"dijitEditorIFrameContainer"); -dojo.addClass(this.iframe,"dijitEditorIFrame"); -dojo.attr(this.iframe,"allowTransparency",true); -if(dojo.isWebKit){ -dojo.style(this.domNode,"KhtmlUserSelect","none"); -} -this.toolbar.startup(); -this.onNormalizedDisplayChanged(); -},destroy:function(){ -dojo.forEach(this._plugins,function(p){ -if(p&&p.destroy){ -p.destroy(); -} -}); -this._plugins=[]; -this.toolbar.destroyRecursive(); -delete this.toolbar; -this.inherited(arguments); -},addPlugin:function(_1,_2){ -var _3=dojo.isString(_1)?{name:_1}:_1; -if(!_3.setEditor){ -var o={"args":_3,"plugin":null,"editor":this}; -dojo.publish(dijit._scopeName+".Editor.getPlugin",[o]); -if(!o.plugin){ -var pc=dojo.getObject(_3.name); -if(pc){ -o.plugin=new pc(_3); -} -} -if(!o.plugin){ -console.warn("Cannot find plugin",_1); -return; -} -_1=o.plugin; -} -if(arguments.length>1){ -this._plugins[_2]=_1; -}else{ -this._plugins.push(_1); -} -_1.setEditor(this); -if(dojo.isFunction(_1.setToolbar)){ -_1.setToolbar(this.toolbar); -} -},startup:function(){ -},resize:function(_4){ -if(_4){ -dijit.layout._LayoutWidget.prototype.resize.apply(this,arguments); -} -},layout:function(){ -var _5=(this._contentBox.h-(this.getHeaderHeight()+this.getFooterHeight()+dojo._getPadBorderExtents(this.iframe.parentNode).h+dojo._getMarginExtents(this.iframe.parentNode).h)); -this.editingArea.style.height=_5+"px"; -if(this.iframe){ -this.iframe.style.height="100%"; -} -this._layoutMode=true; -},_onIEMouseDown:function(e){ -var _6; -var b=this.document.body; -var _7=b.clientWidth; -var _8=b.clientHeight; -var _9=b.clientLeft; -var _a=b.offsetWidth; -var _b=b.offsetHeight; -var _c=b.offsetLeft; -bodyDir=b.dir?b.dir.toLowerCase():""; -if(bodyDir!="rtl"){ -if(_7<_a&&e.x>_7&&e.x<_a){ -_6=true; -} -}else{ -if(e.x<_9&&e.x>_c){ -_6=true; -} -} -if(!_6){ -if(_8<_b&&e.y>_8&&e.y<_b){ -_6=true; -} -} -if(!_6){ -delete this._cursorToStart; -delete this._savedSelection; -if(e.target.tagName=="BODY"){ -setTimeout(dojo.hitch(this,"placeCursorAtEnd"),0); -} -this.inherited(arguments); -} -},onBeforeActivate:function(e){ -this._restoreSelection(); -},onBeforeDeactivate:function(e){ -if(this.customUndo){ -this.endEditing(true); -} -if(e.target.tagName!="BODY"){ -this._saveSelection(); -} -},customUndo:dojo.isIE||dojo.isWebKit,editActionInterval:3,beginEditing:function(_d){ -if(!this._inEditing){ -this._inEditing=true; -this._beginEditing(_d); -} -if(this.editActionInterval>0){ -if(this._editTimer){ -clearTimeout(this._editTimer); -} -this._editTimer=setTimeout(dojo.hitch(this,this.endEditing),this._editInterval); -} -},_steps:[],_undoedSteps:[],execCommand:function(_e){ -if(this.customUndo&&(_e=="undo"||_e=="redo")){ -return this[_e](); -}else{ -if(this.customUndo){ -this.endEditing(); -this._beginEditing(); -} -var r; -try{ -r=this.inherited("execCommand",arguments); -if(dojo.isWebKit&&_e=="paste"&&!r){ -throw {code:1011}; -} -} -catch(e){ -if(e.code==1011&&/copy|cut|paste/.test(_e)){ -var _f=dojo.string.substitute,_10={cut:"X",copy:"C",paste:"V"}; -alert(_f(this.commands.systemShortcut,[this.commands[_e],_f(this.commands[dojo.isMac?"appleKey":"ctrlKey"],[_10[_e]])])); -} -r=false; -} -if(this.customUndo){ -this._endEditing(); -} -return r; -} -},queryCommandEnabled:function(cmd){ -if(this.customUndo&&(cmd=="undo"||cmd=="redo")){ -return cmd=="undo"?(this._steps.length>1):(this._undoedSteps.length>0); -}else{ -return this.inherited("queryCommandEnabled",arguments); -} -},_moveToBookmark:function(b){ -var _11=b.mark; -var _12=b.mark; -var col=b.isCollapsed; -var r,_13,_14,sel; -if(_12){ -if(dojo.isIE){ -if(dojo.isArray(_12)){ -_11=[]; -dojo.forEach(_12,function(n){ -_11.push(dijit.range.getNode(n,this.editNode)); -},this); -dojo.withGlobal(this.window,"moveToBookmark",dijit,[{mark:_11,isCollapsed:col}]); -}else{ -if(_12.startContainer&&_12.endContainer){ -sel=dijit.range.getSelection(this.window); -if(sel&&sel.removeAllRanges){ -sel.removeAllRanges(); -r=dijit.range.create(this.window); -_13=dijit.range.getNode(_12.startContainer,this.editNode); -_14=dijit.range.getNode(_12.endContainer,this.editNode); -if(_13&&_14){ -r.setStart(_13,_12.startOffset); -r.setEnd(_14,_12.endOffset); -sel.addRange(r); -} -} -} -} -}else{ -sel=dijit.range.getSelection(this.window); -if(sel&&sel.removeAllRanges){ -sel.removeAllRanges(); -r=dijit.range.create(this.window); -_13=dijit.range.getNode(_12.startContainer,this.editNode); -_14=dijit.range.getNode(_12.endContainer,this.editNode); -if(_13&&_14){ -r.setStart(_13,_12.startOffset); -r.setEnd(_14,_12.endOffset); -sel.addRange(r); -} -} -} -} -},_changeToStep:function(_15,to){ -this.setValue(to.text); -var b=to.bookmark; -if(!b){ -return; -} -this._moveToBookmark(b); -},undo:function(){ -var ret=false; -if(!this._undoRedoActive){ -this._undoRedoActive=true; -this.endEditing(true); -var s=this._steps.pop(); -if(s&&this._steps.length>0){ -this.focus(); -this._changeToStep(s,this._steps[this._steps.length-1]); -this._undoedSteps.push(s); -this.onDisplayChanged(); -delete this._undoRedoActive; -ret=true; -} -delete this._undoRedoActive; -} -return ret; -},redo:function(){ -var ret=false; -if(!this._undoRedoActive){ -this._undoRedoActive=true; -this.endEditing(true); -var s=this._undoedSteps.pop(); -if(s&&this._steps.length>0){ -this.focus(); -this._changeToStep(this._steps[this._steps.length-1],s); -this._steps.push(s); -this.onDisplayChanged(); -ret=true; -} -delete this._undoRedoActive; -} -return ret; -},endEditing:function(_16){ -if(this._editTimer){ -clearTimeout(this._editTimer); -} -if(this._inEditing){ -this._endEditing(_16); -this._inEditing=false; -} -},_getBookmark:function(){ -var b=dojo.withGlobal(this.window,dijit.getBookmark); -var tmp=[]; -if(b&&b.mark){ -var _17=b.mark; -if(dojo.isIE){ -var sel=dijit.range.getSelection(this.window); -if(!dojo.isArray(_17)){ -if(sel){ -var _18; -if(sel.rangeCount){ -_18=sel.getRangeAt(0); -} -if(_18){ -b.mark=_18.cloneRange(); -}else{ -b.mark=dojo.withGlobal(this.window,dijit.getBookmark); -} -} -}else{ -dojo.forEach(b.mark,function(n){ -tmp.push(dijit.range.getIndex(n,this.editNode).o); -},this); -b.mark=tmp; -} -} -try{ -if(b.mark&&b.mark.startContainer){ -tmp=dijit.range.getIndex(b.mark.startContainer,this.editNode).o; -b.mark={startContainer:tmp,startOffset:b.mark.startOffset,endContainer:b.mark.endContainer===b.mark.startContainer?tmp:dijit.range.getIndex(b.mark.endContainer,this.editNode).o,endOffset:b.mark.endOffset}; -} -} -catch(e){ -b.mark=null; -} -} -return b; -},_beginEditing:function(cmd){ -if(this._steps.length===0){ -this._steps.push({"text":dijit._editor.getChildrenHtml(this.editNode),"bookmark":this._getBookmark()}); -} -},_endEditing:function(_19){ -var v=dijit._editor.getChildrenHtml(this.editNode); -this._undoedSteps=[]; -this._steps.push({text:v,bookmark:this._getBookmark()}); -},onKeyDown:function(e){ -if(!dojo.isIE&&!this.iframe&&e.keyCode==dojo.keys.TAB&&!this.tabIndent){ -this._saveSelection(); -} -if(!this.customUndo){ -this.inherited(arguments); -return; -} -var k=e.keyCode,ks=dojo.keys; -if(e.ctrlKey&&!e.altKey){ -if(k==90||k==122){ -dojo.stopEvent(e); -this.undo(); -return; -}else{ -if(k==89||k==121){ -dojo.stopEvent(e); -this.redo(); -return; -} -} -} -this.inherited(arguments); -switch(k){ -case ks.ENTER: -case ks.BACKSPACE: -case ks.DELETE: -this.beginEditing(); -break; -case 88: -case 86: -if(e.ctrlKey&&!e.altKey&&!e.metaKey){ -this.endEditing(); -if(e.keyCode==88){ -this.beginEditing("cut"); -setTimeout(dojo.hitch(this,this.endEditing),1); -}else{ -this.beginEditing("paste"); -setTimeout(dojo.hitch(this,this.endEditing),1); -} -break; -} -default: -if(!e.ctrlKey&&!e.altKey&&!e.metaKey&&(e.keyCode<dojo.keys.F1||e.keyCode>dojo.keys.F15)){ -this.beginEditing(); -break; -} -case ks.ALT: -this.endEditing(); -break; -case ks.UP_ARROW: -case ks.DOWN_ARROW: -case ks.LEFT_ARROW: -case ks.RIGHT_ARROW: -case ks.HOME: -case ks.END: -case ks.PAGE_UP: -case ks.PAGE_DOWN: -this.endEditing(true); -break; -case ks.CTRL: -case ks.SHIFT: -case ks.TAB: -break; -} -},_onBlur:function(){ -this.inherited("_onBlur",arguments); -this.endEditing(true); -},_saveSelection:function(){ -this._savedSelection=this._getBookmark(); -},_restoreSelection:function(){ -if(this._savedSelection){ -delete this._cursorToStart; -if(dojo.withGlobal(this.window,"isCollapsed",dijit)){ -this._moveToBookmark(this._savedSelection); -} -delete this._savedSelection; -} -},onClick:function(){ -this.endEditing(true); -this.inherited(arguments); -},_setDisabledAttr:function(_1a){ -if(!this.disabled&&_1a){ -this._buttonEnabledPlugins=dojo.filter(this._plugins,function(p){ -if(p&&p.button&&!p.button.get("disabled")){ -p.button.set("disabled",true); -return true; -} -return false; -}); -}else{ -if(this.disabled&&!_1a){ -dojo.forEach(this._buttonEnabledPlugins,function(p){ -p.button.attr("disabled",false); -p.updateState&&p.updateState(); -}); -} -} -this.inherited(arguments); -},_setStateClass:function(){ -this.inherited(arguments); -if(this.document&&this.document.body){ -dojo.style(this.document.body,"color",dojo.style(this.iframe,"color")); -} -}}); -dojo.subscribe(dijit._scopeName+".Editor.getPlugin",null,function(o){ -if(o.plugin){ -return; -} -var _1b=o.args,p; -var _1c=dijit._editor._Plugin; -var _1d=_1b.name; -switch(_1d){ -case "undo": -case "redo": -case "cut": -case "copy": -case "paste": -case "insertOrderedList": -case "insertUnorderedList": -case "indent": -case "outdent": -case "justifyCenter": -case "justifyFull": -case "justifyLeft": -case "justifyRight": -case "delete": -case "selectAll": -case "removeFormat": -case "unlink": -case "insertHorizontalRule": -p=new _1c({command:_1d}); -break; -case "bold": -case "italic": -case "underline": -case "strikethrough": -case "subscript": -case "superscript": -p=new _1c({buttonClass:dijit.form.ToggleButton,command:_1d}); -break; -case "|": -p=new _1c({button:new dijit.ToolbarSeparator(),setEditor:function(_1e){ -this.editor=_1e; -}}); -} -o.plugin=p; +dojo.requireLocalization("dijit._editor", "commands", null, "ROOT,ar,ca,cs,da,de,el,es,fi,fr,he,hu,it,ja,kk,ko,nb,nl,pl,pt,pt-pt,ro,ru,sk,sl,sv,th,tr,zh,zh-tw"); + + +dojo.declare( + "dijit.Editor", + dijit._editor.RichText, + { + // summary: + // A rich text Editing widget + // + // description: + // This widget provides basic WYSIWYG editing features, based on the browser's + // underlying rich text editing capability, accompanied by a toolbar (`dijit.Toolbar`). + // A plugin model is available to extend the editor's capabilities as well as the + // the options available in the toolbar. Content generation may vary across + // browsers, and clipboard operations may have different results, to name + // a few limitations. Note: this widget should not be used with the HTML + // <TEXTAREA> tag -- see dijit._editor.RichText for details. + + // plugins: [const] Object[] + // A list of plugin names (as strings) or instances (as objects) + // for this widget. + // + // When declared in markup, it might look like: + // | plugins="['bold',{name:'dijit._editor.plugins.FontChoice', command:'fontName', generic:true}]" + plugins: null, + + // extraPlugins: [const] Object[] + // A list of extra plugin names which will be appended to plugins array + extraPlugins: null, + + constructor: function(){ + // summary: + // Runs on widget initialization to setup arrays etc. + // tags: + // private + + if(!dojo.isArray(this.plugins)){ + this.plugins=["undo","redo","|","cut","copy","paste","|","bold","italic","underline","strikethrough","|", + "insertOrderedList","insertUnorderedList","indent","outdent","|","justifyLeft","justifyRight","justifyCenter","justifyFull", + "dijit._editor.plugins.EnterKeyHandling" /*, "createLink"*/]; + } + + this._plugins=[]; + this._editInterval = this.editActionInterval * 1000; + + //IE will always lose focus when other element gets focus, while for FF and safari, + //when no iframe is used, focus will be lost whenever another element gets focus. + //For IE, we can connect to onBeforeDeactivate, which will be called right before + //the focus is lost, so we can obtain the selected range. For other browsers, + //no equivelent of onBeforeDeactivate, so we need to do two things to make sure + //selection is properly saved before focus is lost: 1) when user clicks another + //element in the page, in which case we listen to mousedown on the entire page and + //see whether user clicks out of a focus editor, if so, save selection (focus will + //only lost after onmousedown event is fired, so we can obtain correct caret pos.) + //2) when user tabs away from the editor, which is handled in onKeyDown below. + if(dojo.isIE){ + this.events.push("onBeforeDeactivate"); + this.events.push("onBeforeActivate"); + } + }, + + postMixInProperties: function() { + // summary: + // Extension to make sure a deferred is in place before certain functions + // execute, like making sure all the plugins are properly inserted. + + // Set up a deferred so that the value isn't applied to the editor + // until all the plugins load, needed to avoid timing condition + // reported in #10537. + this.setValueDeferred = new dojo.Deferred(); + this.inherited(arguments); + }, + + postCreate: function(){ + //for custom undo/redo, if enabled. + this._steps=this._steps.slice(0); + this._undoedSteps=this._undoedSteps.slice(0); + + if(dojo.isArray(this.extraPlugins)){ + this.plugins=this.plugins.concat(this.extraPlugins); + } + + this.inherited(arguments); + + this.commands = dojo.i18n.getLocalization("dijit._editor", "commands", this.lang); + + if(!this.toolbar){ + // if we haven't been assigned a toolbar, create one + this.toolbar = new dijit.Toolbar({ + dir: this.dir, + lang: this.lang + }); + this.header.appendChild(this.toolbar.domNode); + } + + dojo.forEach(this.plugins, this.addPlugin, this); + + // Okay, denote the value can now be set. + this.setValueDeferred.callback(true); + + dojo.addClass(this.iframe.parentNode, "dijitEditorIFrameContainer"); + dojo.addClass(this.iframe, "dijitEditorIFrame"); + dojo.attr(this.iframe, "allowTransparency", true); + + if(dojo.isWebKit){ + // Disable selecting the entire editor by inadvertant double-clicks. + // on buttons, title bar, etc. Otherwise clicking too fast on + // a button such as undo/redo selects the entire editor. + dojo.style(this.domNode, "KhtmlUserSelect", "none"); + } + this.toolbar.startup(); + this.onNormalizedDisplayChanged(); //update toolbar button status + }, + destroy: function(){ + dojo.forEach(this._plugins, function(p){ + if(p && p.destroy){ + p.destroy(); + } + }); + this._plugins=[]; + this.toolbar.destroyRecursive(); + delete this.toolbar; + this.inherited(arguments); + }, + addPlugin: function(/*String||Object*/plugin, /*Integer?*/index){ + // summary: + // takes a plugin name as a string or a plugin instance and + // adds it to the toolbar and associates it with this editor + // instance. The resulting plugin is added to the Editor's + // plugins array. If index is passed, it's placed in the plugins + // array at that index. No big magic, but a nice helper for + // passing in plugin names via markup. + // + // plugin: String, args object or plugin instance + // + // args: + // This object will be passed to the plugin constructor + // + // index: Integer + // Used when creating an instance from + // something already in this.plugins. Ensures that the new + // instance is assigned to this.plugins at that index. + var args=dojo.isString(plugin)?{name:plugin}:plugin; + if(!args.setEditor){ + var o={"args":args,"plugin":null,"editor":this}; + dojo.publish(dijit._scopeName + ".Editor.getPlugin",[o]); + if(!o.plugin){ + var pc = dojo.getObject(args.name); + if(pc){ + o.plugin=new pc(args); + } + } + if(!o.plugin){ + console.warn('Cannot find plugin',plugin); + return; + } + plugin=o.plugin; + } + if(arguments.length > 1){ + this._plugins[index] = plugin; + }else{ + this._plugins.push(plugin); + } + plugin.setEditor(this); + if(dojo.isFunction(plugin.setToolbar)){ + plugin.setToolbar(this.toolbar); + } + }, + //the following 3 functions are required to make the editor play nice under a layout widget, see #4070 + startup: function(){ + // summary: + // Exists to make Editor work as a child of a layout widget. + // Developers don't need to call this method. + // tags: + // protected + //console.log('startup',arguments); + }, + resize: function(size){ + // summary: + // Resize the editor to the specified size, see `dijit.layout._LayoutWidget.resize` + if(size){ + // we've been given a height/width for the entire editor (toolbar + contents), calls layout() + // to split the allocated size between the toolbar and the contents + dijit.layout._LayoutWidget.prototype.resize.apply(this, arguments); + } + /* + else{ + // do nothing, the editor is already laid out correctly. The user has probably specified + // the height parameter, which was used to set a size on the iframe + } + */ + }, + layout: function(){ + // summary: + // Called from `dijit.layout._LayoutWidget.resize`. This shouldn't be called directly + // tags: + // protected + + // Converts the iframe (or rather the <div> surrounding it) to take all the available space + // except what's needed for the header (toolbars) and footer (breadcrumbs, etc). + // A class was added to the iframe container and some themes style it, so we have to + // calc off the added margins and padding too. See tracker: #10662 + var areaHeight = (this._contentBox.h - + (this.getHeaderHeight() + this.getFooterHeight() + + dojo._getPadBorderExtents(this.iframe.parentNode).h + + dojo._getMarginExtents(this.iframe.parentNode).h)); + this.editingArea.style.height = areaHeight + "px"; + if(this.iframe){ + this.iframe.style.height="100%"; + } + this._layoutMode = true; + }, + _onIEMouseDown: function(/*Event*/ e){ + // summary: + // IE only to prevent 2 clicks to focus + // tags: + // private + var outsideClientArea; + // IE 8's componentFromPoint is broken, which is a shame since it + // was smaller code, but oh well. We have to do this brute force + // to detect if the click was scroller or not. + var b = this.document.body; + var clientWidth = b.clientWidth; + var clientHeight = b.clientHeight; + var clientLeft = b.clientLeft; + var offsetWidth = b.offsetWidth; + var offsetHeight = b.offsetHeight; + var offsetLeft = b.offsetLeft; + + //Check for vertical scroller click. + bodyDir = b.dir ? b.dir.toLowerCase() : ""; + if(bodyDir != "rtl"){ + if(clientWidth < offsetWidth && e.x > clientWidth && e.x < offsetWidth){ + // Check the click was between width and offset width, if so, scroller + outsideClientArea = true; + } + }else{ + // RTL mode, we have to go by the left offsets. + if(e.x < clientLeft && e.x > offsetLeft){ + // Check the click was between width and offset width, if so, scroller + outsideClientArea = true; + } + } + if(!outsideClientArea){ + // Okay, might be horiz scroller, check that. + if(clientHeight < offsetHeight && e.y > clientHeight && e.y < offsetHeight){ + // Horizontal scroller. + outsideClientArea = true; + } + } + if(!outsideClientArea){ + delete this._cursorToStart; // Remove the force to cursor to start position. + delete this._savedSelection; // new mouse position overrides old selection + if(e.target.tagName == "BODY"){ + setTimeout(dojo.hitch(this, "placeCursorAtEnd"), 0); + } + this.inherited(arguments); + } + }, + onBeforeActivate: function(e){ + this._restoreSelection(); + }, + onBeforeDeactivate: function(e){ + // summary: + // Called on IE right before focus is lost. Saves the selected range. + // tags: + // private + if(this.customUndo){ + this.endEditing(true); + } + //in IE, the selection will be lost when other elements get focus, + //let's save focus before the editor is deactivated + if(e.target.tagName != "BODY"){ + this._saveSelection(); + } + //console.log('onBeforeDeactivate',this); + }, + + /* beginning of custom undo/redo support */ + + // customUndo: Boolean + // Whether we shall use custom undo/redo support instead of the native + // browser support. By default, we now use custom undo. It works better + // than native browser support and provides a consistent behavior across + // browsers with a minimal performance hit. We already had the hit on + // the slowest browser, IE, anyway. + customUndo: true, + + // editActionInterval: Integer + // When using customUndo, not every keystroke will be saved as a step. + // Instead typing (including delete) will be grouped together: after + // a user stops typing for editActionInterval seconds, a step will be + // saved; if a user resume typing within editActionInterval seconds, + // the timeout will be restarted. By default, editActionInterval is 3 + // seconds. + editActionInterval: 3, + + beginEditing: function(cmd){ + // summary: + // Called to note that the user has started typing alphanumeric characters, if it's not already noted. + // Deals with saving undo; see editActionInterval parameter. + // tags: + // private + if(!this._inEditing){ + this._inEditing=true; + this._beginEditing(cmd); + } + if(this.editActionInterval>0){ + if(this._editTimer){ + clearTimeout(this._editTimer); + } + this._editTimer = setTimeout(dojo.hitch(this, this.endEditing), this._editInterval); + } + }, + + // TODO: declaring these in the prototype is meaningless, just create in the constructor/postCreate + _steps:[], + _undoedSteps:[], + + execCommand: function(cmd){ + // summary: + // Main handler for executing any commands to the editor, like paste, bold, etc. + // Called by plugins, but not meant to be called by end users. + // tags: + // protected + if(this.customUndo && (cmd == 'undo' || cmd == 'redo')){ + return this[cmd](); + }else{ + if(this.customUndo){ + this.endEditing(); + this._beginEditing(); + } + var r; + var isClipboard = /copy|cut|paste/.test(cmd); + try{ + r = this.inherited(arguments); + if(dojo.isWebKit && isClipboard && !r){ //see #4598: webkit does not guarantee clipboard support from js + throw { code: 1011 }; // throw an object like Mozilla's error + } + }catch(e){ + //TODO: when else might we get an exception? Do we need the Mozilla test below? + if(e.code == 1011 /* Mozilla: service denied */ && isClipboard){ + // Warn user of platform limitation. Cannot programmatically access clipboard. See ticket #4136 + var sub = dojo.string.substitute, + accel = {cut:'X', copy:'C', paste:'V'}; + alert(sub(this.commands.systemShortcut, + [this.commands[cmd], sub(this.commands[dojo.isMac ? 'appleKey' : 'ctrlKey'], [accel[cmd]])])); + } + r = false; + } + if(this.customUndo){ + this._endEditing(); + } + return r; + } + }, + queryCommandEnabled: function(cmd){ + // summary: + // Returns true if specified editor command is enabled. + // Used by the plugins to know when to highlight/not highlight buttons. + // tags: + // protected + if(this.customUndo && (cmd == 'undo' || cmd == 'redo')){ + return cmd == 'undo' ? (this._steps.length > 1) : (this._undoedSteps.length > 0); + }else{ + return this.inherited(arguments); + } + }, + _moveToBookmark: function(b){ + // summary: + // Selects the text specified in bookmark b + // tags: + // private + var bookmark = b.mark; + var mark = b.mark; + var col = b.isCollapsed; + var r, sNode, eNode, sel; + if(mark){ + if(dojo.isIE < 9){ + if(dojo.isArray(mark)){ + //IE CONTROL, have to use the native bookmark. + bookmark = []; + dojo.forEach(mark,function(n){ + bookmark.push(dijit.range.getNode(n,this.editNode)); + },this); + dojo.withGlobal(this.window,'moveToBookmark',dijit,[{mark: bookmark, isCollapsed: col}]); + }else{ + if(mark.startContainer && mark.endContainer){ + // Use the pseudo WC3 range API. This works better for positions + // than the IE native bookmark code. + sel = dijit.range.getSelection(this.window); + if(sel && sel.removeAllRanges){ + sel.removeAllRanges(); + r = dijit.range.create(this.window); + sNode = dijit.range.getNode(mark.startContainer,this.editNode); + eNode = dijit.range.getNode(mark.endContainer,this.editNode); + if(sNode && eNode){ + // Okay, we believe we found the position, so add it into the selection + // There are cases where it may not be found, particularly in undo/redo, when + // IE changes the underlying DOM on us (wraps text in a <p> tag or similar. + // So, in those cases, don't bother restoring selection. + r.setStart(sNode,mark.startOffset); + r.setEnd(eNode,mark.endOffset); + sel.addRange(r); + } + } + } + } + }else{//w3c range + sel = dijit.range.getSelection(this.window); + if(sel && sel.removeAllRanges){ + sel.removeAllRanges(); + r = dijit.range.create(this.window); + sNode = dijit.range.getNode(mark.startContainer,this.editNode); + eNode = dijit.range.getNode(mark.endContainer,this.editNode); + if(sNode && eNode){ + // Okay, we believe we found the position, so add it into the selection + // There are cases where it may not be found, particularly in undo/redo, when + // formatting as been done and so on, so don't restore selection then. + r.setStart(sNode,mark.startOffset); + r.setEnd(eNode,mark.endOffset); + sel.addRange(r); + } + } + } + } + }, + _changeToStep: function(from, to){ + // summary: + // Reverts editor to "to" setting, from the undo stack. + // tags: + // private + this.setValue(to.text); + var b=to.bookmark; + if(!b){ return; } + this._moveToBookmark(b); + }, + undo: function(){ + // summary: + // Handler for editor undo (ex: ctrl-z) operation + // tags: + // private + //console.log('undo'); + var ret = false; + if(!this._undoRedoActive){ + this._undoRedoActive = true; + this.endEditing(true); + var s=this._steps.pop(); + if(s && this._steps.length>0){ + this.focus(); + this._changeToStep(s,this._steps[this._steps.length-1]); + this._undoedSteps.push(s); + this.onDisplayChanged(); + delete this._undoRedoActive; + ret = true; + } + delete this._undoRedoActive; + } + return ret; + }, + redo: function(){ + // summary: + // Handler for editor redo (ex: ctrl-y) operation + // tags: + // private + //console.log('redo'); + var ret = false; + if(!this._undoRedoActive){ + this._undoRedoActive = true; + this.endEditing(true); + var s=this._undoedSteps.pop(); + if(s && this._steps.length>0){ + this.focus(); + this._changeToStep(this._steps[this._steps.length-1],s); + this._steps.push(s); + this.onDisplayChanged(); + ret = true; + } + delete this._undoRedoActive; + } + return ret; + }, + endEditing: function(ignore_caret){ + // summary: + // Called to note that the user has stopped typing alphanumeric characters, if it's not already noted. + // Deals with saving undo; see editActionInterval parameter. + // tags: + // private + if(this._editTimer){ + clearTimeout(this._editTimer); + } + if(this._inEditing){ + this._endEditing(ignore_caret); + this._inEditing=false; + } + }, + + _getBookmark: function(){ + // summary: + // Get the currently selected text + // tags: + // protected + var b=dojo.withGlobal(this.window,dijit.getBookmark); + var tmp=[]; + if(b && b.mark){ + var mark = b.mark; + if(dojo.isIE < 9){ + // Try to use the pseudo range API on IE for better accuracy. + var sel = dijit.range.getSelection(this.window); + if(!dojo.isArray(mark)){ + if(sel){ + var range; + if(sel.rangeCount){ + range = sel.getRangeAt(0); + } + if(range){ + b.mark = range.cloneRange(); + }else{ + b.mark = dojo.withGlobal(this.window,dijit.getBookmark); + } + } + }else{ + // Control ranges (img, table, etc), handle differently. + dojo.forEach(b.mark,function(n){ + tmp.push(dijit.range.getIndex(n,this.editNode).o); + },this); + b.mark = tmp; + } + } + try{ + if(b.mark && b.mark.startContainer){ + tmp=dijit.range.getIndex(b.mark.startContainer,this.editNode).o; + b.mark={startContainer:tmp, + startOffset:b.mark.startOffset, + endContainer:b.mark.endContainer===b.mark.startContainer?tmp:dijit.range.getIndex(b.mark.endContainer,this.editNode).o, + endOffset:b.mark.endOffset}; + } + }catch(e){ + b.mark = null; + } + } + return b; + }, + _beginEditing: function(cmd){ + // summary: + // Called when the user starts typing alphanumeric characters. + // Deals with saving undo; see editActionInterval parameter. + // tags: + // private + if(this._steps.length === 0){ + // You want to use the editor content without post filtering + // to make sure selection restores right for the 'initial' state. + // and undo is called. So not using this.value, as it was 'processed' + // and the line-up for selections may have been altered. + this._steps.push({'text':dijit._editor.getChildrenHtml(this.editNode),'bookmark':this._getBookmark()}); + } + }, + _endEditing: function(ignore_caret){ + // summary: + // Called when the user stops typing alphanumeric characters. + // Deals with saving undo; see editActionInterval parameter. + // tags: + // private + // Avoid filtering to make sure selections restore. + var v = dijit._editor.getChildrenHtml(this.editNode); + + this._undoedSteps=[];//clear undoed steps + this._steps.push({text: v, bookmark: this._getBookmark()}); + }, + onKeyDown: function(e){ + // summary: + // Handler for onkeydown event. + // tags: + // private + + //We need to save selection if the user TAB away from this editor + //no need to call _saveSelection for IE, as that will be taken care of in onBeforeDeactivate + if(!dojo.isIE && !this.iframe && e.keyCode == dojo.keys.TAB && !this.tabIndent){ + this._saveSelection(); + } + if(!this.customUndo){ + this.inherited(arguments); + return; + } + var k = e.keyCode, ks = dojo.keys; + if(e.ctrlKey && !e.altKey){//undo and redo only if the special right Alt + z/y are not pressed #5892 + if(k == 90 || k == 122){ //z + dojo.stopEvent(e); + this.undo(); + return; + }else if(k == 89 || k == 121){ //y + dojo.stopEvent(e); + this.redo(); + return; + } + } + this.inherited(arguments); + + switch(k){ + case ks.ENTER: + case ks.BACKSPACE: + case ks.DELETE: + this.beginEditing(); + break; + case 88: //x + case 86: //v + if(e.ctrlKey && !e.altKey && !e.metaKey){ + this.endEditing();//end current typing step if any + if(e.keyCode == 88){ + this.beginEditing('cut'); + //use timeout to trigger after the cut is complete + setTimeout(dojo.hitch(this, this.endEditing), 1); + }else{ + this.beginEditing('paste'); + //use timeout to trigger after the paste is complete + setTimeout(dojo.hitch(this, this.endEditing), 1); + } + break; + } + //pass through + default: + if(!e.ctrlKey && !e.altKey && !e.metaKey && (e.keyCode<dojo.keys.F1 || e.keyCode>dojo.keys.F15)){ + this.beginEditing(); + break; + } + //pass through + case ks.ALT: + this.endEditing(); + break; + case ks.UP_ARROW: + case ks.DOWN_ARROW: + case ks.LEFT_ARROW: + case ks.RIGHT_ARROW: + case ks.HOME: + case ks.END: + case ks.PAGE_UP: + case ks.PAGE_DOWN: + this.endEditing(true); + break; + //maybe ctrl+backspace/delete, so don't endEditing when ctrl is pressed + case ks.CTRL: + case ks.SHIFT: + case ks.TAB: + break; + } + }, + _onBlur: function(){ + // summary: + // Called from focus manager when focus has moved away from this editor + // tags: + // protected + + //this._saveSelection(); + this.inherited(arguments); + this.endEditing(true); + }, + _saveSelection: function(){ + // summary: + // Save the currently selected text in _savedSelection attribute + // tags: + // private + try{ + this._savedSelection=this._getBookmark(); + }catch(e){ /* Squelch any errors that occur if selection save occurs due to being hidden simultaniously. */} + }, + _restoreSelection: function(){ + // summary: + // Re-select the text specified in _savedSelection attribute; + // see _saveSelection(). + // tags: + // private + if(this._savedSelection){ + // Clear off cursor to start, we're deliberately going to a selection. + delete this._cursorToStart; + // only restore the selection if the current range is collapsed + // if not collapsed, then it means the editor does not lose + // selection and there is no need to restore it + if(dojo.withGlobal(this.window,'isCollapsed',dijit)){ + this._moveToBookmark(this._savedSelection); + } + delete this._savedSelection; + } + }, + + onClick: function(){ + // summary: + // Handler for when editor is clicked + // tags: + // protected + this.endEditing(true); + this.inherited(arguments); + }, + + replaceValue: function(/*String*/ html){ + // summary: + // over-ride of replaceValue to support custom undo and stack maintainence. + // tags: + // protected + if(!this.customUndo){ + this.inherited(arguments); + }else{ + if(this.isClosed){ + this.setValue(html); + }else{ + this.beginEditing(); + if(!html){ + html = " " + } + this.setValue(html); + this.endEditing(); + } + } + }, + + _setDisabledAttr: function(/*Boolean*/ value){ + var disableFunc = dojo.hitch(this, function(){ + if((!this.disabled && value) || (!this._buttonEnabledPlugins && value)){ + // Disable editor: disable all enabled buttons and remember that list + dojo.forEach(this._plugins, function(p){ + p.set("disabled", true); + }); + }else if(this.disabled && !value){ + // Restore plugins to being active. + dojo.forEach(this._plugins, function(p){ + p.set("disabled", false); + }); + } + }); + this.setValueDeferred.addCallback(disableFunc); + this.inherited(arguments); + }, + + _setStateClass: function(){ + try{ + this.inherited(arguments); + + // Let theme set the editor's text color based on editor enabled/disabled state. + // We need to jump through hoops because the main document (where the theme CSS is) + // is separate from the iframe's document. + if(this.document && this.document.body){ + dojo.style(this.document.body, "color", dojo.style(this.iframe, "color")); + } + }catch(e){ /* Squelch any errors caused by focus change if hidden during a state change */} + } + } +); + +// Register the "default plugins", ie, the built-in editor commands +dojo.subscribe(dijit._scopeName + ".Editor.getPlugin",null,function(o){ + if(o.plugin){ return; } + var args = o.args, p; + var _p = dijit._editor._Plugin; + var name = args.name; + switch(name){ + case "undo": case "redo": case "cut": case "copy": case "paste": case "insertOrderedList": + case "insertUnorderedList": case "indent": case "outdent": case "justifyCenter": + case "justifyFull": case "justifyLeft": case "justifyRight": case "delete": + case "selectAll": case "removeFormat": case "unlink": + case "insertHorizontalRule": + p = new _p({ command: name }); + break; + + case "bold": case "italic": case "underline": case "strikethrough": + case "subscript": case "superscript": + p = new _p({ buttonClass: dijit.form.ToggleButton, command: name }); + break; + case "|": + p = new _p({ button: new dijit.ToolbarSeparator(), setEditor: function(editor) {this.editor = editor;} }); + } +// console.log('name',name,p); + o.plugin=p; }); + } |