Editor.js 28 KB

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