]>
Commit | Line | Data |
---|---|---|
f0cfe83e AD |
1 | define("dijit/Editor", [ |
2 | "require", | |
3 | "dojo/_base/array", // array.forEach | |
4 | "dojo/_base/declare", // declare | |
5 | "dojo/_base/Deferred", // Deferred | |
6 | "dojo/i18n", // i18n.getLocalization | |
7 | "dojo/dom-attr", // domAttr.set | |
8 | "dojo/dom-class", // domClass.add | |
9 | "dojo/dom-geometry", | |
10 | "dojo/dom-style", // domStyle.set, get | |
11 | "dojo/_base/event", // event.stop | |
12 | "dojo/keys", // keys.F1 keys.F15 keys.TAB | |
13 | "dojo/_base/lang", // lang.getObject lang.hitch | |
14 | "dojo/sniff", // has("ie") has("mac") has("webkit") | |
15 | "dojo/string", // string.substitute | |
16 | "dojo/topic", // topic.publish() | |
17 | "dojo/_base/window", // win.withGlobal | |
18 | "./_base/focus", // dijit.getBookmark() | |
19 | "./_Container", | |
20 | "./Toolbar", | |
21 | "./ToolbarSeparator", | |
22 | "./layout/_LayoutWidget", | |
23 | "./form/ToggleButton", | |
24 | "./_editor/_Plugin", | |
25 | "./_editor/plugins/EnterKeyHandling", | |
26 | "./_editor/html", | |
27 | "./_editor/range", | |
28 | "./_editor/RichText", | |
29 | "./main", // dijit._scopeName | |
30 | "dojo/i18n!./_editor/nls/commands" | |
31 | ], function(require, array, declare, Deferred, i18n, domAttr, domClass, domGeometry, domStyle, | |
32 | event, keys, lang, has, string, topic, win, | |
33 | focusBase, _Container, Toolbar, ToolbarSeparator, _LayoutWidget, ToggleButton, | |
34 | _Plugin, EnterKeyHandling, html, rangeapi, RichText, dijit){ | |
35 | ||
36 | // module: | |
37 | // dijit/Editor | |
38 | ||
39 | var Editor = declare("dijit.Editor", RichText, { | |
40 | // summary: | |
41 | // A rich text Editing widget | |
42 | // | |
43 | // description: | |
44 | // This widget provides basic WYSIWYG editing features, based on the browser's | |
45 | // underlying rich text editing capability, accompanied by a toolbar (`dijit.Toolbar`). | |
46 | // A plugin model is available to extend the editor's capabilities as well as the | |
47 | // the options available in the toolbar. Content generation may vary across | |
48 | // browsers, and clipboard operations may have different results, to name | |
49 | // a few limitations. Note: this widget should not be used with the HTML | |
50 | // <TEXTAREA> tag -- see dijit/_editor/RichText for details. | |
51 | ||
52 | // plugins: [const] Object[] | |
53 | // A list of plugin names (as strings) or instances (as objects) | |
54 | // for this widget. | |
55 | // | |
56 | // When declared in markup, it might look like: | |
57 | // | plugins="['bold',{name:'dijit._editor.plugins.FontChoice', command:'fontName', generic:true}]" | |
58 | plugins: null, | |
59 | ||
60 | // extraPlugins: [const] Object[] | |
61 | // A list of extra plugin names which will be appended to plugins array | |
62 | extraPlugins: null, | |
63 | ||
64 | constructor: function(/*===== params, srcNodeRef =====*/){ | |
65 | // summary: | |
66 | // Create the widget. | |
67 | // params: Object|null | |
68 | // Initial settings for any of the attributes, except readonly attributes. | |
69 | // srcNodeRef: DOMNode | |
70 | // The editor replaces the specified DOMNode. | |
71 | ||
72 | if(!lang.isArray(this.plugins)){ | |
73 | this.plugins=["undo","redo","|","cut","copy","paste","|","bold","italic","underline","strikethrough","|", | |
74 | "insertOrderedList","insertUnorderedList","indent","outdent","|","justifyLeft","justifyRight","justifyCenter","justifyFull", | |
75 | EnterKeyHandling /*, "createLink"*/]; | |
76 | } | |
77 | ||
78 | this._plugins=[]; | |
79 | this._editInterval = this.editActionInterval * 1000; | |
80 | ||
81 | //IE will always lose focus when other element gets focus, while for FF and safari, | |
82 | //when no iframe is used, focus will be lost whenever another element gets focus. | |
83 | //For IE, we can connect to onBeforeDeactivate, which will be called right before | |
84 | //the focus is lost, so we can obtain the selected range. For other browsers, | |
85 | //no equivalent of onBeforeDeactivate, so we need to do two things to make sure | |
86 | //selection is properly saved before focus is lost: 1) when user clicks another | |
87 | //element in the page, in which case we listen to mousedown on the entire page and | |
88 | //see whether user clicks out of a focus editor, if so, save selection (focus will | |
89 | //only lost after onmousedown event is fired, so we can obtain correct caret pos.) | |
90 | //2) when user tabs away from the editor, which is handled in onKeyDown below. | |
91 | if(has("ie")){ | |
92 | this.events.push("onBeforeDeactivate"); | |
93 | this.events.push("onBeforeActivate"); | |
94 | } | |
95 | }, | |
96 | ||
97 | postMixInProperties: function(){ | |
98 | // summary: | |
99 | // Extension to make sure a deferred is in place before certain functions | |
100 | // execute, like making sure all the plugins are properly inserted. | |
101 | ||
102 | // Set up a deferred so that the value isn't applied to the editor | |
103 | // until all the plugins load, needed to avoid timing condition | |
104 | // reported in #10537. | |
105 | this.setValueDeferred = new Deferred(); | |
106 | this.inherited(arguments); | |
107 | }, | |
108 | ||
109 | postCreate: function(){ | |
110 | //for custom undo/redo, if enabled. | |
111 | this._steps=this._steps.slice(0); | |
112 | this._undoedSteps=this._undoedSteps.slice(0); | |
113 | ||
114 | if(lang.isArray(this.extraPlugins)){ | |
115 | this.plugins=this.plugins.concat(this.extraPlugins); | |
116 | } | |
117 | ||
118 | this.inherited(arguments); | |
119 | ||
120 | this.commands = i18n.getLocalization("dijit._editor", "commands", this.lang); | |
121 | ||
122 | if(!this.toolbar){ | |
123 | // if we haven't been assigned a toolbar, create one | |
124 | this.toolbar = new Toolbar({ | |
125 | ownerDocument: this.ownerDocument, | |
126 | dir: this.dir, | |
127 | lang: this.lang | |
128 | }); | |
129 | this.header.appendChild(this.toolbar.domNode); | |
130 | } | |
131 | ||
132 | array.forEach(this.plugins, this.addPlugin, this); | |
133 | ||
134 | // Okay, denote the value can now be set. | |
135 | this.setValueDeferred.resolve(true); | |
136 | ||
137 | domClass.add(this.iframe.parentNode, "dijitEditorIFrameContainer"); | |
138 | domClass.add(this.iframe, "dijitEditorIFrame"); | |
139 | domAttr.set(this.iframe, "allowTransparency", true); | |
140 | ||
141 | if(has("webkit")){ | |
142 | // Disable selecting the entire editor by inadvertent double-clicks. | |
143 | // on buttons, title bar, etc. Otherwise clicking too fast on | |
144 | // a button such as undo/redo selects the entire editor. | |
145 | domStyle.set(this.domNode, "KhtmlUserSelect", "none"); | |
146 | } | |
147 | this.toolbar.startup(); | |
148 | this.onNormalizedDisplayChanged(); //update toolbar button status | |
149 | }, | |
150 | destroy: function(){ | |
151 | array.forEach(this._plugins, function(p){ | |
152 | if(p && p.destroy){ | |
153 | p.destroy(); | |
154 | } | |
155 | }); | |
156 | this._plugins=[]; | |
157 | this.toolbar.destroyRecursive(); | |
158 | delete this.toolbar; | |
159 | this.inherited(arguments); | |
160 | }, | |
161 | addPlugin: function(/*String||Object||Function*/ plugin, /*Integer?*/ index){ | |
162 | // summary: | |
163 | // takes a plugin name as a string or a plugin instance and | |
164 | // adds it to the toolbar and associates it with this editor | |
165 | // instance. The resulting plugin is added to the Editor's | |
166 | // plugins array. If index is passed, it's placed in the plugins | |
167 | // array at that index. No big magic, but a nice helper for | |
168 | // passing in plugin names via markup. | |
169 | // plugin: | |
170 | // String, args object, plugin instance, or plugin constructor | |
171 | // args: | |
172 | // This object will be passed to the plugin constructor | |
173 | // index: | |
174 | // Used when creating an instance from | |
175 | // something already in this.plugins. Ensures that the new | |
176 | // instance is assigned to this.plugins at that index. | |
177 | var args=lang.isString(plugin)?{name:plugin}:lang.isFunction(plugin)?{ctor:plugin}:plugin; | |
178 | if(!args.setEditor){ | |
179 | var o={"args":args,"plugin":null,"editor":this}; | |
180 | if(args.name){ | |
181 | // search registry for a plugin factory matching args.name, if it's not there then | |
182 | // fallback to 1.0 API: | |
183 | // ask all loaded plugin modules to fill in o.plugin if they can (ie, if they implement args.name) | |
184 | // remove fallback for 2.0. | |
185 | if(_Plugin.registry[args.name]){ | |
186 | o.plugin = _Plugin.registry[args.name](args); | |
187 | }else{ | |
188 | topic.publish(dijit._scopeName + ".Editor.getPlugin", o); // publish | |
189 | } | |
190 | } | |
191 | if(!o.plugin){ | |
192 | try{ | |
193 | // TODO: remove lang.getObject() call in 2.0 | |
194 | var pc = args.ctor || lang.getObject(args.name) || require(args.name); | |
195 | if(pc){ | |
196 | o.plugin = new pc(args); | |
197 | } | |
198 | }catch(e){ | |
199 | throw new Error(this.id + ": cannot find plugin [" + args.name + "]"); | |
200 | } | |
201 | } | |
202 | if(!o.plugin){ | |
203 | throw new Error(this.id + ": cannot find plugin [" + args.name + "]"); | |
204 | } | |
205 | plugin=o.plugin; | |
206 | } | |
207 | if(arguments.length > 1){ | |
208 | this._plugins[index] = plugin; | |
209 | }else{ | |
210 | this._plugins.push(plugin); | |
211 | } | |
212 | plugin.setEditor(this); | |
213 | if(lang.isFunction(plugin.setToolbar)){ | |
214 | plugin.setToolbar(this.toolbar); | |
215 | } | |
216 | }, | |
217 | ||
218 | //the following 2 functions are required to make the editor play nice under a layout widget, see #4070 | |
219 | ||
220 | resize: function(size){ | |
221 | // summary: | |
222 | // Resize the editor to the specified size, see `dijit/layout/_LayoutWidget.resize()` | |
223 | if(size){ | |
224 | // we've been given a height/width for the entire editor (toolbar + contents), calls layout() | |
225 | // to split the allocated size between the toolbar and the contents | |
226 | _LayoutWidget.prototype.resize.apply(this, arguments); | |
227 | } | |
228 | /* | |
229 | else{ | |
230 | // do nothing, the editor is already laid out correctly. The user has probably specified | |
231 | // the height parameter, which was used to set a size on the iframe | |
232 | } | |
233 | */ | |
234 | }, | |
235 | layout: function(){ | |
236 | // summary: | |
237 | // Called from `dijit/layout/_LayoutWidget.resize()`. This shouldn't be called directly | |
238 | // tags: | |
239 | // protected | |
240 | ||
241 | // Converts the iframe (or rather the <div> surrounding it) to take all the available space | |
242 | // except what's needed for the header (toolbars) and footer (breadcrumbs, etc). | |
243 | // A class was added to the iframe container and some themes style it, so we have to | |
244 | // calc off the added margins and padding too. See tracker: #10662 | |
245 | var areaHeight = (this._contentBox.h - | |
246 | (this.getHeaderHeight() + this.getFooterHeight() + | |
247 | domGeometry.getPadBorderExtents(this.iframe.parentNode).h + | |
248 | domGeometry.getMarginExtents(this.iframe.parentNode).h)); | |
249 | this.editingArea.style.height = areaHeight + "px"; | |
250 | if(this.iframe){ | |
251 | this.iframe.style.height="100%"; | |
252 | } | |
253 | this._layoutMode = true; | |
254 | }, | |
255 | ||
256 | _onIEMouseDown: function(/*Event*/ e){ | |
257 | // summary: | |
258 | // IE only to prevent 2 clicks to focus | |
259 | // tags: | |
260 | // private | |
261 | var outsideClientArea; | |
262 | // IE 8's componentFromPoint is broken, which is a shame since it | |
263 | // was smaller code, but oh well. We have to do this brute force | |
264 | // to detect if the click was scroller or not. | |
265 | var b = this.document.body; | |
266 | var clientWidth = b.clientWidth; | |
267 | var clientHeight = b.clientHeight; | |
268 | var clientLeft = b.clientLeft; | |
269 | var offsetWidth = b.offsetWidth; | |
270 | var offsetHeight = b.offsetHeight; | |
271 | var offsetLeft = b.offsetLeft; | |
272 | ||
273 | //Check for vertical scroller click. | |
274 | if(/^rtl$/i.test(b.dir || "")){ | |
275 | if(clientWidth < offsetWidth && e.x > clientWidth && e.x < offsetWidth){ | |
276 | // Check the click was between width and offset width, if so, scroller | |
277 | outsideClientArea = true; | |
278 | } | |
279 | }else{ | |
280 | // RTL mode, we have to go by the left offsets. | |
281 | if(e.x < clientLeft && e.x > offsetLeft){ | |
282 | // Check the click was between width and offset width, if so, scroller | |
283 | outsideClientArea = true; | |
284 | } | |
285 | } | |
286 | if(!outsideClientArea){ | |
287 | // Okay, might be horiz scroller, check that. | |
288 | if(clientHeight < offsetHeight && e.y > clientHeight && e.y < offsetHeight){ | |
289 | // Horizontal scroller. | |
290 | outsideClientArea = true; | |
291 | } | |
292 | } | |
293 | if(!outsideClientArea){ | |
294 | delete this._cursorToStart; // Remove the force to cursor to start position. | |
295 | delete this._savedSelection; // new mouse position overrides old selection | |
296 | if(e.target.tagName == "BODY"){ | |
297 | this.defer("placeCursorAtEnd"); | |
298 | } | |
299 | this.inherited(arguments); | |
300 | } | |
301 | }, | |
302 | onBeforeActivate: function(){ | |
303 | this._restoreSelection(); | |
304 | }, | |
305 | onBeforeDeactivate: function(e){ | |
306 | // summary: | |
307 | // Called on IE right before focus is lost. Saves the selected range. | |
308 | // tags: | |
309 | // private | |
310 | if(this.customUndo){ | |
311 | this.endEditing(true); | |
312 | } | |
313 | //in IE, the selection will be lost when other elements get focus, | |
314 | //let's save focus before the editor is deactivated | |
315 | if(e.target.tagName != "BODY"){ | |
316 | this._saveSelection(); | |
317 | } | |
318 | //console.log('onBeforeDeactivate',this); | |
319 | }, | |
320 | ||
321 | /* beginning of custom undo/redo support */ | |
322 | ||
323 | // customUndo: Boolean | |
324 | // Whether we shall use custom undo/redo support instead of the native | |
325 | // browser support. By default, we now use custom undo. It works better | |
326 | // than native browser support and provides a consistent behavior across | |
327 | // browsers with a minimal performance hit. We already had the hit on | |
328 | // the slowest browser, IE, anyway. | |
329 | customUndo: true, | |
330 | ||
331 | // editActionInterval: Integer | |
332 | // When using customUndo, not every keystroke will be saved as a step. | |
333 | // Instead typing (including delete) will be grouped together: after | |
334 | // a user stops typing for editActionInterval seconds, a step will be | |
335 | // saved; if a user resume typing within editActionInterval seconds, | |
336 | // the timeout will be restarted. By default, editActionInterval is 3 | |
337 | // seconds. | |
338 | editActionInterval: 3, | |
339 | ||
340 | beginEditing: function(cmd){ | |
341 | // summary: | |
342 | // Called to note that the user has started typing alphanumeric characters, if it's not already noted. | |
343 | // Deals with saving undo; see editActionInterval parameter. | |
344 | // tags: | |
345 | // private | |
346 | if(!this._inEditing){ | |
347 | this._inEditing=true; | |
348 | this._beginEditing(cmd); | |
349 | } | |
350 | if(this.editActionInterval>0){ | |
351 | if(this._editTimer){ | |
352 | this._editTimer.remove(); | |
353 | } | |
354 | this._editTimer = this.defer("endEditing", this._editInterval); | |
355 | } | |
356 | }, | |
357 | ||
358 | // TODO: declaring these in the prototype is meaningless, just create in the constructor/postCreate | |
359 | _steps:[], | |
360 | _undoedSteps:[], | |
361 | ||
362 | execCommand: function(cmd){ | |
363 | // summary: | |
364 | // Main handler for executing any commands to the editor, like paste, bold, etc. | |
365 | // Called by plugins, but not meant to be called by end users. | |
366 | // tags: | |
367 | // protected | |
368 | if(this.customUndo && (cmd == 'undo' || cmd == 'redo')){ | |
369 | return this[cmd](); | |
370 | }else{ | |
371 | if(this.customUndo){ | |
372 | this.endEditing(); | |
373 | this._beginEditing(); | |
374 | } | |
375 | var r = this.inherited(arguments); | |
376 | if(this.customUndo){ | |
377 | this._endEditing(); | |
378 | } | |
379 | return r; | |
380 | } | |
381 | }, | |
382 | ||
383 | _pasteImpl: function(){ | |
384 | // summary: | |
385 | // Over-ride of paste command control to make execCommand cleaner | |
386 | // tags: | |
387 | // Protected | |
388 | return this._clipboardCommand("paste"); | |
389 | }, | |
390 | ||
391 | _cutImpl: function(){ | |
392 | // summary: | |
393 | // Over-ride of cut command control to make execCommand cleaner | |
394 | // tags: | |
395 | // Protected | |
396 | return this._clipboardCommand("cut"); | |
397 | }, | |
398 | ||
399 | _copyImpl: function(){ | |
400 | // summary: | |
401 | // Over-ride of copy command control to make execCommand cleaner | |
402 | // tags: | |
403 | // Protected | |
404 | return this._clipboardCommand("copy"); | |
405 | }, | |
406 | ||
407 | _clipboardCommand: function(cmd){ | |
408 | // summary: | |
409 | // Function to handle processing clipboard commands (or at least try to). | |
410 | // tags: | |
411 | // Private | |
412 | var r; | |
413 | try{ | |
414 | // Try to exec the superclass exec-command and see if it works. | |
415 | r = this.document.execCommand(cmd, false, null); | |
416 | if(has("webkit") && !r){ //see #4598: webkit does not guarantee clipboard support from js | |
417 | throw { code: 1011 }; // throw an object like Mozilla's error | |
418 | } | |
419 | }catch(e){ | |
420 | //TODO: when else might we get an exception? Do we need the Mozilla test below? | |
421 | if(e.code == 1011 /* Mozilla: service denied */ || | |
422 | (e.code == 9 && has("opera") /* Opera not supported */)){ | |
423 | // Warn user of platform limitation. Cannot programmatically access clipboard. See ticket #4136 | |
424 | var sub = string.substitute, | |
425 | accel = {cut:'X', copy:'C', paste:'V'}; | |
426 | alert(sub(this.commands.systemShortcut, | |
427 | [this.commands[cmd], sub(this.commands[has("mac") ? 'appleKey' : 'ctrlKey'], [accel[cmd]])])); | |
428 | } | |
429 | r = false; | |
430 | } | |
431 | return r; | |
432 | }, | |
433 | ||
434 | queryCommandEnabled: function(cmd){ | |
435 | // summary: | |
436 | // Returns true if specified editor command is enabled. | |
437 | // Used by the plugins to know when to highlight/not highlight buttons. | |
438 | // tags: | |
439 | // protected | |
440 | if(this.customUndo && (cmd == 'undo' || cmd == 'redo')){ | |
441 | return cmd == 'undo' ? (this._steps.length > 1) : (this._undoedSteps.length > 0); | |
442 | }else{ | |
443 | return this.inherited(arguments); | |
444 | } | |
445 | }, | |
446 | _moveToBookmark: function(b){ | |
447 | // summary: | |
448 | // Selects the text specified in bookmark b | |
449 | // tags: | |
450 | // private | |
451 | var bookmark = b.mark; | |
452 | var mark = b.mark; | |
453 | var col = b.isCollapsed; | |
454 | var r, sNode, eNode, sel; | |
455 | if(mark){ | |
456 | if(has("ie") < 9){ | |
457 | if(lang.isArray(mark)){ | |
458 | //IE CONTROL, have to use the native bookmark. | |
459 | bookmark = []; | |
460 | array.forEach(mark,function(n){ | |
461 | bookmark.push(rangeapi.getNode(n,this.editNode)); | |
462 | },this); | |
463 | win.withGlobal(this.window,'moveToBookmark',focusBase,[{mark: bookmark, isCollapsed: col}]); | |
464 | }else{ | |
465 | if(mark.startContainer && mark.endContainer){ | |
466 | // Use the pseudo WC3 range API. This works better for positions | |
467 | // than the IE native bookmark code. | |
468 | sel = rangeapi.getSelection(this.window); | |
469 | if(sel && sel.removeAllRanges){ | |
470 | sel.removeAllRanges(); | |
471 | r = rangeapi.create(this.window); | |
472 | sNode = rangeapi.getNode(mark.startContainer,this.editNode); | |
473 | eNode = rangeapi.getNode(mark.endContainer,this.editNode); | |
474 | if(sNode && eNode){ | |
475 | // Okay, we believe we found the position, so add it into the selection | |
476 | // There are cases where it may not be found, particularly in undo/redo, when | |
477 | // IE changes the underlying DOM on us (wraps text in a <p> tag or similar. | |
478 | // So, in those cases, don't bother restoring selection. | |
479 | r.setStart(sNode,mark.startOffset); | |
480 | r.setEnd(eNode,mark.endOffset); | |
481 | sel.addRange(r); | |
482 | } | |
483 | } | |
484 | } | |
485 | } | |
486 | }else{//w3c range | |
487 | sel = rangeapi.getSelection(this.window); | |
488 | if(sel && sel.removeAllRanges){ | |
489 | sel.removeAllRanges(); | |
490 | r = rangeapi.create(this.window); | |
491 | sNode = rangeapi.getNode(mark.startContainer,this.editNode); | |
492 | eNode = rangeapi.getNode(mark.endContainer,this.editNode); | |
493 | if(sNode && eNode){ | |
494 | // Okay, we believe we found the position, so add it into the selection | |
495 | // There are cases where it may not be found, particularly in undo/redo, when | |
496 | // formatting as been done and so on, so don't restore selection then. | |
497 | r.setStart(sNode,mark.startOffset); | |
498 | r.setEnd(eNode,mark.endOffset); | |
499 | sel.addRange(r); | |
500 | } | |
501 | } | |
502 | } | |
503 | } | |
504 | }, | |
505 | _changeToStep: function(from, to){ | |
506 | // summary: | |
507 | // Reverts editor to "to" setting, from the undo stack. | |
508 | // tags: | |
509 | // private | |
510 | this.setValue(to.text); | |
511 | var b=to.bookmark; | |
512 | if(!b){ return; } | |
513 | this._moveToBookmark(b); | |
514 | }, | |
515 | undo: function(){ | |
516 | // summary: | |
517 | // Handler for editor undo (ex: ctrl-z) operation | |
518 | // tags: | |
519 | // private | |
520 | var ret = false; | |
521 | if(!this._undoRedoActive){ | |
522 | this._undoRedoActive = true; | |
523 | this.endEditing(true); | |
524 | var s=this._steps.pop(); | |
525 | if(s && this._steps.length>0){ | |
526 | this.focus(); | |
527 | this._changeToStep(s,this._steps[this._steps.length-1]); | |
528 | this._undoedSteps.push(s); | |
529 | this.onDisplayChanged(); | |
530 | delete this._undoRedoActive; | |
531 | ret = true; | |
532 | } | |
533 | delete this._undoRedoActive; | |
534 | } | |
535 | return ret; | |
536 | }, | |
537 | redo: function(){ | |
538 | // summary: | |
539 | // Handler for editor redo (ex: ctrl-y) operation | |
540 | // tags: | |
541 | // private | |
542 | var ret = false; | |
543 | if(!this._undoRedoActive){ | |
544 | this._undoRedoActive = true; | |
545 | this.endEditing(true); | |
546 | var s=this._undoedSteps.pop(); | |
547 | if(s && this._steps.length>0){ | |
548 | this.focus(); | |
549 | this._changeToStep(this._steps[this._steps.length-1],s); | |
550 | this._steps.push(s); | |
551 | this.onDisplayChanged(); | |
552 | ret = true; | |
553 | } | |
554 | delete this._undoRedoActive; | |
555 | } | |
556 | return ret; | |
557 | }, | |
558 | endEditing: function(ignore_caret){ | |
559 | // summary: | |
560 | // Called to note that the user has stopped typing alphanumeric characters, if it's not already noted. | |
561 | // Deals with saving undo; see editActionInterval parameter. | |
562 | // tags: | |
563 | // private | |
564 | if(this._editTimer){ | |
565 | this._editTimer = this._editTimer.remove(); | |
566 | } | |
567 | if(this._inEditing){ | |
568 | this._endEditing(ignore_caret); | |
569 | this._inEditing=false; | |
570 | } | |
571 | }, | |
572 | ||
573 | _getBookmark: function(){ | |
574 | // summary: | |
575 | // Get the currently selected text | |
576 | // tags: | |
577 | // protected | |
578 | var b=win.withGlobal(this.window,focusBase.getBookmark); | |
579 | var tmp=[]; | |
580 | if(b && b.mark){ | |
581 | var mark = b.mark; | |
582 | if(has("ie") < 9){ | |
583 | // Try to use the pseudo range API on IE for better accuracy. | |
584 | var sel = rangeapi.getSelection(this.window); | |
585 | if(!lang.isArray(mark)){ | |
586 | if(sel){ | |
587 | var range; | |
588 | if(sel.rangeCount){ | |
589 | range = sel.getRangeAt(0); | |
590 | } | |
591 | if(range){ | |
592 | b.mark = range.cloneRange(); | |
593 | }else{ | |
594 | b.mark = win.withGlobal(this.window,focusBase.getBookmark); | |
595 | } | |
596 | } | |
597 | }else{ | |
598 | // Control ranges (img, table, etc), handle differently. | |
599 | array.forEach(b.mark,function(n){ | |
600 | tmp.push(rangeapi.getIndex(n,this.editNode).o); | |
601 | },this); | |
602 | b.mark = tmp; | |
603 | } | |
604 | } | |
605 | try{ | |
606 | if(b.mark && b.mark.startContainer){ | |
607 | tmp=rangeapi.getIndex(b.mark.startContainer,this.editNode).o; | |
608 | b.mark={startContainer:tmp, | |
609 | startOffset:b.mark.startOffset, | |
610 | endContainer:b.mark.endContainer===b.mark.startContainer?tmp:rangeapi.getIndex(b.mark.endContainer,this.editNode).o, | |
611 | endOffset:b.mark.endOffset}; | |
612 | } | |
613 | }catch(e){ | |
614 | b.mark = null; | |
615 | } | |
616 | } | |
617 | return b; | |
618 | }, | |
619 | _beginEditing: function(){ | |
620 | // summary: | |
621 | // Called when the user starts typing alphanumeric characters. | |
622 | // Deals with saving undo; see editActionInterval parameter. | |
623 | // tags: | |
624 | // private | |
625 | if(this._steps.length === 0){ | |
626 | // You want to use the editor content without post filtering | |
627 | // to make sure selection restores right for the 'initial' state. | |
628 | // and undo is called. So not using this.value, as it was 'processed' | |
629 | // and the line-up for selections may have been altered. | |
630 | this._steps.push({'text':html.getChildrenHtml(this.editNode),'bookmark':this._getBookmark()}); | |
631 | } | |
632 | }, | |
633 | _endEditing: function(){ | |
634 | // summary: | |
635 | // Called when the user stops typing alphanumeric characters. | |
636 | // Deals with saving undo; see editActionInterval parameter. | |
637 | // tags: | |
638 | // private | |
639 | ||
640 | // Avoid filtering to make sure selections restore. | |
641 | var v = html.getChildrenHtml(this.editNode); | |
642 | ||
643 | this._undoedSteps=[];//clear undoed steps | |
644 | this._steps.push({text: v, bookmark: this._getBookmark()}); | |
645 | }, | |
646 | onKeyDown: function(e){ | |
647 | // summary: | |
648 | // Handler for onkeydown event. | |
649 | // tags: | |
650 | // private | |
651 | ||
652 | //We need to save selection if the user TAB away from this editor | |
653 | //no need to call _saveSelection for IE, as that will be taken care of in onBeforeDeactivate | |
654 | if(!has("ie") && !this.iframe && e.keyCode == keys.TAB && !this.tabIndent){ | |
655 | this._saveSelection(); | |
656 | } | |
657 | if(!this.customUndo){ | |
658 | this.inherited(arguments); | |
659 | return; | |
660 | } | |
661 | var k = e.keyCode; | |
662 | if(e.ctrlKey && !e.altKey){//undo and redo only if the special right Alt + z/y are not pressed #5892 | |
663 | if(k == 90 || k == 122){ //z | |
664 | event.stop(e); | |
665 | this.undo(); | |
666 | return; | |
667 | }else if(k == 89 || k == 121){ //y | |
668 | event.stop(e); | |
669 | this.redo(); | |
670 | return; | |
671 | } | |
672 | } | |
673 | this.inherited(arguments); | |
674 | ||
675 | switch(k){ | |
676 | case keys.ENTER: | |
677 | case keys.BACKSPACE: | |
678 | case keys.DELETE: | |
679 | this.beginEditing(); | |
680 | break; | |
681 | case 88: //x | |
682 | case 86: //v | |
683 | if(e.ctrlKey && !e.altKey && !e.metaKey){ | |
684 | this.endEditing();//end current typing step if any | |
685 | if(e.keyCode == 88){ | |
686 | this.beginEditing('cut'); | |
687 | }else{ | |
688 | this.beginEditing('paste'); | |
689 | } | |
690 | //use timeout to trigger after the paste is complete | |
691 | this.defer("endEditing", 1); | |
692 | break; | |
693 | } | |
694 | //pass through | |
695 | default: | |
696 | if(!e.ctrlKey && !e.altKey && !e.metaKey && (e.keyCode<keys.F1 || e.keyCode>keys.F15)){ | |
697 | this.beginEditing(); | |
698 | break; | |
699 | } | |
700 | //pass through | |
701 | case keys.ALT: | |
702 | this.endEditing(); | |
703 | break; | |
704 | case keys.UP_ARROW: | |
705 | case keys.DOWN_ARROW: | |
706 | case keys.LEFT_ARROW: | |
707 | case keys.RIGHT_ARROW: | |
708 | case keys.HOME: | |
709 | case keys.END: | |
710 | case keys.PAGE_UP: | |
711 | case keys.PAGE_DOWN: | |
712 | this.endEditing(true); | |
713 | break; | |
714 | //maybe ctrl+backspace/delete, so don't endEditing when ctrl is pressed | |
715 | case keys.CTRL: | |
716 | case keys.SHIFT: | |
717 | case keys.TAB: | |
718 | break; | |
719 | } | |
720 | }, | |
721 | _onBlur: function(){ | |
722 | // summary: | |
723 | // Called from focus manager when focus has moved away from this editor | |
724 | // tags: | |
725 | // protected | |
726 | ||
727 | //this._saveSelection(); | |
728 | this.inherited(arguments); | |
729 | this.endEditing(true); | |
730 | }, | |
731 | _saveSelection: function(){ | |
732 | // summary: | |
733 | // Save the currently selected text in _savedSelection attribute | |
734 | // tags: | |
735 | // private | |
736 | try{ | |
737 | this._savedSelection=this._getBookmark(); | |
738 | }catch(e){ /* Squelch any errors that occur if selection save occurs due to being hidden simultaneously. */} | |
739 | }, | |
740 | _restoreSelection: function(){ | |
741 | // summary: | |
742 | // Re-select the text specified in _savedSelection attribute; | |
743 | // see _saveSelection(). | |
744 | // tags: | |
745 | // private | |
746 | if(this._savedSelection){ | |
747 | // Clear off cursor to start, we're deliberately going to a selection. | |
748 | delete this._cursorToStart; | |
749 | // only restore the selection if the current range is collapsed | |
750 | // if not collapsed, then it means the editor does not lose | |
751 | // selection and there is no need to restore it | |
752 | if(win.withGlobal(this.window,'isCollapsed',focusBase)){ | |
753 | this._moveToBookmark(this._savedSelection); | |
754 | } | |
755 | delete this._savedSelection; | |
756 | } | |
757 | }, | |
758 | ||
759 | onClick: function(){ | |
760 | // summary: | |
761 | // Handler for when editor is clicked | |
762 | // tags: | |
763 | // protected | |
764 | this.endEditing(true); | |
765 | this.inherited(arguments); | |
766 | }, | |
767 | ||
768 | replaceValue: function(/*String*/ html){ | |
769 | // summary: | |
770 | // over-ride of replaceValue to support custom undo and stack maintenance. | |
771 | // tags: | |
772 | // protected | |
773 | if(!this.customUndo){ | |
774 | this.inherited(arguments); | |
775 | }else{ | |
776 | if(this.isClosed){ | |
777 | this.setValue(html); | |
778 | }else{ | |
779 | this.beginEditing(); | |
780 | if(!html){ | |
781 | html = " "; // | |
782 | } | |
783 | this.setValue(html); | |
784 | this.endEditing(); | |
785 | } | |
786 | } | |
787 | }, | |
788 | ||
789 | _setDisabledAttr: function(/*Boolean*/ value){ | |
790 | this.setValueDeferred.then(lang.hitch(this, function(){ | |
791 | if((!this.disabled && value) || (!this._buttonEnabledPlugins && value)){ | |
792 | // Disable editor: disable all enabled buttons and remember that list | |
793 | array.forEach(this._plugins, function(p){ | |
794 | p.set("disabled", true); | |
795 | }); | |
796 | }else if(this.disabled && !value){ | |
797 | // Restore plugins to being active. | |
798 | array.forEach(this._plugins, function(p){ | |
799 | p.set("disabled", false); | |
800 | }); | |
801 | } | |
802 | })); | |
803 | this.inherited(arguments); | |
804 | }, | |
805 | ||
806 | _setStateClass: function(){ | |
807 | try{ | |
808 | this.inherited(arguments); | |
809 | ||
810 | // Let theme set the editor's text color based on editor enabled/disabled state. | |
811 | // We need to jump through hoops because the main document (where the theme CSS is) | |
812 | // is separate from the iframe's document. | |
813 | if(this.document && this.document.body){ | |
814 | domStyle.set(this.document.body, "color", domStyle.get(this.iframe, "color")); | |
815 | } | |
816 | }catch(e){ /* Squelch any errors caused by focus change if hidden during a state change */} | |
817 | } | |
818 | }); | |
819 | ||
820 | // Register the "default plugins", ie, the built-in editor commands | |
821 | function simplePluginFactory(args){ | |
822 | return new _Plugin({ command: args.name }); | |
823 | } | |
824 | function togglePluginFactory(args){ | |
825 | return new _Plugin({ buttonClass: ToggleButton, command: args.name }); | |
826 | } | |
827 | lang.mixin(_Plugin.registry, { | |
828 | "undo": simplePluginFactory, | |
829 | "redo": simplePluginFactory, | |
830 | "cut": simplePluginFactory, | |
831 | "copy": simplePluginFactory, | |
832 | "paste": simplePluginFactory, | |
833 | "insertOrderedList": simplePluginFactory, | |
834 | "insertUnorderedList": simplePluginFactory, | |
835 | "indent": simplePluginFactory, | |
836 | "outdent": simplePluginFactory, | |
837 | "justifyCenter": simplePluginFactory, | |
838 | "justifyFull": simplePluginFactory, | |
839 | "justifyLeft": simplePluginFactory, | |
840 | "justifyRight": simplePluginFactory, | |
841 | "delete": simplePluginFactory, | |
842 | "selectAll": simplePluginFactory, | |
843 | "removeFormat": simplePluginFactory, | |
844 | "unlink": simplePluginFactory, | |
845 | "insertHorizontalRule": simplePluginFactory, | |
846 | ||
847 | "bold": togglePluginFactory, | |
848 | "italic": togglePluginFactory, | |
849 | "underline": togglePluginFactory, | |
850 | "strikethrough": togglePluginFactory, | |
851 | "subscript": togglePluginFactory, | |
852 | "superscript": togglePluginFactory, | |
853 | ||
854 | "|": function(){ | |
855 | return new _Plugin({ | |
856 | setEditor: function(editor){ | |
857 | this.editor = editor; | |
858 | this.button = new ToolbarSeparator({ownerDocument: editor.ownerDocument}); | |
859 | } | |
860 | }); | |
861 | } | |
862 | }); | |
863 | ||
864 | return Editor; | |
865 | }); |