Editor.js.uncompressed.js 28 KB

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