abstracttoolbarmodifier.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. /* global ToolbarConfigurator */
  2. 'use strict';
  3. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create
  4. if ( typeof Object.create != 'function' ) {
  5. ( function() {
  6. var F = function() {};
  7. Object.create = function( o ) {
  8. if ( arguments.length > 1 ) {
  9. throw Error( 'Second argument not supported' );
  10. }
  11. if ( o === null ) {
  12. throw Error( 'Cannot set a null [[Prototype]]' );
  13. }
  14. if ( typeof o != 'object' ) {
  15. throw TypeError( 'Argument must be an object' );
  16. }
  17. F.prototype = o;
  18. return new F();
  19. };
  20. } )();
  21. }
  22. // Copy of the divarea plugin (with some enhancements), so we always have some editable mode, regardless of the build's config.
  23. CKEDITOR.plugins.add( 'toolbarconfiguratorarea', {
  24. // Use afterInit to override wysiwygarea's mode. May still fail to override divarea, but divarea is nice.
  25. afterInit: function( editor ) {
  26. editor.addMode( 'wysiwyg', function( callback ) {
  27. var editingBlock = CKEDITOR.dom.element.createFromHtml( '<div class="cke_wysiwyg_div cke_reset" hidefocus="true"></div>' );
  28. var contentSpace = editor.ui.space( 'contents' );
  29. contentSpace.append( editingBlock );
  30. editingBlock = editor.editable( editingBlock );
  31. editingBlock.detach = CKEDITOR.tools.override( editingBlock.detach,
  32. function( org ) {
  33. return function() {
  34. org.apply( this, arguments );
  35. this.remove();
  36. };
  37. } );
  38. editor.setData( editor.getData( 1 ), callback );
  39. editor.fire( 'contentDom' );
  40. } );
  41. // Additions to the divarea.
  42. // Speed up data processing.
  43. editor.dataProcessor.toHtml = function( html ) {
  44. return html;
  45. };
  46. editor.dataProcessor.toDataFormat = function( html ) {
  47. return html;
  48. };
  49. // End of the additions.
  50. }
  51. } );
  52. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
  53. if ( !Object.keys ) {
  54. Object.keys = ( function() {
  55. var hasOwnProperty = Object.prototype.hasOwnProperty,
  56. hasDontEnumBug = !( { toString: null } ).propertyIsEnumerable( 'toString' ),
  57. dontEnums = [
  58. 'toString',
  59. 'toLocaleString',
  60. 'valueOf',
  61. 'hasOwnProperty',
  62. 'isPrototypeOf',
  63. 'propertyIsEnumerable',
  64. 'constructor'
  65. ],
  66. dontEnumsLength = dontEnums.length;
  67. return function( obj ) {
  68. if ( typeof obj !== 'object' && ( typeof obj !== 'function' || obj === null ) )
  69. throw new TypeError( 'Object.keys called on non-object' );
  70. var result = [], prop, i;
  71. for ( prop in obj ) {
  72. if ( hasOwnProperty.call( obj, prop ) )
  73. result.push( prop );
  74. }
  75. if ( hasDontEnumBug ) {
  76. for ( i = 0; i < dontEnumsLength; i++ ) {
  77. if ( hasOwnProperty.call( obj, dontEnums[ i ] ) )
  78. result.push( dontEnums[ i ] );
  79. }
  80. }
  81. return result;
  82. };
  83. }() );
  84. }
  85. ( function() {
  86. /**
  87. * @class ToolbarConfigurator.AbstractToolbarModifier
  88. * @param {String} editorId An id of modified editor
  89. * @constructor
  90. */
  91. function AbstractToolbarModifier( editorId, cfg ) {
  92. this.cfg = cfg || {};
  93. this.hidden = false;
  94. this.editorId = editorId;
  95. this.fullToolbarEditor = new ToolbarConfigurator.FullToolbarEditor();
  96. this.mainContainer = null;
  97. this.originalConfig = null;
  98. this.actualConfig = null;
  99. this.waitForReady = false;
  100. this.isEditableVisible = false;
  101. this.toolbarContainer = null;
  102. this.toolbarButtons = [];
  103. }
  104. // Expose the class.
  105. ToolbarConfigurator.AbstractToolbarModifier = AbstractToolbarModifier;
  106. /**
  107. * @param {String} config
  108. */
  109. AbstractToolbarModifier.prototype.setConfig = function( config ) {
  110. this._onInit( undefined, config, true );
  111. };
  112. /**
  113. * @param {Function} [callback]
  114. */
  115. AbstractToolbarModifier.prototype.init = function( callback ) {
  116. var that = this;
  117. this.mainContainer = new CKEDITOR.dom.element( 'div' );
  118. if ( this.fullToolbarEditor.editorInstance !== null ) {
  119. throw 'Only one instance of ToolbarModifier is allowed';
  120. }
  121. if ( !this.editorInstance ) {
  122. // Do not refresh yet, let's wait for the full toolbar editor (see below).
  123. this._createEditor( false );
  124. }
  125. this.editorInstance.once( 'loaded', function() {
  126. that.fullToolbarEditor.init( function() {
  127. that._onInit( callback );
  128. if ( typeof that.onRefresh == 'function' ) {
  129. that.onRefresh();
  130. }
  131. }, that.editorInstance.config );
  132. } );
  133. return this.mainContainer;
  134. };
  135. /**
  136. * Called editor initialization finished.
  137. *
  138. * @param {Function} callback
  139. * @param {String} [actualConfig]
  140. * @private
  141. */
  142. AbstractToolbarModifier.prototype._onInit = function( callback, actualConfig ) {
  143. this.originalConfig = this.editorInstance.config;
  144. if ( !actualConfig ) {
  145. this.actualConfig = JSON.parse( JSON.stringify( this.originalConfig ) );
  146. } else {
  147. this.actualConfig = JSON.parse( actualConfig );
  148. }
  149. if ( !this.actualConfig.toolbarGroups && !this.actualConfig.toolbar ) {
  150. this.actualConfig.toolbarGroups = getDefaultToolbarGroups( this.editorInstance );
  151. }
  152. if ( typeof callback === 'function' )
  153. callback( this.mainContainer );
  154. // Here we are going to keep only `name` and `groups` data from editor `toolbar` property.
  155. function getDefaultToolbarGroups( editor ) {
  156. var toolbarGroups = editor.toolbar,
  157. copy = [];
  158. var max = toolbarGroups.length;
  159. for ( var i = 0; i < max; i++ ) {
  160. var group = toolbarGroups[ i ];
  161. if ( typeof group == 'string' ) {
  162. copy.push( group ); // separator
  163. } else {
  164. copy.push( {
  165. name: group.name,
  166. groups: group.groups ? group.groups.slice() : []
  167. } );
  168. }
  169. }
  170. return copy;
  171. }
  172. };
  173. /**
  174. * Creates DOM structure of tool.
  175. *
  176. * @returns {CKEDITOR.dom.element}
  177. * @private
  178. */
  179. AbstractToolbarModifier.prototype._createModifier = function() {
  180. this.mainContainer.addClass( 'unselectable' );
  181. if ( this.modifyContainer ) {
  182. this.modifyContainer.remove();
  183. }
  184. this.modifyContainer = new CKEDITOR.dom.element( 'div' );
  185. this.modifyContainer.addClass( 'toolbarModifier' );
  186. this.mainContainer.append( this.modifyContainer );
  187. return this.mainContainer;
  188. };
  189. /**
  190. * Find editable area in CKEditor instance DOM container
  191. *
  192. * @returns {CKEDITOR.dom.element}
  193. */
  194. AbstractToolbarModifier.prototype.getEditableArea = function() {
  195. var selector = ( '#' + this.editorInstance.id + '_contents' );
  196. return this.editorInstance.container.findOne( selector );
  197. };
  198. /**
  199. * Hide editable area in modified editor by sets its height to 0.
  200. *
  201. * @private
  202. */
  203. AbstractToolbarModifier.prototype._hideEditable = function() {
  204. var area = this.getEditableArea();
  205. this.isEditableVisible = false;
  206. this.lastEditableAreaHeight = area.getStyle( 'height' );
  207. area.setStyle( 'height', '0' );
  208. };
  209. /**
  210. * Show editable area in modified editor.
  211. *
  212. * @private
  213. */
  214. AbstractToolbarModifier.prototype._showEditable = function() {
  215. this.isEditableVisible = true;
  216. this.getEditableArea().setStyle( 'height', this.lastEditableAreaHeight || 'auto' );
  217. };
  218. /**
  219. * Toggle editable area visibility.
  220. *
  221. * @private
  222. */
  223. AbstractToolbarModifier.prototype._toggleEditable = function() {
  224. if ( this.isEditableVisible )
  225. this._hideEditable();
  226. else
  227. this._showEditable();
  228. };
  229. /**
  230. * Usually called when configuration changes.
  231. *
  232. * @private
  233. */
  234. AbstractToolbarModifier.prototype._refreshEditor = function() {
  235. var that = this,
  236. status = this.editorInstance.status;
  237. // Wait for ready only once.
  238. if ( this.waitForReady )
  239. return;
  240. // Not ready.
  241. if ( status == 'unloaded' || status == 'loaded' ) {
  242. this.waitForReady = true;
  243. this.editorInstance.once( 'instanceReady', function() {
  244. refresh();
  245. }, this );
  246. // Ready or destroyed.
  247. } else {
  248. refresh();
  249. }
  250. function refresh() {
  251. that.editorInstance.destroy();
  252. that._createEditor( true, that.getActualConfig() );
  253. that.waitForReady = false;
  254. }
  255. };
  256. /**
  257. * Creates editor that can be used to present the toolbar configuration.
  258. *
  259. * @private
  260. */
  261. AbstractToolbarModifier.prototype._createEditor = function( doRefresh, configOverrides ) {
  262. var that = this;
  263. this.editorInstance = CKEDITOR.replace( this.editorId );
  264. this.editorInstance.on( 'configLoaded', function() {
  265. var config = that.editorInstance.config;
  266. if ( configOverrides ) {
  267. CKEDITOR.tools.extend( config, configOverrides, true );
  268. }
  269. AbstractToolbarModifier.extendPluginsConfig( config );
  270. } );
  271. // Prevent creating any other space than the top one.
  272. this.editorInstance.on( 'uiSpace', function( evt ) {
  273. if ( evt.data.space != 'top' ) {
  274. evt.stop();
  275. }
  276. }, null, null, -999 );
  277. this.editorInstance.once( 'loaded', function() {
  278. var btns = that.editorInstance.ui.instances;
  279. for ( var i in btns ) {
  280. if ( btns[ i ] ) {
  281. btns[ i ].click = empty;
  282. btns[ i ].onClick = empty;
  283. }
  284. }
  285. if ( !that.isEditableVisible ) {
  286. that._hideEditable();
  287. }
  288. if ( that.currentActive && that.currentActive.name ) {
  289. that._highlightGroup( that.currentActive.name );
  290. }
  291. if ( that.hidden ) {
  292. that.hideUI();
  293. } else {
  294. that.showUI();
  295. }
  296. if ( doRefresh && ( typeof that.onRefresh === 'function' ) ) {
  297. that.onRefresh();
  298. }
  299. } );
  300. function empty() {}
  301. };
  302. /**
  303. * Always returns copy of config.
  304. *
  305. * @returns {Object}
  306. */
  307. AbstractToolbarModifier.prototype.getActualConfig = function() {
  308. return JSON.parse( JSON.stringify( this.actualConfig ) );
  309. };
  310. /**
  311. * Creates toolbar in tool.
  312. *
  313. * @private
  314. */
  315. AbstractToolbarModifier.prototype._createToolbar = function() {
  316. if ( !this.toolbarButtons.length ) {
  317. return;
  318. }
  319. this.toolbarContainer = new CKEDITOR.dom.element( 'div' );
  320. this.toolbarContainer.addClass( 'toolbar' );
  321. var max = this.toolbarButtons.length;
  322. for ( var i = 0; i < max; i += 1 ) {
  323. this._createToolbarBtn( this.toolbarButtons[ i ] );
  324. }
  325. };
  326. /**
  327. * Create toolbar button and add it to toolbar container
  328. *
  329. * @param {Object} cfg
  330. * @returns {CKEDITOR.dom.element}
  331. * @private
  332. */
  333. AbstractToolbarModifier.prototype._createToolbarBtn = function( cfg ) {
  334. var btnText = ( typeof cfg.text === 'string' ? cfg.text : cfg.text.inactive ),
  335. btn = ToolbarConfigurator.FullToolbarEditor.createButton( btnText, cfg.cssClass );
  336. this.toolbarContainer.append( btn );
  337. btn.data( 'group', cfg.group );
  338. btn.addClass( cfg.position );
  339. btn.on( 'click', function() {
  340. cfg.clickCallback.call( this, btn, cfg );
  341. }, this );
  342. return btn;
  343. };
  344. /**
  345. * @private
  346. * @param {Object} config
  347. */
  348. AbstractToolbarModifier.prototype._fixGroups = function( config ) {
  349. var groups = config.toolbarGroups || [];
  350. var max = groups.length;
  351. for ( var i = 0; i < max; i += 1 ) {
  352. var currentGroup = groups[ i ];
  353. // separator, in config, is in raw format
  354. // need to make it more sophisticated to keep unique id
  355. // for each one
  356. if ( currentGroup == '/' ) {
  357. currentGroup = groups[ i ] = {};
  358. currentGroup.type = 'separator';
  359. currentGroup.name = ( 'separator' + CKEDITOR.tools.getNextNumber() );
  360. continue;
  361. }
  362. // sometimes subgroups are not set (basic package), so need to
  363. // create them artifically
  364. currentGroup.groups = currentGroup.groups || [];
  365. // when there is no subgroup with same name like its parent name
  366. // then it have to be added artificially
  367. // in order to maintain consistency between user interface and config
  368. if ( CKEDITOR.tools.indexOf( currentGroup.groups, currentGroup.name ) == -1 ) {
  369. this.editorInstance.ui.addToolbarGroup( currentGroup.name, currentGroup.groups[ currentGroup.groups.length - 1 ], currentGroup.name );
  370. currentGroup.groups.push( currentGroup.name );
  371. }
  372. this._fixSubgroups( currentGroup );
  373. }
  374. };
  375. /**
  376. * Transform subgroup string to object literal
  377. * with keys: {String} name and {Number} totalBtns
  378. * Please note: this method modify Object provided in first argument
  379. *
  380. * input:
  381. * [
  382. * { groups: [ 'nameOne', 'nameTwo' ] }
  383. * ]
  384. *
  385. * output:
  386. * [
  387. * { groups: [ { name: 'nameOne', totalBtns: 3 }, { name: 'nameTwo', totalBtns: 5 } ] }
  388. * ]
  389. *
  390. * @param {Object} group
  391. * @private
  392. */
  393. AbstractToolbarModifier.prototype._fixSubgroups = function( group ) {
  394. var subGroups = group.groups;
  395. var max = subGroups.length;
  396. for ( var i = 0; i < max; i += 1 ) {
  397. var subgroupName = subGroups[ i ];
  398. subGroups[ i ] = {
  399. name: subgroupName,
  400. totalBtns: ToolbarConfigurator.ToolbarModifier.getTotalSubGroupButtonsNumber( subgroupName, this.fullToolbarEditor )
  401. };
  402. }
  403. };
  404. /**
  405. * Same as JSON.stringify method but returned string is in one line
  406. *
  407. * @param {Object} json
  408. * @param {Object} opts
  409. * @param {Boolean} opts.addSpaces
  410. * @param {Boolean} opts.noQuotesOnKey
  411. * @param {Boolean} opts.singleQuotes
  412. * @returns {Object}
  413. */
  414. AbstractToolbarModifier.stringifyJSONintoOneLine = function( json, opts ) {
  415. opts = opts || {};
  416. var stringJSON = JSON.stringify( json, null, '' );
  417. // IE8 make new line characters
  418. stringJSON = stringJSON.replace( /\n/g, '' );
  419. if ( opts.addSpaces ) {
  420. stringJSON = stringJSON.replace( /(\{|:|,|\[|\])/g, function( sentence ) {
  421. return sentence + ' ';
  422. } );
  423. stringJSON = stringJSON.replace( /(\])/g, function( sentence ) {
  424. return ' ' + sentence;
  425. } );
  426. }
  427. if ( opts.noQuotesOnKey ) {
  428. stringJSON = stringJSON.replace( /"(\w*)":/g, function( sentence, word ) {
  429. return word + ':';
  430. } );
  431. }
  432. if ( opts.singleQuotes ) {
  433. stringJSON = stringJSON.replace( /\"/g, '\'' );
  434. }
  435. return stringJSON;
  436. };
  437. /**
  438. * Hide toolbar configurator
  439. */
  440. AbstractToolbarModifier.prototype.hideUI = function() {
  441. this.hidden = true;
  442. this.mainContainer.hide();
  443. if ( this.editorInstance.container ) {
  444. this.editorInstance.container.hide();
  445. }
  446. };
  447. /**
  448. * Show toolbar configurator
  449. */
  450. AbstractToolbarModifier.prototype.showUI = function() {
  451. this.hidden = false;
  452. this.mainContainer.show();
  453. if ( this.editorInstance.container ) {
  454. this.editorInstance.container.show();
  455. }
  456. };
  457. /**
  458. * Extends plugins setttings in the specified config with settings useful for
  459. * the toolbar configurator.
  460. *
  461. * @static
  462. */
  463. AbstractToolbarModifier.extendPluginsConfig = function( config ) {
  464. var extraPlugins = config.extraPlugins;
  465. // Enable the special, lightweight area to replace wysiwygarea.
  466. config.extraPlugins = ( extraPlugins ? extraPlugins + ',' : '' ) + 'toolbarconfiguratorarea';
  467. };
  468. } )();