plugin.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. /**
  2. * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or http://ckeditor.com/license
  4. */
  5. /**
  6. * @fileOverview Increase and Decrease Indent commands.
  7. */
  8. ( function() {
  9. 'use strict';
  10. var TRISTATE_DISABLED = CKEDITOR.TRISTATE_DISABLED,
  11. TRISTATE_OFF = CKEDITOR.TRISTATE_OFF;
  12. CKEDITOR.plugins.add( 'indent', {
  13. // jscs:disable maximumLineLength
  14. lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,tt,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
  15. // jscs:enable maximumLineLength
  16. icons: 'indent,indent-rtl,outdent,outdent-rtl', // %REMOVE_LINE_CORE%
  17. hidpi: true, // %REMOVE_LINE_CORE%
  18. init: function( editor ) {
  19. var genericDefinition = CKEDITOR.plugins.indent.genericDefinition;
  20. // Register generic commands.
  21. setupGenericListeners( editor, editor.addCommand( 'indent', new genericDefinition( true ) ) );
  22. setupGenericListeners( editor, editor.addCommand( 'outdent', new genericDefinition() ) );
  23. // Create and register toolbar button if possible.
  24. if ( editor.ui.addButton ) {
  25. editor.ui.addButton( 'Indent', {
  26. label: editor.lang.indent.indent,
  27. command: 'indent',
  28. directional: true,
  29. toolbar: 'indent,20'
  30. } );
  31. editor.ui.addButton( 'Outdent', {
  32. label: editor.lang.indent.outdent,
  33. command: 'outdent',
  34. directional: true,
  35. toolbar: 'indent,10'
  36. } );
  37. }
  38. // Register dirChanged listener.
  39. editor.on( 'dirChanged', function( evt ) {
  40. var range = editor.createRange(),
  41. dataNode = evt.data.node;
  42. range.setStartBefore( dataNode );
  43. range.setEndAfter( dataNode );
  44. var walker = new CKEDITOR.dom.walker( range ),
  45. node;
  46. while ( ( node = walker.next() ) ) {
  47. if ( node.type == CKEDITOR.NODE_ELEMENT ) {
  48. // A child with the defined dir is to be ignored.
  49. if ( !node.equals( dataNode ) && node.getDirection() ) {
  50. range.setStartAfter( node );
  51. walker = new CKEDITOR.dom.walker( range );
  52. continue;
  53. }
  54. // Switch alignment classes.
  55. var classes = editor.config.indentClasses;
  56. if ( classes ) {
  57. var suffix = ( evt.data.dir == 'ltr' ) ? [ '_rtl', '' ] : [ '', '_rtl' ];
  58. for ( var i = 0; i < classes.length; i++ ) {
  59. if ( node.hasClass( classes[ i ] + suffix[ 0 ] ) ) {
  60. node.removeClass( classes[ i ] + suffix[ 0 ] );
  61. node.addClass( classes[ i ] + suffix[ 1 ] );
  62. }
  63. }
  64. }
  65. // Switch the margins.
  66. var marginLeft = node.getStyle( 'margin-right' ),
  67. marginRight = node.getStyle( 'margin-left' );
  68. marginLeft ? node.setStyle( 'margin-left', marginLeft ) : node.removeStyle( 'margin-left' );
  69. marginRight ? node.setStyle( 'margin-right', marginRight ) : node.removeStyle( 'margin-right' );
  70. }
  71. }
  72. } );
  73. }
  74. } );
  75. /**
  76. * Global command class definitions and global helpers.
  77. *
  78. * @class
  79. * @singleton
  80. */
  81. CKEDITOR.plugins.indent = {
  82. /**
  83. * A base class for a generic command definition, responsible mainly for creating
  84. * Increase Indent and Decrease Indent toolbar buttons as well as for refreshing
  85. * UI states.
  86. *
  87. * Commands of this class do not perform any indentation by themselves. They
  88. * delegate this job to content-specific indentation commands (i.e. indentlist).
  89. *
  90. * @class CKEDITOR.plugins.indent.genericDefinition
  91. * @extends CKEDITOR.commandDefinition
  92. * @param {CKEDITOR.editor} editor The editor instance this command will be
  93. * applied to.
  94. * @param {String} name The name of the command.
  95. * @param {Boolean} [isIndent] Defines the command as indenting or outdenting.
  96. */
  97. genericDefinition: function( isIndent ) {
  98. /**
  99. * Determines whether the command belongs to the indentation family.
  100. * Otherwise it is assumed to be an outdenting command.
  101. *
  102. * @readonly
  103. * @property {Boolean} [=false]
  104. */
  105. this.isIndent = !!isIndent;
  106. // Mimic naive startDisabled behavior for outdent.
  107. this.startDisabled = !this.isIndent;
  108. },
  109. /**
  110. * A base class for specific indentation command definitions responsible for
  111. * handling a pre-defined set of elements i.e. indentlist for lists or
  112. * indentblock for text block elements.
  113. *
  114. * Commands of this class perform indentation operations and modify the DOM structure.
  115. * They listen for events fired by {@link CKEDITOR.plugins.indent.genericDefinition}
  116. * and execute defined actions.
  117. *
  118. * **NOTE**: This is not an {@link CKEDITOR.command editor command}.
  119. * Context-specific commands are internal, for indentation system only.
  120. *
  121. * @class CKEDITOR.plugins.indent.specificDefinition
  122. * @param {CKEDITOR.editor} editor The editor instance this command will be
  123. * applied to.
  124. * @param {String} name The name of the command.
  125. * @param {Boolean} [isIndent] Defines the command as indenting or outdenting.
  126. */
  127. specificDefinition: function( editor, name, isIndent ) {
  128. this.name = name;
  129. this.editor = editor;
  130. /**
  131. * An object of jobs handled by the command. Each job consists
  132. * of two functions: `refresh` and `exec` as well as the execution priority.
  133. *
  134. * * The `refresh` function determines whether a job is doable for
  135. * a particular context. These functions are executed in the
  136. * order of priorities, one by one, for all plugins that registered
  137. * jobs. As jobs are related to generic commands, refreshing
  138. * occurs when the global command is firing the `refresh` event.
  139. *
  140. * **Note**: This function must return either {@link CKEDITOR#TRISTATE_DISABLED}
  141. * or {@link CKEDITOR#TRISTATE_OFF}.
  142. *
  143. * * The `exec` function modifies the DOM if possible. Just like
  144. * `refresh`, `exec` functions are executed in the order of priorities
  145. * while the generic command is executed. This function is not executed
  146. * if `refresh` for this job returned {@link CKEDITOR#TRISTATE_DISABLED}.
  147. *
  148. * **Note**: This function must return a Boolean value, indicating whether it
  149. * was successful. If a job was successful, then no other jobs are being executed.
  150. *
  151. * Sample definition:
  152. *
  153. * command.jobs = {
  154. * // Priority = 20.
  155. * '20': {
  156. * refresh( editor, path ) {
  157. * if ( condition )
  158. * return CKEDITOR.TRISTATE_OFF;
  159. * else
  160. * return CKEDITOR.TRISTATE_DISABLED;
  161. * },
  162. * exec( editor ) {
  163. * // DOM modified! This was OK.
  164. * return true;
  165. * }
  166. * },
  167. * // Priority = 60. This job is done later.
  168. * '60': {
  169. * // Another job.
  170. * }
  171. * };
  172. *
  173. * For additional information, please check comments for
  174. * the `setupGenericListeners` function.
  175. *
  176. * @readonly
  177. * @property {Object} [={}]
  178. */
  179. this.jobs = {};
  180. /**
  181. * Determines whether the editor that the command belongs to has
  182. * {@link CKEDITOR.config#enterMode config.enterMode} set to {@link CKEDITOR#ENTER_BR}.
  183. *
  184. * @readonly
  185. * @see CKEDITOR.config#enterMode
  186. * @property {Boolean} [=false]
  187. */
  188. this.enterBr = editor.config.enterMode == CKEDITOR.ENTER_BR;
  189. /**
  190. * Determines whether the command belongs to the indentation family.
  191. * Otherwise it is assumed to be an outdenting command.
  192. *
  193. * @readonly
  194. * @property {Boolean} [=false]
  195. */
  196. this.isIndent = !!isIndent;
  197. /**
  198. * The name of the global command related to this one.
  199. *
  200. * @readonly
  201. */
  202. this.relatedGlobal = isIndent ? 'indent' : 'outdent';
  203. /**
  204. * A keystroke associated with this command (*Tab* or *Shift+Tab*).
  205. *
  206. * @readonly
  207. */
  208. this.indentKey = isIndent ? 9 : CKEDITOR.SHIFT + 9;
  209. /**
  210. * Stores created markers for the command so they can eventually be
  211. * purged after the `exec` function is run.
  212. */
  213. this.database = {};
  214. },
  215. /**
  216. * Registers content-specific commands as a part of the indentation system
  217. * directed by generic commands. Once a command is registered,
  218. * it listens for events of a related generic command.
  219. *
  220. * CKEDITOR.plugins.indent.registerCommands( editor, {
  221. * 'indentlist': new indentListCommand( editor, 'indentlist' ),
  222. * 'outdentlist': new indentListCommand( editor, 'outdentlist' )
  223. * } );
  224. *
  225. * Content-specific commands listen for the generic command's `exec` and
  226. * try to execute their own jobs, one after another. If some execution is
  227. * successful, `evt.data.done` is set so no more jobs (commands) are involved.
  228. *
  229. * Content-specific commands also listen for the generic command's `refresh`
  230. * and fill the `evt.data.states` object with states of jobs. A generic command
  231. * uses this data to determine its own state and to update the UI.
  232. *
  233. * @member CKEDITOR.plugins.indent
  234. * @param {CKEDITOR.editor} editor The editor instance this command is
  235. * applied to.
  236. * @param {Object} commands An object of {@link CKEDITOR.command}.
  237. */
  238. registerCommands: function( editor, commands ) {
  239. editor.on( 'pluginsLoaded', function() {
  240. for ( var name in commands ) {
  241. ( function( editor, command ) {
  242. var relatedGlobal = editor.getCommand( command.relatedGlobal );
  243. for ( var priority in command.jobs ) {
  244. // Observe generic exec event and execute command when necessary.
  245. // If the command was successfully handled by the command and
  246. // DOM has been modified, stop event propagation so no other plugin
  247. // will bother. Job is done.
  248. relatedGlobal.on( 'exec', function( evt ) {
  249. if ( evt.data.done )
  250. return;
  251. // Make sure that anything this command will do is invisible
  252. // for undoManager. What undoManager only can see and
  253. // remember is the execution of the global command (relatedGlobal).
  254. editor.fire( 'lockSnapshot' );
  255. if ( command.execJob( editor, priority ) )
  256. evt.data.done = true;
  257. editor.fire( 'unlockSnapshot' );
  258. // Clean up the markers.
  259. CKEDITOR.dom.element.clearAllMarkers( command.database );
  260. }, this, null, priority );
  261. // Observe generic refresh event and force command refresh.
  262. // Once refreshed, save command state in event data
  263. // so generic command plugin can update its own state and UI.
  264. relatedGlobal.on( 'refresh', function( evt ) {
  265. if ( !evt.data.states )
  266. evt.data.states = {};
  267. evt.data.states[ command.name + '@' + priority ] =
  268. command.refreshJob( editor, priority, evt.data.path );
  269. }, this, null, priority );
  270. }
  271. // Since specific indent commands have no UI elements,
  272. // they need to be manually registered as a editor feature.
  273. editor.addFeature( command );
  274. } )( this, commands[ name ] );
  275. }
  276. } );
  277. }
  278. };
  279. CKEDITOR.plugins.indent.genericDefinition.prototype = {
  280. context: 'p',
  281. exec: function() {}
  282. };
  283. CKEDITOR.plugins.indent.specificDefinition.prototype = {
  284. /**
  285. * Executes the content-specific procedure if the context is correct.
  286. * It calls the `exec` function of a job of the given `priority`
  287. * that modifies the DOM.
  288. *
  289. * @param {CKEDITOR.editor} editor The editor instance this command
  290. * will be applied to.
  291. * @param {Number} priority The priority of the job to be executed.
  292. * @returns {Boolean} Indicates whether the job was successful.
  293. */
  294. execJob: function( editor, priority ) {
  295. var job = this.jobs[ priority ];
  296. if ( job.state != TRISTATE_DISABLED )
  297. return job.exec.call( this, editor );
  298. },
  299. /**
  300. * Calls the `refresh` function of a job of the given `priority`.
  301. * The function returns the state of the job which can be either
  302. * {@link CKEDITOR#TRISTATE_DISABLED} or {@link CKEDITOR#TRISTATE_OFF}.
  303. *
  304. * @param {CKEDITOR.editor} editor The editor instance this command
  305. * will be applied to.
  306. * @param {Number} priority The priority of the job to be executed.
  307. * @returns {Number} The state of the job.
  308. */
  309. refreshJob: function( editor, priority, path ) {
  310. var job = this.jobs[ priority ];
  311. if ( !editor.activeFilter.checkFeature( this ) )
  312. job.state = TRISTATE_DISABLED;
  313. else
  314. job.state = job.refresh.call( this, editor, path );
  315. return job.state;
  316. },
  317. /**
  318. * Checks if the element path contains the element handled
  319. * by this indentation command.
  320. *
  321. * @param {CKEDITOR.dom.elementPath} node A path to be checked.
  322. * @returns {CKEDITOR.dom.element}
  323. */
  324. getContext: function( path ) {
  325. return path.contains( this.context );
  326. }
  327. };
  328. /**
  329. * Attaches event listeners for this generic command. Since the indentation
  330. * system is event-oriented, generic commands communicate with
  331. * content-specific commands using the `exec` and `refresh` events.
  332. *
  333. * Listener priorities are crucial. Different indentation phases
  334. * are executed with different priorities.
  335. *
  336. * For the `exec` event:
  337. *
  338. * * 0: Selection and bookmarks are saved by the generic command.
  339. * * 1-99: Content-specific commands try to indent the code by executing
  340. * their own jobs ({@link CKEDITOR.plugins.indent.specificDefinition#jobs}).
  341. * * 100: Bookmarks are re-selected by the generic command.
  342. *
  343. * The visual interpretation looks as follows:
  344. *
  345. * +------------------+
  346. * | Exec event fired |
  347. * +------ + ---------+
  348. * |
  349. * 0 -<----------+ Selection and bookmarks saved.
  350. * |
  351. * |
  352. * 25 -<---+ Exec 1st job of plugin#1 (return false, continuing...).
  353. * |
  354. * |
  355. * 50 -<---+ Exec 1st job of plugin#2 (return false, continuing...).
  356. * |
  357. * |
  358. * 75 -<---+ Exec 2nd job of plugin#1 (only if plugin#2 failed).
  359. * |
  360. * |
  361. * 100 -<-----------+ Re-select bookmarks, clean-up.
  362. * |
  363. * +-------- v ----------+
  364. * | Exec event finished |
  365. * +---------------------+
  366. *
  367. * For the `refresh` event:
  368. *
  369. * * <100: Content-specific commands refresh their job states according
  370. * to the given path. Jobs save their states in the `evt.data.states` object
  371. * passed along with the event. This can be either {@link CKEDITOR#TRISTATE_DISABLED}
  372. * or {@link CKEDITOR#TRISTATE_OFF}.
  373. * * 100: Command state is determined according to what states
  374. * have been returned by content-specific jobs (`evt.data.states`).
  375. * UI elements are updated at this stage.
  376. *
  377. * **Note**: If there is at least one job with the {@link CKEDITOR#TRISTATE_OFF} state,
  378. * then the generic command state is also {@link CKEDITOR#TRISTATE_OFF}. Otherwise,
  379. * the command state is {@link CKEDITOR#TRISTATE_DISABLED}.
  380. *
  381. * @param {CKEDITOR.command} command The command to be set up.
  382. * @private
  383. */
  384. function setupGenericListeners( editor, command ) {
  385. var selection, bookmarks;
  386. // Set the command state according to content-specific
  387. // command states.
  388. command.on( 'refresh', function( evt ) {
  389. // If no state comes with event data, disable command.
  390. var states = [ TRISTATE_DISABLED ];
  391. for ( var s in evt.data.states )
  392. states.push( evt.data.states[ s ] );
  393. this.setState( CKEDITOR.tools.search( states, TRISTATE_OFF ) ? TRISTATE_OFF : TRISTATE_DISABLED );
  394. }, command, null, 100 );
  395. // Initialization. Save bookmarks and mark event as not handled
  396. // by any plugin (command) yet.
  397. command.on( 'exec', function( evt ) {
  398. selection = editor.getSelection();
  399. bookmarks = selection.createBookmarks( 1 );
  400. // Mark execution as not handled yet.
  401. if ( !evt.data )
  402. evt.data = {};
  403. evt.data.done = false;
  404. }, command, null, 0 );
  405. // Housekeeping. Make sure selectionChange will be called.
  406. // Also re-select previously saved bookmarks.
  407. command.on( 'exec', function() {
  408. editor.forceNextSelectionCheck();
  409. selection.selectBookmarks( bookmarks );
  410. }, command, null, 100 );
  411. }
  412. } )();