866 lines
28 KiB
JavaScript
866 lines
28 KiB
JavaScript
|
define("dijit/Editor", [
|
||
|
"require",
|
||
|
"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/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",
|
||
|
"./main", // dijit._scopeName
|
||
|
"dojo/i18n!./_editor/nls/commands"
|
||
|
], function(require, 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
|
||
|
|
||
|
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")){
|
||
|
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({
|
||
|
ownerDocument: this.ownerDocument,
|
||
|
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.resolve(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:
|
||
|
// 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 <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() +
|
||
|
domGeometry.getPadBorderExtents(this.iframe.parentNode).h +
|
||
|
domGeometry.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.
|
||
|
if(/^rtl$/i.test(b.dir || "")){
|
||
|
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"){
|
||
|
this.defer("placeCursorAtEnd");
|
||
|
}
|
||
|
this.inherited(arguments);
|
||
|
}
|
||
|
},
|
||
|
onBeforeActivate: function(){
|
||
|
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){
|
||
|
this._editTimer.remove();
|
||
|
}
|
||
|
this._editTimer = this.defer("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 = this.inherited(arguments);
|
||
|
if(this.customUndo){
|
||
|
this._endEditing();
|
||
|
}
|
||
|
return r;
|
||
|
}
|
||
|
},
|
||
|
|
||
|
_pasteImpl: function(){
|
||
|
// summary:
|
||
|
// Over-ride of paste command control to make execCommand cleaner
|
||
|
// tags:
|
||
|
// Protected
|
||
|
return this._clipboardCommand("paste");
|
||
|
},
|
||
|
|
||
|
_cutImpl: function(){
|
||
|
// summary:
|
||
|
// Over-ride of cut command control to make execCommand cleaner
|
||
|
// tags:
|
||
|
// Protected
|
||
|
return this._clipboardCommand("cut");
|
||
|
},
|
||
|
|
||
|
_copyImpl: function(){
|
||
|
// summary:
|
||
|
// Over-ride of copy command control to make execCommand cleaner
|
||
|
// tags:
|
||
|
// Protected
|
||
|
return this._clipboardCommand("copy");
|
||
|
},
|
||
|
|
||
|
_clipboardCommand: function(cmd){
|
||
|
// summary:
|
||
|
// Function to handle processing clipboard commands (or at least try to).
|
||
|
// tags:
|
||
|
// Private
|
||
|
var r;
|
||
|
try{
|
||
|
// Try to exec the superclass exec-command and see if it works.
|
||
|
r = this.document.execCommand(cmd, false, null);
|
||
|
if(has("webkit") && !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 */ ||
|
||
|
(e.code == 9 && has("opera") /* Opera not supported */)){
|
||
|
// Warn user of platform limitation. Cannot programmatically access clipboard. See ticket #4136
|
||
|
var sub = string.substitute,
|
||
|
accel = {cut:'X', copy:'C', paste:'V'};
|
||
|
alert(sub(this.commands.systemShortcut,
|
||
|
[this.commands[cmd], sub(this.commands[has("mac") ? 'appleKey' : 'ctrlKey'], [accel[cmd]])]));
|
||
|
}
|
||
|
r = false;
|
||
|
}
|
||
|
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(has("ie") < 9){
|
||
|
if(lang.isArray(mark)){
|
||
|
//IE CONTROL, have to use the native bookmark.
|
||
|
bookmark = [];
|
||
|
array.forEach(mark,function(n){
|
||
|
bookmark.push(rangeapi.getNode(n,this.editNode));
|
||
|
},this);
|
||
|
win.withGlobal(this.window,'moveToBookmark',focusBase,[{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 = 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
|
||
|
// 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 = 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=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');
|
||
|
}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(win.withGlobal(this.window,'isCollapsed',focusBase)){
|
||
|
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"));
|
||
|
}
|
||
|
}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;
|
||
|
});
|