mirror of
				https://git.tt-rss.org/fox/tt-rss.git
				synced 2025-11-04 05:41:29 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			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;
 | 
						|
});
 |