toolbarmodifier.js 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366
  1. /* global ToolbarConfigurator, alert */
  2. 'use strict';
  3. ( function() {
  4. var AbstractToolbarModifier = ToolbarConfigurator.AbstractToolbarModifier;
  5. /**
  6. * @class ToolbarConfigurator.ToolbarModifier
  7. * @param {String} editorId An id of modified editor
  8. * @param {Object} cfg
  9. * @extends AbstractToolbarModifier
  10. * @constructor
  11. */
  12. function ToolbarModifier( editorId, cfg ) {
  13. AbstractToolbarModifier.call( this, editorId, cfg );
  14. this.removedButtons = null;
  15. this.originalConfig = null;
  16. this.actualConfig = null;
  17. this.emptyVisible = false;
  18. // edit, paste, config
  19. this.state = 'edit';
  20. this.toolbarButtons = [
  21. {
  22. text: {
  23. active: 'Hide empty toolbar groups',
  24. inactive: 'Show empty toolbar groups'
  25. },
  26. group: 'edit',
  27. position: 'left',
  28. cssClass: 'button-a-soft',
  29. clickCallback: function( button, buttonDefinition ) {
  30. var className = 'button-a-background';
  31. button[ button.hasClass( className ) ? 'removeClass' : 'addClass' ]( className );
  32. this._toggleVisibilityEmptyElements();
  33. if ( this.emptyVisible ) {
  34. button.setText( buttonDefinition.text.active );
  35. } else {
  36. button.setText( buttonDefinition.text.inactive );
  37. }
  38. }
  39. },
  40. {
  41. text: 'Add row separator',
  42. group: 'edit',
  43. position: 'left',
  44. cssClass: 'button-a-soft',
  45. clickCallback: function() {
  46. this._addSeparator();
  47. }
  48. },
  49. /*{
  50. text: 'Paste config',
  51. group: 'edit',
  52. position: 'left',
  53. clickCallback: function() {
  54. this.state = 'paste';
  55. this.modifyContainer.addClass( 'hidden' );
  56. this.configContainer.removeClass( 'hidden' );
  57. this.configContainer.setHtml( '<textarea></textarea>' );
  58. this.showToolbarBtnsByGroupName( 'config' );
  59. }
  60. },*/
  61. {
  62. text: 'Select config',
  63. group: 'config',
  64. position: 'left',
  65. cssClass: 'button-a-soft',
  66. clickCallback: function() {
  67. this.configContainer.findOne( 'textarea' ).$.select();
  68. }
  69. },
  70. {
  71. text: 'Back to configurator',
  72. group: 'config',
  73. position: 'right',
  74. cssClass: 'button-a-background',
  75. clickCallback: function() {
  76. if ( this.state === 'paste' ) {
  77. var cfg = this.configContainer.findOne( 'textarea' ).getValue();
  78. cfg = ToolbarModifier.evaluateToolbarGroupsConfig( cfg );
  79. if ( cfg ) {
  80. this.setConfig( cfg );
  81. } else {
  82. alert( 'Your pasted config is wrong.' );
  83. }
  84. }
  85. this.state = 'edit';
  86. this._showConfigurationTool();
  87. this.showToolbarBtnsByGroupName( this.state );
  88. }
  89. },
  90. {
  91. text: 'Get toolbar <span class="highlight">config</span>',
  92. group: 'edit',
  93. position: 'right',
  94. cssClass: 'button-a-background icon-pos-left icon-download',
  95. clickCallback: function() {
  96. this.state = 'config';
  97. this._showConfig();
  98. this.showToolbarBtnsByGroupName( this.state );
  99. }
  100. }
  101. ];
  102. this.cachedActiveElement = null;
  103. }
  104. // Expose the class.
  105. ToolbarConfigurator.ToolbarModifier = ToolbarModifier;
  106. ToolbarModifier.prototype = Object.create( ToolbarConfigurator.AbstractToolbarModifier.prototype );
  107. /**
  108. * @returns {Object}
  109. */
  110. ToolbarModifier.prototype.getActualConfig = function() {
  111. var copy = AbstractToolbarModifier.prototype.getActualConfig.call( this );
  112. if ( copy.toolbarGroups ) {
  113. var max = copy.toolbarGroups.length;
  114. for ( var i = 0; i < max; i += 1 ) {
  115. var currentGroup = copy.toolbarGroups[ i ];
  116. copy.toolbarGroups[ i ] = ToolbarModifier.parseGroupToConfigValue( currentGroup );
  117. }
  118. }
  119. return copy;
  120. };
  121. /**
  122. * @param {Function} callback
  123. * @param {String} [config]
  124. * @param {Boolean} [forceKeepRemoveButtons=false]
  125. * @private
  126. */
  127. ToolbarModifier.prototype._onInit = function( callback, config, forceKeepRemoveButtons ) {
  128. forceKeepRemoveButtons = ( forceKeepRemoveButtons === true );
  129. AbstractToolbarModifier.prototype._onInit.call( this, undefined, config );
  130. this.removedButtons = [];
  131. if ( forceKeepRemoveButtons ) {
  132. if ( this.actualConfig.removeButtons ) {
  133. this.removedButtons = this.actualConfig.removeButtons.split( ',' );
  134. } else {
  135. this.removedButtons = [];
  136. }
  137. } else {
  138. if ( !( 'removeButtons' in this.originalConfig ) ) {
  139. this.originalConfig.removeButtons = '';
  140. this.removedButtons = [];
  141. } else {
  142. this.removedButtons = this.originalConfig.removeButtons ? this.originalConfig.removeButtons.split( ',' ) : [];
  143. }
  144. }
  145. if ( !this.actualConfig.toolbarGroups )
  146. this.actualConfig.toolbarGroups = this.fullToolbarEditor.getFullToolbarGroupsConfig();
  147. this._fixGroups( this.actualConfig );
  148. this._calculateTotalBtns();
  149. this._createModifier();
  150. this._refreshMoveBtnsAvalibility();
  151. this._refreshBtnTabIndexes();
  152. if ( typeof callback === 'function' )
  153. callback( this.mainContainer );
  154. };
  155. /**
  156. * @private
  157. */
  158. ToolbarModifier.prototype._showConfigurationTool = function() {
  159. this.configContainer.addClass( 'hidden' );
  160. this.modifyContainer.removeClass( 'hidden' );
  161. };
  162. /**
  163. * Show configuration file in tool
  164. *
  165. * @private
  166. */
  167. ToolbarModifier.prototype._showConfig = function() {
  168. var that = this,
  169. actualConfig = this.getActualConfig(),
  170. cfg = {};
  171. if ( actualConfig.toolbarGroups ) {
  172. cfg.toolbarGroups = actualConfig.toolbarGroups;
  173. var groups = prepareGroups( actualConfig.toolbarGroups, this.cfg.trimEmptyGroups );
  174. cfg.toolbarGroups = '\n\t\t' + groups.join( ',\n\t\t' );
  175. }
  176. function prepareGroups( toolbarGroups, trimEmptyGroups ) {
  177. var groups = [],
  178. max = toolbarGroups.length;
  179. for ( var i = 0; i < max; i++ ) {
  180. var group = toolbarGroups[ i ];
  181. if ( group === '/' ) {
  182. groups.push( '\'/\'' );
  183. continue;
  184. }
  185. if ( trimEmptyGroups ) {
  186. var max2 = group.groups.length;
  187. while ( max2-- ) {
  188. var subgroup = group.groups[ max2 ];
  189. if ( ToolbarModifier.getTotalSubGroupButtonsNumber( subgroup, that.fullToolbarEditor ) === 0 ) {
  190. group.groups.splice( max2, 1 );
  191. }
  192. }
  193. }
  194. if ( !( trimEmptyGroups && group.groups.length === 0 ) ) {
  195. groups.push( AbstractToolbarModifier.stringifyJSONintoOneLine( group, {
  196. addSpaces: true,
  197. noQuotesOnKey: true,
  198. singleQuotes: true
  199. } ) );
  200. }
  201. }
  202. return groups;
  203. }
  204. if ( actualConfig.removeButtons ) {
  205. cfg.removeButtons = actualConfig.removeButtons;
  206. }
  207. var content = [
  208. '<textarea class="configCode" readonly>',
  209. 'CKEDITOR.editorConfig = function( config ) {\n',
  210. ( cfg.toolbarGroups ? '\tconfig.toolbarGroups = [' + cfg.toolbarGroups + '\n\t];' : '' ),
  211. ( cfg.removeButtons ? '\n\n' : '' ),
  212. ( cfg.removeButtons ? '\tconfig.removeButtons = \'' + cfg.removeButtons + '\';' : '' ),
  213. '\n};',
  214. '</textarea>'
  215. ].join( '' );
  216. this.modifyContainer.addClass( 'hidden' );
  217. this.configContainer.removeClass( 'hidden' );
  218. this.configContainer.setHtml( content );
  219. };
  220. /**
  221. * Toggle empty groups and subgroups visibility.
  222. *
  223. * @private
  224. */
  225. ToolbarModifier.prototype._toggleVisibilityEmptyElements = function() {
  226. if ( this.modifyContainer.hasClass( 'empty-visible' ) ) {
  227. this.modifyContainer.removeClass( 'empty-visible' );
  228. this.emptyVisible = false;
  229. } else {
  230. this.modifyContainer.addClass( 'empty-visible' );
  231. this.emptyVisible = true;
  232. }
  233. this._refreshMoveBtnsAvalibility();
  234. };
  235. /**
  236. * Creates HTML main container of modifier.
  237. *
  238. * @returns {CKEDITOR.dom.element}
  239. * @private
  240. */
  241. ToolbarModifier.prototype._createModifier = function() {
  242. var that = this;
  243. AbstractToolbarModifier.prototype._createModifier.call( this );
  244. this.modifyContainer.setHtml( this._toolbarConfigToListString() );
  245. var groupLi = this.modifyContainer.find( 'li[data-type="group"]' );
  246. this.modifyContainer.on( 'mouseleave', function() {
  247. this._dehighlightActiveToolGroup();
  248. }, this );
  249. var max = groupLi.count();
  250. for ( var i = 0; i < max; i += 1 ) {
  251. groupLi.getItem( i ).on( 'mouseenter', onGroupHover );
  252. }
  253. function onGroupHover() {
  254. that._highlightGroup( this.data( 'name' ) );
  255. }
  256. CKEDITOR.document.on( 'keypress', function( e ) {
  257. var nativeEvent = e.data.$,
  258. keyCode = nativeEvent.keyCode,
  259. spaceOrEnter = ( keyCode === 32 || keyCode === 13 ),
  260. active = new CKEDITOR.dom.element( CKEDITOR.document.$.activeElement );
  261. var mainContainer = active.getAscendant( function( node ) {
  262. return node.$ === that.mainContainer.$;
  263. } );
  264. if ( !mainContainer || !spaceOrEnter ) {
  265. return;
  266. }
  267. if ( active.data( 'type' ) === 'button' ) {
  268. active.findOne( 'input' ).$.click();
  269. }
  270. } );
  271. this.modifyContainer.on( 'click', function( e ) {
  272. var origEvent = e.data.$,
  273. target = new CKEDITOR.dom.element( ( origEvent.target || origEvent.srcElement ) ),
  274. relativeGroupOrSeparatorLi = ToolbarModifier.getGroupOrSeparatorLiAncestor( target );
  275. if ( !relativeGroupOrSeparatorLi ) {
  276. return;
  277. }
  278. that.cachedActiveElement = document.activeElement;
  279. // checkbox clicked
  280. if ( target.$ instanceof HTMLInputElement )
  281. that._handleCheckboxClicked( target );
  282. // link clicked
  283. else if ( target.$ instanceof HTMLButtonElement ) {
  284. if ( origEvent.preventDefault )
  285. origEvent.preventDefault();
  286. else
  287. origEvent.returnValue = false;
  288. var result = that._handleAnchorClicked( target.$ );
  289. if ( result && result.action == 'remove' )
  290. return;
  291. }
  292. var elementType = relativeGroupOrSeparatorLi.data( 'type' ),
  293. elementName = relativeGroupOrSeparatorLi.data( 'name' );
  294. that._setActiveElement( elementType, elementName );
  295. if ( that.cachedActiveElement )
  296. that.cachedActiveElement.focus();
  297. } );
  298. if ( !this.toolbarContainer ) {
  299. this._createToolbar();
  300. this.toolbarContainer.insertBefore( this.mainContainer.getChildren().getItem( 0 ) );
  301. }
  302. this.showToolbarBtnsByGroupName( 'edit' );
  303. if ( !this.configContainer ) {
  304. this.configContainer = new CKEDITOR.dom.element( 'div' );
  305. this.configContainer.addClass( 'configContainer' );
  306. this.configContainer.addClass( 'hidden' );
  307. this.mainContainer.append( this.configContainer );
  308. }
  309. return this.mainContainer;
  310. };
  311. /**
  312. * Show toolbar buttons related to group name provided in argument
  313. * and hide other buttons
  314. * Please note: this method works on toolbar in tool, which is located
  315. * on top of the tool
  316. *
  317. * @param {String} groupName
  318. */
  319. ToolbarModifier.prototype.showToolbarBtnsByGroupName = function( groupName ) {
  320. if ( !this.toolbarContainer ) {
  321. return;
  322. }
  323. var allButtons = this.toolbarContainer.find( 'button' );
  324. var max = allButtons.count();
  325. for ( var i = 0; i < max; i += 1 ) {
  326. var currentBtn = allButtons.getItem( i );
  327. if ( currentBtn.data( 'group' ) == groupName )
  328. currentBtn.removeClass( 'hidden' );
  329. else
  330. currentBtn.addClass( 'hidden' );
  331. }
  332. };
  333. /**
  334. * Parse group "model" to configuration value
  335. *
  336. * @param {Object} group
  337. * @returns {Object}
  338. * @private
  339. */
  340. ToolbarModifier.parseGroupToConfigValue = function( group ) {
  341. if ( group.type == 'separator' ) {
  342. return '/';
  343. }
  344. var groups = group.groups,
  345. max = groups.length;
  346. delete group.totalBtns;
  347. for ( var i = 0; i < max; i += 1 ) {
  348. groups[ i ] = groups[ i ].name;
  349. }
  350. return group;
  351. };
  352. /**
  353. * Find closest Li ancestor in DOM tree which is group or separator element
  354. *
  355. * @param {CKEDITOR.dom.element} element
  356. * @returns {CKEDITOR.dom.element}
  357. */
  358. ToolbarModifier.getGroupOrSeparatorLiAncestor = function( element ) {
  359. if ( element.$ instanceof HTMLLIElement && element.data( 'type' ) == 'group' )
  360. return element;
  361. else {
  362. return ToolbarModifier.getFirstAncestor( element, function( ancestor ) {
  363. var type = ancestor.data( 'type' );
  364. return ( type == 'group' || type == 'separator' );
  365. } );
  366. }
  367. };
  368. /**
  369. * Set active element in tool by provided type and name.
  370. *
  371. * @param {String} type
  372. * @param {String} name
  373. */
  374. ToolbarModifier.prototype._setActiveElement = function( type, name ) {
  375. // clear current active element
  376. if ( this.currentActive )
  377. this.currentActive.elem.removeClass( 'active' );
  378. if ( type === null ) {
  379. this._dehighlightActiveToolGroup();
  380. this.currentActive = null;
  381. return;
  382. }
  383. var liElem = this.mainContainer.findOne( 'ul[data-type=table-body] li[data-type="' + type + '"][data-name="' + name + '"]' );
  384. liElem.addClass( 'active' );
  385. // setup model
  386. this.currentActive = {
  387. type: type,
  388. name: name,
  389. elem: liElem
  390. };
  391. // highlight group in toolbar
  392. if ( type == 'group' )
  393. this._highlightGroup( name );
  394. if ( type == 'separator' )
  395. this._dehighlightActiveToolGroup();
  396. };
  397. /**
  398. * @returns {CKEDITOR.dom.element|null}
  399. */
  400. ToolbarModifier.prototype.getActiveToolGroup = function() {
  401. if ( this.editorInstance.container )
  402. return this.editorInstance.container.findOne( '.cke_toolgroup.active, .cke_toolbar.active' );
  403. else
  404. return null;
  405. };
  406. /**
  407. * @private
  408. */
  409. ToolbarModifier.prototype._dehighlightActiveToolGroup = function() {
  410. var currentActive = this.getActiveToolGroup();
  411. if ( currentActive )
  412. currentActive.removeClass( 'active' );
  413. // @see ToolbarModifier.prototype._highlightGroup.
  414. if ( this.editorInstance.container ) {
  415. this.editorInstance.container.removeClass( 'some-toolbar-active' );
  416. }
  417. };
  418. /**
  419. * Highlight group by its name, and dehighlight current group.
  420. *
  421. * @param {String} name
  422. */
  423. ToolbarModifier.prototype._highlightGroup = function( name ) {
  424. if ( !this.editorInstance.container )
  425. return;
  426. var foundBtnName = this.getFirstEnabledButtonInGroup( name ),
  427. foundBtn = this.editorInstance.container.findOne( '.cke_button__' + foundBtnName + ', .cke_combo__' + foundBtnName );
  428. this._dehighlightActiveToolGroup();
  429. // Helpful to dim other toolbar groups if one is highlighted.
  430. if ( this.editorInstance.container ) {
  431. this.editorInstance.container.addClass( 'some-toolbar-active' );
  432. }
  433. if ( foundBtn ) {
  434. var btnToolbar = ToolbarModifier.getFirstAncestor( foundBtn, function( ancestor ) {
  435. return ancestor.hasClass( 'cke_toolbar' );
  436. } );
  437. if ( btnToolbar )
  438. btnToolbar.addClass( 'active' );
  439. }
  440. };
  441. /**
  442. * @param {String} groupName
  443. * @return {String|null}
  444. */
  445. ToolbarModifier.prototype.getFirstEnabledButtonInGroup = function( groupName ) {
  446. var groups = this.actualConfig.toolbarGroups,
  447. groupIndex = this.getGroupIndex( groupName ),
  448. group = groups[ groupIndex ];
  449. if ( groupIndex === -1 ) {
  450. return null;
  451. }
  452. var max = group.groups ? group.groups.length : 0;
  453. for ( var i = 0; i < max; i += 1 ) {
  454. var currSubgroupName = group.groups[ i ].name,
  455. firstEnabled = this.getFirstEnabledButtonInSubgroup( currSubgroupName );
  456. if ( firstEnabled )
  457. return firstEnabled;
  458. }
  459. return null;
  460. };
  461. /**
  462. * @param {String} subgroupName
  463. * @returns {String|null}
  464. */
  465. ToolbarModifier.prototype.getFirstEnabledButtonInSubgroup = function( subgroupName ) {
  466. var subgroupBtns = this.fullToolbarEditor.buttonsByGroup[ subgroupName ];
  467. var max = subgroupBtns ? subgroupBtns.length : 0;
  468. for ( var i = 0; i < max; i += 1 ) {
  469. var currBtnName = subgroupBtns[ i ].name;
  470. if ( !this.isButtonRemoved( currBtnName ) )
  471. return currBtnName;
  472. }
  473. return null;
  474. };
  475. /**
  476. * Sets up parameters and call adequate action.
  477. *
  478. * @param {CKEDITOR.dom.element} checkbox
  479. * @private
  480. */
  481. ToolbarModifier.prototype._handleCheckboxClicked = function( checkbox ) {
  482. var closestLi = checkbox.getAscendant( 'li' ),
  483. elementName = closestLi.data( 'name' ),
  484. aboutToAddToRemoved = !checkbox.$.checked;
  485. if ( aboutToAddToRemoved )
  486. this._addButtonToRemoved( elementName );
  487. else
  488. this._removeButtonFromRemoved( elementName );
  489. };
  490. /**
  491. * Sets up parameters and call adequate action.
  492. *
  493. * @param {HTMLAnchorElement} anchor
  494. * @private
  495. */
  496. ToolbarModifier.prototype._handleAnchorClicked = function( anchor ) {
  497. var anchorDOM = new CKEDITOR.dom.element( anchor ),
  498. relativeLi = anchorDOM.getAscendant( 'li' ),
  499. relativeUl = relativeLi.getAscendant( 'ul' ),
  500. elementType = relativeLi.data( 'type' ),
  501. elementName = relativeLi.data( 'name' ),
  502. direction = anchorDOM.data( 'direction' ),
  503. nearestLi = ( direction === 'up' ? relativeLi.getPrevious() : relativeLi.getNext() ),
  504. groupName,
  505. subgroupName,
  506. newIndex;
  507. // nothing to do
  508. if ( anchorDOM.hasClass( 'disabled' ) )
  509. return null;
  510. // remove separator and nothing else
  511. if ( anchorDOM.hasClass( 'remove' ) ) {
  512. relativeLi.remove();
  513. this._removeSeparator( relativeLi.data( 'name' ) );
  514. this._setActiveElement( null );
  515. return { action: 'remove' };
  516. }
  517. if ( !anchorDOM.hasClass( 'move' ) || !nearestLi )
  518. return { action: null };
  519. // move group or separator
  520. if ( elementType === 'group' || elementType === 'separator' ) {
  521. groupName = elementName;
  522. newIndex = this._moveGroup( direction, groupName );
  523. }
  524. // move subgroup
  525. if ( elementType === 'subgroup' ) {
  526. subgroupName = elementName;
  527. groupName = relativeLi.getAscendant( 'li' ).data( 'name' );
  528. newIndex = this._moveSubgroup( direction, groupName, subgroupName );
  529. }
  530. // Visual effect
  531. if ( direction === 'up' )
  532. relativeLi.insertBefore( relativeUl.getChild( newIndex ) );
  533. if ( direction === 'down' )
  534. relativeLi.insertAfter( relativeUl.getChild( newIndex ) );
  535. // Should know whether there is next li element after modifications.
  536. var nextLi = relativeLi;
  537. // We are looking for next li element in list (to check whether current one is the last one)
  538. var found;
  539. while ( nextLi = ( direction === 'up' ? nextLi.getPrevious() : nextLi.getNext() ) ) {
  540. if ( !this.emptyVisible && nextLi.hasClass( 'empty' ) ) {
  541. continue;
  542. }
  543. found = nextLi;
  544. break;
  545. }
  546. // If not found, it means that we reached end.
  547. if ( !found ) {
  548. var selector = ( '[data-direction="' + ( direction === 'up' ? 'down' : 'up' ) + '"]' );
  549. // Shifting direction.
  550. this.cachedActiveElement = anchorDOM.getParent().findOne( selector );
  551. }
  552. this._refreshMoveBtnsAvalibility();
  553. this._refreshBtnTabIndexes();
  554. return {
  555. action: 'move'
  556. };
  557. };
  558. /**
  559. * First element can not be moved up, and last element can not be moved down,
  560. * so they are disabled.
  561. */
  562. ToolbarModifier.prototype._refreshMoveBtnsAvalibility = function() {
  563. var that = this,
  564. disabledBtns = this.mainContainer.find( 'ul[data-type=table-body] li > p > span > button.move.disabled' );
  565. // enabling all disabled buttons
  566. var max = disabledBtns.count();
  567. for ( var i = 0; i < max; i += 1 ) {
  568. var currentBtn = disabledBtns.getItem( i );
  569. currentBtn.removeClass( 'disabled' );
  570. }
  571. function disableElementsInLists( ulList ) {
  572. var max = ulList.count();
  573. for ( i = 0; i < max; i += 1 ) {
  574. that._disableElementsInList( ulList.getItem( i ) );
  575. }
  576. }
  577. // Disable buttons in toolbars.
  578. disableElementsInLists( this.mainContainer.find( 'ul[data-type=table-body]' ) );
  579. // Disable buttons in toolbar groups.
  580. disableElementsInLists( this.mainContainer.find( 'ul[data-type=table-body] > li > ul' ) );
  581. };
  582. /**
  583. * @private
  584. */
  585. ToolbarModifier.prototype._refreshBtnTabIndexes = function() {
  586. var tabindexed = this.mainContainer.find( '[data-tab="true"]' );
  587. var max = tabindexed.count();
  588. for ( var i = 0; i < max; i++ ) {
  589. var item = tabindexed.getItem( i ),
  590. disabled = item.hasClass( 'disabled' );
  591. item.setAttribute( 'tabindex', disabled ? -1 : i );
  592. }
  593. };
  594. /**
  595. * Disable buttons to move elements up and down which should be disabled.
  596. *
  597. * @param {CKEDITOR.dom.element} ul
  598. * @private
  599. */
  600. ToolbarModifier.prototype._disableElementsInList = function( ul ) {
  601. var liList = ul.getChildren();
  602. if ( !liList.count() )
  603. return;
  604. var firstDisabled, lastDisabled;
  605. if ( this.emptyVisible ) {
  606. firstDisabled = ul.getFirst();
  607. lastDisabled = ul.getLast();
  608. } else {
  609. firstDisabled = ul.getFirst( isNotEmptyChecker );
  610. lastDisabled = ul.getLast( isNotEmptyChecker );
  611. }
  612. function isNotEmptyChecker( element ) {
  613. return !element.hasClass( 'empty' );
  614. }
  615. if ( firstDisabled )
  616. var firstDisabledBtn = firstDisabled.findOne( 'p button[data-direction="up"]' );
  617. if ( lastDisabled )
  618. var lastDisabledBtn = lastDisabled.findOne( 'p button[data-direction="down"]' );
  619. if ( firstDisabledBtn ) {
  620. firstDisabledBtn.addClass( 'disabled' );
  621. firstDisabledBtn.setAttribute( 'tabindex', '-1' );
  622. }
  623. if ( lastDisabledBtn ) {
  624. lastDisabledBtn.addClass( 'disabled' );
  625. lastDisabledBtn.setAttribute( 'tabindex', '-1' );
  626. }
  627. };
  628. /**
  629. * Gets group index in actual config toolbarGroups
  630. *
  631. * @param {String} name
  632. * @returns {Number}
  633. */
  634. ToolbarModifier.prototype.getGroupIndex = function( name ) {
  635. var groups = this.actualConfig.toolbarGroups;
  636. var max = groups.length;
  637. for ( var i = 0; i < max; i += 1 ) {
  638. if ( groups[ i ].name === name )
  639. return i;
  640. }
  641. return -1;
  642. };
  643. /**
  644. * Handle adding separator.
  645. *
  646. * @private
  647. */
  648. ToolbarModifier.prototype._addSeparator = function() {
  649. var separatorIndex = this._determineSeparatorToAddIndex(),
  650. separator = ToolbarModifier.createSeparatorLiteral(),
  651. domSeparator = CKEDITOR.dom.element.createFromHtml( ToolbarModifier.getToolbarSeparatorString( separator ) );
  652. this.actualConfig.toolbarGroups.splice( separatorIndex, 0, separator );
  653. domSeparator.insertBefore( this.modifyContainer.findOne( 'ul[data-type=table-body]' ).getChild( separatorIndex ) );
  654. this._setActiveElement( 'separator', separator.name );
  655. this._refreshMoveBtnsAvalibility();
  656. this._refreshBtnTabIndexes();
  657. this._refreshEditor();
  658. };
  659. /**
  660. * Handle removing separator.
  661. *
  662. * @param {String} name
  663. */
  664. ToolbarModifier.prototype._removeSeparator = function( name ) {
  665. var separatorIndex = CKEDITOR.tools.indexOf( this.actualConfig.toolbarGroups, function( group ) {
  666. return group.type == 'separator' && group.name == name;
  667. } );
  668. this.actualConfig.toolbarGroups.splice( separatorIndex, 1 );
  669. this._refreshMoveBtnsAvalibility();
  670. this._refreshBtnTabIndexes();
  671. this._refreshEditor();
  672. };
  673. /**
  674. * Determine index where separator should be added, based on currently selected element.
  675. *
  676. * @returns {Number}
  677. * @private
  678. */
  679. ToolbarModifier.prototype._determineSeparatorToAddIndex = function() {
  680. if ( !this.currentActive )
  681. return 0;
  682. var groupLi;
  683. if ( this.currentActive.elem.data( 'type' ) == 'group' || this.currentActive.elem.data( 'type' ) == 'separator' )
  684. groupLi = this.currentActive.elem;
  685. else
  686. groupLi = this.currentActive.elem.getAscendant( 'li' );
  687. return groupLi.getIndex();
  688. };
  689. /**
  690. * @param {Array} elementsArray
  691. * @param {Number} elementIndex
  692. * @param {String} direction
  693. * @returns {Number}
  694. * @private
  695. */
  696. ToolbarModifier.prototype._moveElement = function( elementsArray, elementIndex, direction ) {
  697. var nextIndex;
  698. if ( this.emptyVisible )
  699. nextIndex = ( direction == 'down' ? elementIndex + 1 : elementIndex - 1 );
  700. else {
  701. // When empty elements are not visible, there is need to skip them.
  702. nextIndex = ToolbarModifier.getFirstElementIndexWith( elementsArray, elementIndex, direction, isEmptyOrSeparatorChecker );
  703. }
  704. function isEmptyOrSeparatorChecker( element ) {
  705. return element.totalBtns || element.type == 'separator';
  706. }
  707. var offset = nextIndex - elementIndex;
  708. return ToolbarModifier.moveTo( offset, elementsArray, elementIndex );
  709. };
  710. /**
  711. * Moves group located in config level up or down and refresh editor.
  712. *
  713. * @param {String} direction
  714. * @param {String} groupName
  715. * @returns {Number}
  716. */
  717. ToolbarModifier.prototype._moveGroup = function( direction, groupName ) {
  718. var groupIndex = this.getGroupIndex( groupName ),
  719. groups = this.actualConfig.toolbarGroups,
  720. newIndex = this._moveElement( groups, groupIndex, direction );
  721. this._refreshMoveBtnsAvalibility();
  722. this._refreshBtnTabIndexes();
  723. this._refreshEditor();
  724. return newIndex;
  725. };
  726. /**
  727. * Moves subgroup located in config level up or down and refresh editor.
  728. *
  729. * @param {String} direction
  730. * @param {String} groupName
  731. * @param {String} subgroupName
  732. * @private
  733. */
  734. ToolbarModifier.prototype._moveSubgroup = function( direction, groupName, subgroupName ) {
  735. var groupIndex = this.getGroupIndex( groupName ),
  736. groups = this.actualConfig.toolbarGroups,
  737. group = groups[ groupIndex ],
  738. subgroupIndex = CKEDITOR.tools.indexOf( group.groups, function( subgroup ) {
  739. return subgroup.name == subgroupName;
  740. } ),
  741. newIndex = this._moveElement( group.groups, subgroupIndex, direction );
  742. this._refreshEditor();
  743. return newIndex;
  744. };
  745. /**
  746. * Set `totalBtns` property in `actualConfig.toolbarGroups` elements.
  747. *
  748. * @private
  749. */
  750. ToolbarModifier.prototype._calculateTotalBtns = function() {
  751. var groups = this.actualConfig.toolbarGroups;
  752. var i = groups.length;
  753. // from the end
  754. while ( i-- ) {
  755. var currentGroup = groups[ i ],
  756. totalBtns = ToolbarModifier.getTotalGroupButtonsNumber( currentGroup, this.fullToolbarEditor );
  757. if ( currentGroup.type == 'separator' ) {
  758. // nothing to do with separator
  759. continue;
  760. }
  761. currentGroup.totalBtns = totalBtns;
  762. }
  763. };
  764. /**
  765. * Add button to removeButtons field in config and refresh editor.
  766. *
  767. * @param {String} buttonName
  768. * @private
  769. */
  770. ToolbarModifier.prototype._addButtonToRemoved = function( buttonName ) {
  771. if ( CKEDITOR.tools.indexOf( this.removedButtons, buttonName ) != -1 )
  772. throw 'Button already added to removed';
  773. this.removedButtons.push( buttonName );
  774. this.actualConfig.removeButtons = this.removedButtons.join( ',' );
  775. this._refreshEditor();
  776. };
  777. /**
  778. * Remove button from removeButtons field in config and refresh editor.
  779. *
  780. * @param {String} buttonName
  781. * @private
  782. */
  783. ToolbarModifier.prototype._removeButtonFromRemoved = function( buttonName ) {
  784. var foundAtIndex = CKEDITOR.tools.indexOf( this.removedButtons, buttonName );
  785. if ( foundAtIndex === -1 )
  786. throw 'Trying to remove button from removed, but not found';
  787. this.removedButtons.splice( foundAtIndex, 1 );
  788. this.actualConfig.removeButtons = this.removedButtons.join( ',' );
  789. this._refreshEditor();
  790. };
  791. /**
  792. * Parse group "model" to configuration value
  793. *
  794. * @param {Object} group
  795. * @returns {Object}
  796. * @static
  797. */
  798. ToolbarModifier.parseGroupToConfigValue = function( group ) {
  799. if ( group.type == 'separator' ) {
  800. return '/';
  801. }
  802. var groups = group.groups,
  803. max = groups.length;
  804. delete group.totalBtns;
  805. for ( var i = 0; i < max; i += 1 ) {
  806. groups[ i ] = groups[ i ].name;
  807. }
  808. return group;
  809. };
  810. /**
  811. * Find closest Li ancestor in DOM tree which is group or separator element
  812. *
  813. * @param {CKEDITOR.dom.element} element
  814. * @returns {CKEDITOR.dom.element}
  815. * @static
  816. */
  817. ToolbarModifier.getGroupOrSeparatorLiAncestor = function( element ) {
  818. if ( element.$ instanceof HTMLLIElement && element.data( 'type' ) == 'group' )
  819. return element;
  820. else {
  821. return ToolbarModifier.getFirstAncestor( element, function( ancestor ) {
  822. var type = ancestor.data( 'type' );
  823. return ( type == 'group' || type == 'separator' );
  824. } );
  825. }
  826. };
  827. /**
  828. * Create separator literal with unique id.
  829. *
  830. * @public
  831. * @static
  832. * @return {Object}
  833. */
  834. ToolbarModifier.createSeparatorLiteral = function() {
  835. return {
  836. type: 'separator',
  837. name: ( 'separator' + CKEDITOR.tools.getNextNumber() )
  838. };
  839. };
  840. /**
  841. * Creates HTML unordered list string based on toolbarGroups field in config.
  842. *
  843. * @returns {String}
  844. * @static
  845. */
  846. ToolbarModifier.prototype._toolbarConfigToListString = function() {
  847. var groups = this.actualConfig.toolbarGroups || [],
  848. listString = '<ul data-type="table-body">';
  849. var max = groups.length;
  850. for ( var i = 0; i < max; i += 1 ) {
  851. var currentGroup = groups[ i ];
  852. if ( currentGroup.type === 'separator' )
  853. listString += ToolbarModifier.getToolbarSeparatorString( currentGroup );
  854. else
  855. listString += this._getToolbarGroupString( currentGroup );
  856. }
  857. listString += '</ul>';
  858. var headerString = ToolbarModifier.getToolbarHeaderString();
  859. return headerString + listString;
  860. };
  861. /**
  862. * Created HTML group list element based on group field in config.
  863. *
  864. * @param {Object} group
  865. * @returns {String}
  866. * @private
  867. */
  868. ToolbarModifier.prototype._getToolbarGroupString = function( group ) {
  869. var subgroups = group.groups,
  870. groupString = '';
  871. groupString += [
  872. '<li ',
  873. 'data-type="group" ',
  874. 'data-name="', group.name, '" ',
  875. ( group.totalBtns ? '' : 'class="empty"' ),
  876. '>'
  877. ].join( '' );
  878. groupString += ToolbarModifier.getToolbarElementPreString( group ) + '<ul>';
  879. var max = subgroups.length;
  880. for ( var i = 0; i < max; i += 1 ) {
  881. var currentSubgroup = subgroups[ i ],
  882. subgroupBtns = this.fullToolbarEditor.buttonsByGroup[ currentSubgroup.name ];
  883. groupString += this._getToolbarSubgroupString( currentSubgroup, subgroupBtns );
  884. }
  885. groupString += '</ul></li>';
  886. return groupString;
  887. };
  888. /**
  889. * @param {Object} separator
  890. * @returns {String}
  891. * @static
  892. */
  893. ToolbarModifier.getToolbarSeparatorString = function( separator ) {
  894. return [
  895. '<li ',
  896. 'data-type="', separator.type , '" ',
  897. 'data-name="', separator.name , '"',
  898. '>',
  899. ToolbarModifier.getToolbarElementPreString( 'row separator' ),
  900. '</li>'
  901. ].join( '' );
  902. };
  903. /**
  904. * @returns {string}
  905. */
  906. ToolbarModifier.getToolbarHeaderString = function() {
  907. return '<ul data-type="table-header">' +
  908. '<li data-type="header">' +
  909. '<p>Toolbars</p>' +
  910. '<ul>' +
  911. '<li>' +
  912. '<p>Toolbar groups</p>' +
  913. '<p>Toolbar group items</p>' +
  914. '</li>' +
  915. '</ul>' +
  916. '</li>' +
  917. '</ul>';
  918. };
  919. /**
  920. * Find and return first ancestor of element provided in first argument
  921. * which match the criteria checked in function provided in second argument.
  922. *
  923. * @param {CKEDITOR.dom.element} element
  924. * @param {Function} checker
  925. * @returns {CKEDITOR.dom.element|null}
  926. */
  927. ToolbarModifier.getFirstAncestor = function( element, checker ) {
  928. var ancestors = element.getParents(),
  929. i = ancestors.length;
  930. while ( i-- ) {
  931. if ( checker( ancestors[ i ] ) )
  932. return ancestors[ i ];
  933. }
  934. return null;
  935. };
  936. /**
  937. * Looking through array elements start from index provided in second argument
  938. * and go 'up' or 'down' in array
  939. * last argument is condition checker which should return Boolean value
  940. *
  941. * User cases:
  942. *
  943. * ToolbarModifier.getFirstElementIndexWith( [3, 4, 8, 1, 4], 2, 'down', function( elem ) { return elem == 4; } ); // 4
  944. * ToolbarModifier.getFirstElementIndexWith( [3, 4, 8, 1, 4], 2, 'up', function( elem ) { return elem == 4; } ); // 1
  945. *
  946. * @param {Array} array
  947. * @param {Number} i
  948. * @param {String} direction 'up' or 'down'
  949. * @param {Function} conditionChecker
  950. * @static
  951. * @returns {Number} index of found element
  952. */
  953. ToolbarModifier.getFirstElementIndexWith = function( array, i, direction, conditionChecker ) {
  954. function whileChecker() {
  955. var result;
  956. if ( direction === 'up' )
  957. result = i--;
  958. else
  959. result = ( ++i < array.length );
  960. return result;
  961. }
  962. while ( whileChecker() ) {
  963. if ( conditionChecker( array[ i ] ) )
  964. return i;
  965. }
  966. return -1;
  967. };
  968. /**
  969. * Moves array element at index level up or down.
  970. *
  971. * @static
  972. * @param {String} direction
  973. * @param {Array} array
  974. * @param {Number} index
  975. * @returns {Number}
  976. */
  977. ToolbarModifier.moveTo = function( offset, array, index ) {
  978. var element, newIndex;
  979. if ( index !== -1 )
  980. element = array.splice( index, 1 )[ 0 ];
  981. newIndex = index + offset;
  982. array.splice( newIndex, 0, element );
  983. return newIndex;
  984. };
  985. /**
  986. * @static
  987. * @param {Object} subgroup
  988. * @returns {Number}
  989. */
  990. ToolbarModifier.getTotalSubGroupButtonsNumber = function( subgroup, fullToolbarEditor ) {
  991. var subgroupName = ( typeof subgroup == 'string' ? subgroup : subgroup.name ),
  992. subgroupBtns = fullToolbarEditor.buttonsByGroup[ subgroupName ];
  993. return ( subgroupBtns ? subgroupBtns.length : 0 );
  994. };
  995. /**
  996. * Returns all buttons number in group which are nested in subgroups also.
  997. *
  998. * @param {Object} group
  999. * @param {ToolbarModifier.FullToolbarEditor}
  1000. * @static
  1001. * @returns {Number}
  1002. */
  1003. ToolbarModifier.getTotalGroupButtonsNumber = function( group, fullToolbarEditor ) {
  1004. var total = 0,
  1005. subgroups = group.groups;
  1006. var max = subgroups ? subgroups.length : 0;
  1007. for ( var i = 0; i < max; i += 1 )
  1008. total += ToolbarModifier.getTotalSubGroupButtonsNumber( subgroups[ i ], fullToolbarEditor );
  1009. return total;
  1010. };
  1011. /**
  1012. * Creates HTML subgroup list element based on subgroup field in config.
  1013. *
  1014. * @param {Object} subgroup
  1015. * @param {Array} groupBtns
  1016. * @returns {String}
  1017. * @private
  1018. */
  1019. ToolbarModifier.prototype._getToolbarSubgroupString = function( subgroup, groupBtns ) {
  1020. var subgroupString = '';
  1021. subgroupString += [
  1022. '<li ',
  1023. 'data-type="subgroup" ',
  1024. 'data-name="', subgroup.name, '" ',
  1025. ( subgroup.totalBtns ? '' : 'class="empty" ' ),
  1026. '>'
  1027. ].join( '' );
  1028. subgroupString += ToolbarModifier.getToolbarElementPreString( subgroup.name );
  1029. subgroupString += '<ul>';
  1030. var max = groupBtns ? groupBtns.length : 0;
  1031. for ( var i = 0; i < max; i += 1 )
  1032. subgroupString += this.getButtonString( groupBtns[ i ] );
  1033. subgroupString += '</ul>';
  1034. subgroupString += '</li>';
  1035. return subgroupString;
  1036. };
  1037. /**
  1038. * @param {String} buttonName
  1039. * @returns {String|null}
  1040. * @private
  1041. */
  1042. ToolbarModifier.prototype._getConfigButtonName = function( buttonName ) {
  1043. var items = this.fullToolbarEditor.editorInstance.ui.items;
  1044. var name;
  1045. for ( name in items ) {
  1046. if ( items[ name ].name == buttonName )
  1047. return name;
  1048. }
  1049. return null;
  1050. };
  1051. /**
  1052. * @param {String} buttonName
  1053. * @returns {Boolean}
  1054. */
  1055. ToolbarModifier.prototype.isButtonRemoved = function( buttonName ) {
  1056. return CKEDITOR.tools.indexOf( this.removedButtons, this._getConfigButtonName( buttonName ) ) != -1;
  1057. };
  1058. /**
  1059. * @param {CKEDITOR.ui.button/CKEDITOR.ui.richCombo} button
  1060. * @returns {String}
  1061. * @public
  1062. */
  1063. ToolbarModifier.prototype.getButtonString = function( button ) {
  1064. var checked = ( this.isButtonRemoved( button.name ) ? '' : 'checked="checked"' );
  1065. return [
  1066. '<li data-tab="true" data-type="button" data-name="', this._getConfigButtonName( button.name ), '">',
  1067. '<label title="', button.label, '" >',
  1068. '<input ',
  1069. 'tabindex="-1"',
  1070. 'type="checkbox"',
  1071. checked,
  1072. '/>',
  1073. button.$.getOuterHtml(),
  1074. '</label>',
  1075. '</li>'
  1076. ].join( '' );
  1077. };
  1078. /**
  1079. * Creates group header string.
  1080. *
  1081. * @param {Object|String} group
  1082. * @returns {String}
  1083. * @static
  1084. */
  1085. ToolbarModifier.getToolbarElementPreString = function( group ) {
  1086. var name = ( group.name ? group.name : group );
  1087. return [
  1088. '<p>',
  1089. '<span>',
  1090. '<button title="Move element upward" data-tab="true" data-direction="up" class="move icon-up-big"></button>',
  1091. '<button title="Move element downward" data-tab="true" data-direction="down" class="move icon-down-big"></button>',
  1092. ( name == 'row separator' ? '<button title="Remove element" data-tab="true" class="remove icon-trash"></button>' : '' ),
  1093. name,
  1094. '</span>',
  1095. '</p>'
  1096. ].join( '' );
  1097. };
  1098. /**
  1099. * @static
  1100. * @param {String} cfg
  1101. * @returns {String}
  1102. */
  1103. ToolbarModifier.evaluateToolbarGroupsConfig = function( cfg ) {
  1104. cfg = ( function( cfg ) {
  1105. var config = {}, result;
  1106. /*jshint -W002 */
  1107. try {
  1108. result = eval( '(' + cfg + ')' );
  1109. } catch ( e ) {
  1110. try {
  1111. result = eval( cfg );
  1112. } catch ( e ) {
  1113. return null;
  1114. }
  1115. }
  1116. /*jshint +W002 */
  1117. if ( config.toolbarGroups && typeof config.toolbarGroups.length === 'number' ) {
  1118. return JSON.stringify( config );
  1119. } else if ( result && typeof result.length === 'number' ) {
  1120. return JSON.stringify( { toolbarGroups: result } );
  1121. } else if ( result && result.toolbarGroups ) {
  1122. return JSON.stringify( result );
  1123. } else {
  1124. return null;
  1125. }
  1126. }( cfg ) );
  1127. return cfg;
  1128. };
  1129. return ToolbarModifier;
  1130. } )();