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