define([ "require", "dojo/_base/array", // array.forEach "dojo/_base/declare", // declare "dojo/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/keys", // keys.F1 keys.F15 keys.TAB "dojo/_base/lang", // lang.getObject lang.hitch "dojo/sniff", // has("ie") has("mac") has("webkit") "dojo/string", // string.substitute "dojo/topic", // topic.publish() "./_Container", "./Toolbar", "./ToolbarSeparator", "./layout/_LayoutWidget", "./form/ToggleButton", "./_editor/_Plugin", "./_editor/plugins/EnterKeyHandling", "./_editor/html", "./_editor/range", "./_editor/RichText", "./main", // dijit._scopeName "dojo/i18n!./_editor/nls/commands" ], function(require, array, declare, Deferred, i18n, domAttr, domClass, domGeometry, domStyle, keys, lang, has, string, topic, _Container, Toolbar, ToolbarSeparator, _LayoutWidget, ToggleButton, _Plugin, EnterKeyHandling, html, rangeapi, RichText, dijit){ // module: // dijit/Editor 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(/*===== params, srcNodeRef =====*/){ // summary: // Create the widget. // params: Object|null // Initial settings for any of the attributes, except readonly attributes. // srcNodeRef: DOMNode // The editor replaces the specified DOMNode. 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") || has("trident") || has("edge")){ 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(){ this.inherited(arguments); //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.commands = i18n.getLocalization("dijit._editor", "commands", this.lang); 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"); } }, startup: function(){ this.inherited(arguments); if(!this.toolbar){ // if we haven't been assigned a toolbar, create one this.toolbar = new Toolbar({ ownerDocument: this.ownerDocument, dir: this.dir, lang: this.lang, "aria-label": this.id }); this.header.appendChild(this.toolbar.domNode); } array.forEach(this.plugins, this.addPlugin, this); // Okay, denote the value can now be set. this.setValueDeferred.resolve(true); domClass.add(this.iframe.parentNode, "dijitEditorIFrameContainer"); domClass.add(this.iframe, "dijitEditorIFrame"); domAttr.set(this.iframe, "allowTransparency", true); 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: // 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){ try{ // TODO: remove lang.getObject() call in 2.0 var pc = args.ctor || lang.getObject(args.name) || require(args.name); if(pc){ o.plugin = new pc(args); } }catch(e){ throw new Error(this.id + ": cannot find plugin [" + args.name + "]"); } } if(!o.plugin){ throw new Error(this.id + ": cannot find plugin [" + args.name + "]"); } 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 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 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){ this._editTimer = this._editTimer.remove(); } if(this._inEditing){ this._endEditing(ignore_caret); this._inEditing = false; } }, _getBookmark: function(){ // summary: // Get the currently selected text // tags: // protected var b = this.selection.getBookmark(); var tmp = []; if(b && b.mark){ var mark = b.mark; if(has("ie") < 9 || (has("ie") === 9 && has("quirks"))){ // 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 = this.selection.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.shiftKey && !e.altKey){//undo and redo only if the special right Alt + z/y are not pressed #5892 if(k == 90 || k == 122){ //z, but also F11 key e.stopPropagation(); e.preventDefault(); this.undo(); return; }else if(k == 89 || k == 121){ //y e.stopPropagation(); e.preventDefault(); 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'); }else{ this.beginEditing('paste'); } //use timeout to trigger after the paste is complete this.defer("endEditing", 1); break; } //pass through default: if(!e.ctrlKey && !e.altKey && !e.metaKey && (e.keyCode < keys.F1 || e.keyCode > keys.F15)){ this.beginEditing(); break; } //pass through case keys.ALT: this.endEditing(); break; case keys.UP_ARROW: case keys.DOWN_ARROW: case keys.LEFT_ARROW: case keys.RIGHT_ARROW: case keys.HOME: case keys.END: case keys.PAGE_UP: case keys.PAGE_DOWN: this.endEditing(true); break; //maybe ctrl+backspace/delete, so don't endEditing when ctrl is pressed case keys.CTRL: case keys.SHIFT: case keys.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 simultaneously. */ } }, _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(this.selection.isCollapsed()){ 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 maintenance. // 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){ this.setValueDeferred.then(lang.hitch(this, function(){ if((!this.disabled && value) || (!this._buttonEnabledPlugins && value)){ // Disable editor: disable all enabled buttons and remember that list array.forEach(this._plugins, function(p){ p.set("disabled", true); }); }else if(this.disabled && !value){ // Restore plugins to being active. array.forEach(this._plugins, function(p){ p.set("disabled", false); }); } })); 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){ domStyle.set(this.document.body, "color", domStyle.get(this.iframe, "color")); domStyle.set(this.document.body, "background-color", domStyle.get(this.iframe, "background-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 function simplePluginFactory(args){ return new _Plugin({ command: args.name }); } function togglePluginFactory(args){ return new _Plugin({ buttonClass: ToggleButton, command: args.name }); } lang.mixin(_Plugin.registry, { "undo": simplePluginFactory, "redo": simplePluginFactory, "cut": simplePluginFactory, "copy": simplePluginFactory, "paste": simplePluginFactory, "insertOrderedList": simplePluginFactory, "insertUnorderedList": simplePluginFactory, "indent": simplePluginFactory, "outdent": simplePluginFactory, "justifyCenter": simplePluginFactory, "justifyFull": simplePluginFactory, "justifyLeft": simplePluginFactory, "justifyRight": simplePluginFactory, "delete": simplePluginFactory, "selectAll": simplePluginFactory, "removeFormat": simplePluginFactory, "unlink": simplePluginFactory, "insertHorizontalRule": simplePluginFactory, "bold": togglePluginFactory, "italic": togglePluginFactory, "underline": togglePluginFactory, "strikethrough": togglePluginFactory, "subscript": togglePluginFactory, "superscript": togglePluginFactory, "|": function(){ return new _Plugin({ setEditor: function(editor){ this.editor = editor; this.button = new ToolbarSeparator({ownerDocument: editor.ownerDocument}); } }); } }); return Editor; });