define("dijit/Editor", [ "dojo/_base/array", // array.forEach "dojo/_base/declare", // declare "dojo/_base/Deferred", // Deferred "dojo/i18n", // i18n.getLocalization "dojo/dom-attr", // domAttr.set "dojo/dom-class", // domClass.add "dojo/dom-geometry", "dojo/dom-style", // domStyle.set, get "dojo/_base/event", // event.stop "dojo/keys", // keys.F1 keys.F15 keys.TAB "dojo/_base/lang", // lang.getObject lang.hitch "dojo/_base/sniff", // has("ie") has("mac") has("webkit") "dojo/string", // string.substitute "dojo/topic", // topic.publish() "dojo/_base/window", // win.withGlobal "./_base/focus", // dijit.getBookmark() "./_Container", "./Toolbar", "./ToolbarSeparator", "./layout/_LayoutWidget", "./form/ToggleButton", "./_editor/_Plugin", "./_editor/plugins/EnterKeyHandling", "./_editor/html", "./_editor/range", "./_editor/RichText", ".", // dijit._scopeName "dojo/i18n!./_editor/nls/commands" ], function(array, declare, Deferred, i18n, domAttr, domClass, domGeometry, domStyle, event, keys, lang, has, string, topic, win, focusBase, _Container, Toolbar, ToolbarSeparator, _LayoutWidget, ToggleButton, _Plugin, EnterKeyHandling, html, rangeapi, RichText, dijit){ // module: // dijit/Editor // summary: // A rich text Editing widget var Editor = declare("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(!lang.isArray(this.plugins)){ this.plugins=["undo","redo","|","cut","copy","paste","|","bold","italic","underline","strikethrough","|", "insertOrderedList","insertUnorderedList","indent","outdent","|","justifyLeft","justifyRight","justifyCenter","justifyFull", 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 equivalent 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(has("ie")){ 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 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(lang.isArray(this.extraPlugins)){ this.plugins=this.plugins.concat(this.extraPlugins); } this.inherited(arguments); this.commands = i18n.getLocalization("dijit._editor", "commands", this.lang); if(!this.toolbar){ // if we haven't been assigned a toolbar, create one this.toolbar = new Toolbar({ dir: this.dir, lang: this.lang }); this.header.appendChild(this.toolbar.domNode); } array.forEach(this.plugins, this.addPlugin, this); // Okay, denote the value can now be set. this.setValueDeferred.callback(true); domClass.add(this.iframe.parentNode, "dijitEditorIFrameContainer"); domClass.add(this.iframe, "dijitEditorIFrame"); domAttr.set(this.iframe, "allowTransparency", true); if(has("webkit")){ // Disable selecting the entire editor by inadvertent double-clicks. // on buttons, title bar, etc. Otherwise clicking too fast on // a button such as undo/redo selects the entire editor. domStyle.set(this.domNode, "KhtmlUserSelect", "none"); } this.toolbar.startup(); this.onNormalizedDisplayChanged(); //update toolbar button status }, destroy: function(){ array.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||Function*/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, plugin instance, or plugin constructor // // 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=lang.isString(plugin)?{name:plugin}:lang.isFunction(plugin)?{ctor:plugin}:plugin; if(!args.setEditor){ var o={"args":args,"plugin":null,"editor":this}; if(args.name){ // search registry for a plugin factory matching args.name, if it's not there then // fallback to 1.0 API: // ask all loaded plugin modules to fill in o.plugin if they can (ie, if they implement args.name) // remove fallback for 2.0. if(_Plugin.registry[args.name]){ o.plugin = _Plugin.registry[args.name](args); }else{ topic.publish(dijit._scopeName + ".Editor.getPlugin", o); // publish } } if(!o.plugin){ var pc = args.ctor || lang.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(lang.isFunction(plugin.setToolbar)){ plugin.setToolbar(this.toolbar); } }, //the following 2 functions are required to make the editor play nice under a layout widget, see #4070 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 _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
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 = rangeapi.getSelection(this.window);
if(sel && sel.removeAllRanges){
sel.removeAllRanges();
r = rangeapi.create(this.window);
sNode = rangeapi.getNode(mark.startContainer,this.editNode);
eNode = rangeapi.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=win.withGlobal(this.window,focusBase.getBookmark);
var tmp=[];
if(b && b.mark){
var mark = b.mark;
if(has("ie") < 9){
// Try to use the pseudo range API on IE for better accuracy.
var sel = rangeapi.getSelection(this.window);
if(!lang.isArray(mark)){
if(sel){
var range;
if(sel.rangeCount){
range = sel.getRangeAt(0);
}
if(range){
b.mark = range.cloneRange();
}else{
b.mark = win.withGlobal(this.window,focusBase.getBookmark);
}
}
}else{
// Control ranges (img, table, etc), handle differently.
array.forEach(b.mark,function(n){
tmp.push(rangeapi.getIndex(n,this.editNode).o);
},this);
b.mark = tmp;
}
}
try{
if(b.mark && b.mark.startContainer){
tmp=rangeapi.getIndex(b.mark.startContainer,this.editNode).o;
b.mark={startContainer:tmp,
startOffset:b.mark.startOffset,
endContainer:b.mark.endContainer===b.mark.startContainer?tmp:rangeapi.getIndex(b.mark.endContainer,this.editNode).o,
endOffset:b.mark.endOffset};
}
}catch(e){
b.mark = null;
}
}
return b;
},
_beginEditing: function(){
// 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':html.getChildrenHtml(this.editNode),'bookmark':this._getBookmark()});
}
},
_endEditing: function(){
// 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 = html.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(!has("ie") && !this.iframe && e.keyCode == keys.TAB && !this.tabIndent){
this._saveSelection();
}
if(!this.customUndo){
this.inherited(arguments);
return;
}
var k = e.keyCode;
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
event.stop(e);
this.undo();
return;
}else if(k == 89 || k == 121){ //y
event.stop(e);
this.redo();
return;
}
}
this.inherited(arguments);
switch(k){
case keys.ENTER:
case keys.BACKSPACE:
case keys.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(lang.hitch(this, this.endEditing), 1);
}else{
this.beginEditing('paste');
//use timeout to trigger after the paste is complete
setTimeout(lang.hitch(this, this.endEditing), 1);
}
break;
}
//pass through
default:
if(!e.ctrlKey && !e.altKey && !e.metaKey && (e.keyCode