aboutsummaryrefslogtreecommitdiff
path: root/sources/samples/toolbarconfigurator/js
diff options
context:
space:
mode:
Diffstat (limited to 'sources/samples/toolbarconfigurator/js')
-rw-r--r--sources/samples/toolbarconfigurator/js/abstracttoolbarmodifier.js566
-rw-r--r--sources/samples/toolbarconfigurator/js/fulltoolbareditor.js365
-rw-r--r--sources/samples/toolbarconfigurator/js/toolbarmodifier.js1366
-rw-r--r--sources/samples/toolbarconfigurator/js/toolbartextmodifier.js623
4 files changed, 2920 insertions, 0 deletions
diff --git a/sources/samples/toolbarconfigurator/js/abstracttoolbarmodifier.js b/sources/samples/toolbarconfigurator/js/abstracttoolbarmodifier.js
new file mode 100644
index 0000000..af6cd62
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/js/abstracttoolbarmodifier.js
@@ -0,0 +1,566 @@
1/* global ToolbarConfigurator */
2
3'use strict';
4
5// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create
6if ( typeof Object.create != 'function' ) {
7 ( function() {
8 var F = function() {};
9 Object.create = function( o ) {
10 if ( arguments.length > 1 ) {
11 throw Error( 'Second argument not supported' );
12 }
13 if ( o === null ) {
14 throw Error( 'Cannot set a null [[Prototype]]' );
15 }
16 if ( typeof o != 'object' ) {
17 throw TypeError( 'Argument must be an object' );
18 }
19 F.prototype = o;
20 return new F();
21 };
22 } )();
23}
24
25// Copy of the divarea plugin (with some enhancements), so we always have some editable mode, regardless of the build's config.
26CKEDITOR.plugins.add( 'toolbarconfiguratorarea', {
27 // Use afterInit to override wysiwygarea's mode. May still fail to override divarea, but divarea is nice.
28 afterInit: function( editor ) {
29 editor.addMode( 'wysiwyg', function( callback ) {
30 var editingBlock = CKEDITOR.dom.element.createFromHtml( '<div class="cke_wysiwyg_div cke_reset" hidefocus="true"></div>' );
31
32 var contentSpace = editor.ui.space( 'contents' );
33 contentSpace.append( editingBlock );
34
35 editingBlock = editor.editable( editingBlock );
36
37 editingBlock.detach = CKEDITOR.tools.override( editingBlock.detach,
38 function( org ) {
39 return function() {
40 org.apply( this, arguments );
41 this.remove();
42 };
43 } );
44
45 editor.setData( editor.getData( 1 ), callback );
46 editor.fire( 'contentDom' );
47 } );
48
49 // Additions to the divarea.
50
51 // Speed up data processing.
52 editor.dataProcessor.toHtml = function( html ) {
53 return html;
54 };
55 editor.dataProcessor.toDataFormat = function( html ) {
56 return html;
57 };
58
59 // End of the additions.
60 }
61} );
62
63// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
64if ( !Object.keys ) {
65 Object.keys = ( function() {
66 var hasOwnProperty = Object.prototype.hasOwnProperty,
67 hasDontEnumBug = !( { toString: null } ).propertyIsEnumerable( 'toString' ),
68 dontEnums = [
69 'toString',
70 'toLocaleString',
71 'valueOf',
72 'hasOwnProperty',
73 'isPrototypeOf',
74 'propertyIsEnumerable',
75 'constructor'
76 ],
77 dontEnumsLength = dontEnums.length;
78
79 return function( obj ) {
80 if ( typeof obj !== 'object' && ( typeof obj !== 'function' || obj === null ) )
81 throw new TypeError( 'Object.keys called on non-object' );
82
83 var result = [], prop, i;
84
85 for ( prop in obj ) {
86 if ( hasOwnProperty.call( obj, prop ) )
87 result.push( prop );
88
89 }
90
91 if ( hasDontEnumBug ) {
92 for ( i = 0; i < dontEnumsLength; i++ ) {
93 if ( hasOwnProperty.call( obj, dontEnums[ i ] ) )
94 result.push( dontEnums[ i ] );
95
96 }
97 }
98 return result;
99 };
100 }() );
101}
102
103( function() {
104 /**
105 * @class ToolbarConfigurator.AbstractToolbarModifier
106 * @param {String} editorId An id of modified editor
107 * @constructor
108 */
109 function AbstractToolbarModifier( editorId, cfg ) {
110 this.cfg = cfg || {};
111 this.hidden = false;
112 this.editorId = editorId;
113 this.fullToolbarEditor = new ToolbarConfigurator.FullToolbarEditor();
114
115 this.mainContainer = null;
116
117 this.originalConfig = null;
118 this.actualConfig = null;
119
120 this.waitForReady = false;
121 this.isEditableVisible = false;
122
123 this.toolbarContainer = null;
124 this.toolbarButtons = [];
125 }
126
127 // Expose the class.
128 ToolbarConfigurator.AbstractToolbarModifier = AbstractToolbarModifier;
129
130 /**
131 * @param {String} config
132 */
133 AbstractToolbarModifier.prototype.setConfig = function( config ) {
134 this._onInit( undefined, config, true );
135 };
136
137 /**
138 * @param {Function} [callback]
139 */
140 AbstractToolbarModifier.prototype.init = function( callback ) {
141 var that = this;
142
143 this.mainContainer = new CKEDITOR.dom.element( 'div' );
144
145 if ( this.fullToolbarEditor.editorInstance !== null ) {
146 throw 'Only one instance of ToolbarModifier is allowed';
147 }
148
149 if ( !this.editorInstance ) {
150 // Do not refresh yet, let's wait for the full toolbar editor (see below).
151 this._createEditor( false );
152 }
153
154 this.editorInstance.once( 'loaded', function() {
155 that.fullToolbarEditor.init( function() {
156 that._onInit( callback );
157
158 if ( typeof that.onRefresh == 'function' ) {
159 that.onRefresh();
160 }
161 }, that.editorInstance.config );
162 } );
163
164 return this.mainContainer;
165 };
166
167 /**
168 * Called editor initialization finished.
169 *
170 * @param {Function} callback
171 * @param {String} [actualConfig]
172 * @private
173 */
174 AbstractToolbarModifier.prototype._onInit = function( callback, actualConfig ) {
175 this.originalConfig = this.editorInstance.config;
176
177 if ( !actualConfig ) {
178 this.actualConfig = JSON.parse( JSON.stringify( this.originalConfig ) );
179 } else {
180 this.actualConfig = JSON.parse( actualConfig );
181 }
182
183 if ( !this.actualConfig.toolbarGroups && !this.actualConfig.toolbar ) {
184 this.actualConfig.toolbarGroups = getDefaultToolbarGroups( this.editorInstance );
185 }
186
187 if ( typeof callback === 'function' )
188 callback( this.mainContainer );
189
190 // Here we are going to keep only `name` and `groups` data from editor `toolbar` property.
191 function getDefaultToolbarGroups( editor ) {
192 var toolbarGroups = editor.toolbar,
193 copy = [];
194
195 var max = toolbarGroups.length;
196 for ( var i = 0; i < max; i++ ) {
197 var group = toolbarGroups[ i ];
198
199 if ( typeof group == 'string' ) {
200 copy.push( group ); // separator
201 } else {
202 copy.push( {
203 name: group.name,
204 groups: group.groups ? group.groups.slice() : []
205 } );
206 }
207 }
208
209 return copy;
210 }
211 };
212
213 /**
214 * Creates DOM structure of tool.
215 *
216 * @returns {CKEDITOR.dom.element}
217 * @private
218 */
219 AbstractToolbarModifier.prototype._createModifier = function() {
220 this.mainContainer.addClass( 'unselectable' );
221
222 if ( this.modifyContainer ) {
223 this.modifyContainer.remove();
224 }
225
226 this.modifyContainer = new CKEDITOR.dom.element( 'div' );
227 this.modifyContainer.addClass( 'toolbarModifier' );
228
229 this.mainContainer.append( this.modifyContainer );
230
231 return this.mainContainer;
232 };
233
234 /**
235 * Find editable area in CKEditor instance DOM container
236 *
237 * @returns {CKEDITOR.dom.element}
238 */
239 AbstractToolbarModifier.prototype.getEditableArea = function() {
240 var selector = ( '#' + this.editorInstance.id + '_contents' );
241
242 return this.editorInstance.container.findOne( selector );
243 };
244
245 /**
246 * Hide editable area in modified editor by sets its height to 0.
247 *
248 * @private
249 */
250 AbstractToolbarModifier.prototype._hideEditable = function() {
251 var area = this.getEditableArea();
252
253 this.isEditableVisible = false;
254
255 this.lastEditableAreaHeight = area.getStyle( 'height' );
256 area.setStyle( 'height', '0' );
257 };
258
259 /**
260 * Show editable area in modified editor.
261 *
262 * @private
263 */
264 AbstractToolbarModifier.prototype._showEditable = function() {
265 this.isEditableVisible = true;
266
267 this.getEditableArea().setStyle( 'height', this.lastEditableAreaHeight || 'auto' );
268 };
269
270 /**
271 * Toggle editable area visibility.
272 *
273 * @private
274 */
275 AbstractToolbarModifier.prototype._toggleEditable = function() {
276 if ( this.isEditableVisible )
277 this._hideEditable();
278 else
279 this._showEditable();
280 };
281
282 /**
283 * Usually called when configuration changes.
284 *
285 * @private
286 */
287 AbstractToolbarModifier.prototype._refreshEditor = function() {
288 var that = this,
289 status = this.editorInstance.status;
290
291 // Wait for ready only once.
292 if ( this.waitForReady )
293 return;
294
295 // Not ready.
296 if ( status == 'unloaded' || status == 'loaded' ) {
297 this.waitForReady = true;
298
299 this.editorInstance.once( 'instanceReady', function() {
300 refresh();
301 }, this );
302 // Ready or destroyed.
303 } else {
304 refresh();
305 }
306
307 function refresh() {
308 that.editorInstance.destroy();
309 that._createEditor( true, that.getActualConfig() );
310 that.waitForReady = false;
311 }
312 };
313
314 /**
315 * Creates editor that can be used to present the toolbar configuration.
316 *
317 * @private
318 */
319 AbstractToolbarModifier.prototype._createEditor = function( doRefresh, configOverrides ) {
320 var that = this;
321
322 this.editorInstance = CKEDITOR.replace( this.editorId );
323
324 this.editorInstance.on( 'configLoaded', function() {
325 var config = that.editorInstance.config;
326
327 if ( configOverrides ) {
328 CKEDITOR.tools.extend( config, configOverrides, true );
329 }
330
331 AbstractToolbarModifier.extendPluginsConfig( config );
332 } );
333
334 // Prevent creating any other space than the top one.
335 this.editorInstance.on( 'uiSpace', function( evt ) {
336 if ( evt.data.space != 'top' ) {
337 evt.stop();
338 }
339 }, null, null, -999 );
340
341 this.editorInstance.once( 'loaded', function() {
342 var btns = that.editorInstance.ui.instances;
343
344 for ( var i in btns ) {
345 if ( btns[ i ] ) {
346 btns[ i ].click = empty;
347 btns[ i ].onClick = empty;
348 }
349 }
350
351 if ( !that.isEditableVisible ) {
352 that._hideEditable();
353 }
354
355 if ( that.currentActive && that.currentActive.name ) {
356 that._highlightGroup( that.currentActive.name );
357 }
358
359 if ( that.hidden ) {
360 that.hideUI();
361 } else {
362 that.showUI();
363 }
364
365 if ( doRefresh && ( typeof that.onRefresh === 'function' ) ) {
366 that.onRefresh();
367 }
368 } );
369
370 function empty() {}
371 };
372
373 /**
374 * Always returns copy of config.
375 *
376 * @returns {Object}
377 */
378 AbstractToolbarModifier.prototype.getActualConfig = function() {
379 return JSON.parse( JSON.stringify( this.actualConfig ) );
380 };
381
382 /**
383 * Creates toolbar in tool.
384 *
385 * @private
386 */
387 AbstractToolbarModifier.prototype._createToolbar = function() {
388 if ( !this.toolbarButtons.length ) {
389 return;
390 }
391
392 this.toolbarContainer = new CKEDITOR.dom.element( 'div' );
393 this.toolbarContainer.addClass( 'toolbar' );
394
395 var max = this.toolbarButtons.length;
396 for ( var i = 0; i < max; i += 1 ) {
397 this._createToolbarBtn( this.toolbarButtons[ i ] );
398 }
399 };
400
401 /**
402 * Create toolbar button and add it to toolbar container
403 *
404 * @param {Object} cfg
405 * @returns {CKEDITOR.dom.element}
406 * @private
407 */
408 AbstractToolbarModifier.prototype._createToolbarBtn = function( cfg ) {
409 var btnText = ( typeof cfg.text === 'string' ? cfg.text : cfg.text.inactive ),
410 btn = ToolbarConfigurator.FullToolbarEditor.createButton( btnText, cfg.cssClass );
411
412 this.toolbarContainer.append( btn );
413 btn.data( 'group', cfg.group );
414 btn.addClass( cfg.position );
415 btn.on( 'click', function() {
416 cfg.clickCallback.call( this, btn, cfg );
417 }, this );
418
419 return btn;
420 };
421
422 /**
423 * @private
424 * @param {Object} config
425 */
426 AbstractToolbarModifier.prototype._fixGroups = function( config ) {
427 var groups = config.toolbarGroups || [];
428
429 var max = groups.length;
430 for ( var i = 0; i < max; i += 1 ) {
431 var currentGroup = groups[ i ];
432
433 // separator, in config, is in raw format
434 // need to make it more sophisticated to keep unique id
435 // for each one
436 if ( currentGroup == '/' ) {
437 currentGroup = groups[ i ] = {};
438 currentGroup.type = 'separator';
439 currentGroup.name = ( 'separator' + CKEDITOR.tools.getNextNumber() );
440 continue;
441 }
442
443 // sometimes subgroups are not set (basic package), so need to
444 // create them artifically
445 currentGroup.groups = currentGroup.groups || [];
446
447 // when there is no subgroup with same name like its parent name
448 // then it have to be added artificially
449 // in order to maintain consistency between user interface and config
450 if ( CKEDITOR.tools.indexOf( currentGroup.groups, currentGroup.name ) == -1 ) {
451 this.editorInstance.ui.addToolbarGroup( currentGroup.name, currentGroup.groups[ currentGroup.groups.length - 1 ], currentGroup.name );
452 currentGroup.groups.push( currentGroup.name );
453 }
454
455 this._fixSubgroups( currentGroup );
456 }
457 };
458
459 /**
460 * Transform subgroup string to object literal
461 * with keys: {String} name and {Number} totalBtns
462 * Please note: this method modify Object provided in first argument
463 *
464 * input:
465 * [
466 * { groups: [ 'nameOne', 'nameTwo' ] }
467 * ]
468 *
469 * output:
470 * [
471 * { groups: [ { name: 'nameOne', totalBtns: 3 }, { name: 'nameTwo', totalBtns: 5 } ] }
472 * ]
473 *
474 * @param {Object} group
475 * @private
476 */
477 AbstractToolbarModifier.prototype._fixSubgroups = function( group ) {
478 var subGroups = group.groups;
479
480 var max = subGroups.length;
481 for ( var i = 0; i < max; i += 1 ) {
482 var subgroupName = subGroups[ i ];
483
484 subGroups[ i ] = {
485 name: subgroupName,
486 totalBtns: ToolbarConfigurator.ToolbarModifier.getTotalSubGroupButtonsNumber( subgroupName, this.fullToolbarEditor )
487 };
488 }
489 };
490
491 /**
492 * Same as JSON.stringify method but returned string is in one line
493 *
494 * @param {Object} json
495 * @param {Object} opts
496 * @param {Boolean} opts.addSpaces
497 * @param {Boolean} opts.noQuotesOnKey
498 * @param {Boolean} opts.singleQuotes
499 * @returns {Object}
500 */
501 AbstractToolbarModifier.stringifyJSONintoOneLine = function( json, opts ) {
502 opts = opts || {};
503 var stringJSON = JSON.stringify( json, null, '' );
504
505 // IE8 make new line characters
506 stringJSON = stringJSON.replace( /\n/g, '' );
507
508 if ( opts.addSpaces ) {
509 stringJSON = stringJSON.replace( /(\{|:|,|\[|\])/g, function( sentence ) {
510 return sentence + ' ';
511 } );
512
513 stringJSON = stringJSON.replace( /(\])/g, function( sentence ) {
514 return ' ' + sentence;
515 } );
516 }
517
518 if ( opts.noQuotesOnKey ) {
519 stringJSON = stringJSON.replace( /"(\w*)":/g, function( sentence, word ) {
520 return word + ':';
521 } );
522 }
523
524 if ( opts.singleQuotes ) {
525 stringJSON = stringJSON.replace( /\"/g, '\'' );
526 }
527
528 return stringJSON;
529 };
530
531 /**
532 * Hide toolbar configurator
533 */
534 AbstractToolbarModifier.prototype.hideUI = function() {
535 this.hidden = true;
536 this.mainContainer.hide();
537 if ( this.editorInstance.container ) {
538 this.editorInstance.container.hide();
539 }
540 };
541
542 /**
543 * Show toolbar configurator
544 */
545 AbstractToolbarModifier.prototype.showUI = function() {
546 this.hidden = false;
547 this.mainContainer.show();
548 if ( this.editorInstance.container ) {
549 this.editorInstance.container.show();
550 }
551 };
552
553
554 /**
555 * Extends plugins setttings in the specified config with settings useful for
556 * the toolbar configurator.
557 *
558 * @static
559 */
560 AbstractToolbarModifier.extendPluginsConfig = function( config ) {
561 var extraPlugins = config.extraPlugins;
562
563 // Enable the special, lightweight area to replace wysiwygarea.
564 config.extraPlugins = ( extraPlugins ? extraPlugins + ',' : '' ) + 'toolbarconfiguratorarea';
565 };
566} )();
diff --git a/sources/samples/toolbarconfigurator/js/fulltoolbareditor.js b/sources/samples/toolbarconfigurator/js/fulltoolbareditor.js
new file mode 100644
index 0000000..2b9f15f
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/js/fulltoolbareditor.js
@@ -0,0 +1,365 @@
1/* exported ToolbarConfigurator */
2/* global ToolbarConfigurator */
3
4'use strict';
5
6window.ToolbarConfigurator = {};
7
8( function() {
9 /**
10 * @class ToolbarConfigurator.FullToolbarEditor
11 * @constructor
12 */
13 function FullToolbarEditor() {
14 this.instanceid = 'fte' + CKEDITOR.tools.getNextId();
15 this.textarea = new CKEDITOR.dom.element( 'textarea' );
16 this.textarea.setAttributes( {
17 id: this.instanceid,
18 name: this.instanceid,
19 contentEditable: true
20 } );
21
22 this.buttons = null;
23 this.editorInstance = null;
24 }
25
26 // Expose the class.
27 ToolbarConfigurator.FullToolbarEditor = FullToolbarEditor;
28
29 /**
30 * @param {Function} callback
31 * @param {Object} cfg
32 */
33 FullToolbarEditor.prototype.init = function( callback ) {
34 var that = this;
35
36 document.body.appendChild( this.textarea.$ );
37
38 CKEDITOR.replace( this.instanceid );
39
40 this.editorInstance = CKEDITOR.instances[ this.instanceid ];
41
42 this.editorInstance.once( 'configLoaded', function( e ) {
43 var cfg = e.editor.config;
44
45 // We want all the buttons.
46 delete cfg.removeButtons;
47 delete cfg.toolbarGroups;
48 delete cfg.toolbar;
49 ToolbarConfigurator.AbstractToolbarModifier.extendPluginsConfig( cfg );
50
51 e.editor.once( 'loaded', function() {
52 that.buttons = FullToolbarEditor.toolbarToButtons( that.editorInstance.toolbar );
53
54 that.buttonsByGroup = FullToolbarEditor.groupButtons( that.buttons );
55
56 that.buttonNamesByGroup = that.groupButtonNamesByGroup( that.buttons );
57
58 e.editor.container.hide();
59
60 if ( typeof callback === 'function' )
61 callback( that.buttons );
62 } );
63 } );
64 };
65
66 /**
67 * Group array of button names by their group parents.
68 *
69 * @param {Array} buttons
70 * @returns {Object}
71 */
72 FullToolbarEditor.prototype.groupButtonNamesByGroup = function( buttons ) {
73 var that = this,
74 groups = FullToolbarEditor.groupButtons( buttons );
75
76 for ( var groupName in groups ) {
77 var currGroup = groups[ groupName ];
78
79 groups[ groupName ] = FullToolbarEditor.map( currGroup, function( button ) {
80 return that.getCamelCasedButtonName( button.name );
81 } );
82 }
83
84 return groups;
85 };
86
87 /**
88 * Returns group literal.
89 *
90 * @param {String} name
91 * @returns {Object}
92 */
93 FullToolbarEditor.prototype.getGroupByName = function( name ) {
94 var groups = this.editorInstance.config.toolbarGroups || this.getFullToolbarGroupsConfig();
95
96 var max = groups.length;
97 for ( var i = 0; i < max; i += 1 ) {
98 if ( groups[ i ].name === name )
99 return groups[ i ];
100 }
101
102 return null;
103 };
104
105 /**
106 * @param {String} name
107 * @returns {String | null}
108 */
109 FullToolbarEditor.prototype.getCamelCasedButtonName = function( name ) {
110 var items = this.editorInstance.ui.items;
111
112 for ( var key in items ) {
113 if ( items[ key ].name == name )
114 return key;
115 }
116
117 return null;
118 };
119
120 /**
121 * Returns full toolbarGroups config value which is used when
122 * there is no toolbarGroups field in config.
123 *
124 * @param {Boolean} [pickSeparators=false]
125 * @returns {Array}
126 */
127 FullToolbarEditor.prototype.getFullToolbarGroupsConfig = function( pickSeparators ) {
128 pickSeparators = ( pickSeparators === true ? true : false );
129
130 var result = [],
131 toolbarGroups = this.editorInstance.toolbar;
132
133 var max = toolbarGroups.length;
134 for ( var i = 0; i < max; i += 1 ) {
135 var currentGroup = toolbarGroups[ i ],
136 copiedGroup = {};
137
138 if ( typeof currentGroup.name != 'string' ) {
139 // this is not a group
140 if ( pickSeparators ) {
141 result.push( '/' );
142 }
143 continue;
144 }
145
146 copiedGroup.name = currentGroup.name;
147 if ( currentGroup.groups )
148 copiedGroup.groups = Array.prototype.slice.call( currentGroup.groups );
149
150 result.push( copiedGroup );
151 }
152
153 return result;
154 };
155
156 /**
157 * Filters array items based on checker provided in second argument.
158 * Returns new array.
159 *
160 * @param {Array} arr
161 * @param {Function} checker
162 * @returns {Array}
163 */
164 FullToolbarEditor.filter = function( arr, checker ) {
165 var max = ( arr && arr.length ? arr.length : 0 ),
166 result = [];
167
168 for ( var i = 0; i < max; i += 1 ) {
169 if ( checker( arr[ i ] ) )
170 result.push( arr[ i ] );
171 }
172
173 return result;
174 };
175
176 /**
177 * Simplified http://underscorejs.org/#map functionality
178 *
179 * @param {Array | Object} enumerable
180 * @param {Function} modifier
181 * @returns {Array | Object}
182 */
183 FullToolbarEditor.map = function( enumerable, modifier ) {
184 var result;
185
186 if ( CKEDITOR.tools.isArray( enumerable ) ) {
187 result = [];
188
189 var max = enumerable.length;
190 for ( var i = 0; i < max; i += 1 )
191 result.push( modifier( enumerable[ i ] ) );
192 } else {
193 result = {};
194
195 for ( var key in enumerable )
196 result[ key ] = modifier( enumerable[ key ] );
197 }
198
199 return result;
200 };
201
202 /**
203 * Group buttons by their parent names.
204 *
205 * @static
206 * @param {Array} buttons
207 * @returns {Object} The object (`name => group`) representing CKEDITOR.ui.button or CKEDITOR.ui.richCombo
208 */
209 FullToolbarEditor.groupButtons = function( buttons ) {
210 var groups = {};
211
212 var max = buttons.length;
213 for ( var i = 0; i < max; i += 1 ) {
214 var currBtn = buttons[ i ],
215 currBtnGroupName = currBtn.toolbar.split( ',' )[ 0 ];
216
217 groups[ currBtnGroupName ] = groups[ currBtnGroupName ] || [];
218
219 groups[ currBtnGroupName ].push( currBtn );
220 }
221
222 return groups;
223 };
224
225 /**
226 * Pick all buttons from toolbar.
227 *
228 * @static
229 * @param {Array} groups
230 * @returns {Array}
231 */
232 FullToolbarEditor.toolbarToButtons = function( groups ) {
233 var buttons = [];
234
235 var max = groups.length;
236 for ( var i = 0; i < max; i += 1 ) {
237 var currentGroup = groups[ i ];
238
239 if ( typeof currentGroup == 'object' )
240 buttons = buttons.concat( FullToolbarEditor.groupToButtons( groups[ i ] ) );
241 }
242
243 return buttons;
244 };
245
246 /**
247 * Creates HTML button representation for view.
248 *
249 * @static
250 * @param {CKEDITOR.ui.button | CKEDITOR.ui.richCombo} button
251 * @returns {CKEDITOR.dom.element}
252 */
253 FullToolbarEditor.createToolbarButton = function( button ) {
254 var $button = new CKEDITOR.dom.element( 'a' ),
255 icon = FullToolbarEditor.createIcon( button.name, button.icon, button.command );
256
257 $button.setStyle( 'float', 'none' );
258
259 $button.addClass( 'cke_' + ( CKEDITOR.lang.dir == 'rtl' ? 'rtl' : 'ltr' ) );
260
261 if ( button instanceof CKEDITOR.ui.button ) {
262 $button.addClass( 'cke_button' );
263 $button.addClass( 'cke_toolgroup' );
264
265 $button.append( icon );
266 } else if ( CKEDITOR.ui.richCombo && button instanceof CKEDITOR.ui.richCombo ) {
267 var comboLabel = new CKEDITOR.dom.element( 'span' ),
268 comboOpen = new CKEDITOR.dom.element( 'span' ),
269 comboArrow = new CKEDITOR.dom.element( 'span' );
270
271 $button.addClass( 'cke_combo_button' );
272
273 comboLabel.addClass( 'cke_combo_text' );
274 comboLabel.addClass( 'cke_combo_inlinelabel' );
275 comboLabel.setText( button.label );
276
277 comboOpen.addClass( 'cke_combo_open' );
278 comboArrow.addClass( 'cke_combo_arrow' );
279 comboOpen.append( comboArrow );
280
281 $button.append( comboLabel );
282 $button.append( comboOpen );
283 }
284
285 return $button;
286 };
287
288 /**
289 * Create and return icon element.
290 *
291 * @param {String} name
292 * @param {String} icon
293 * @param {String} command
294 * @static
295 * @returns {CKEDITOR.dom.element}
296 */
297 FullToolbarEditor.createIcon = function( name, icon, command ) {
298 var iconStyle = CKEDITOR.skin.getIconStyle( name, ( CKEDITOR.lang.dir == 'rtl' ) );
299
300 // We don't know exactly how to get icon style. Especially for extra plugins,
301 // Which definition may vary.
302 iconStyle = iconStyle || CKEDITOR.skin.getIconStyle( icon, ( CKEDITOR.lang.dir == 'rtl' ) );
303 iconStyle = iconStyle || CKEDITOR.skin.getIconStyle( command, ( CKEDITOR.lang.dir == 'rtl' ) );
304
305 var iconElement = new CKEDITOR.dom.element( 'span' );
306
307 iconElement.addClass( 'cke_button_icon' );
308 iconElement.addClass( 'cke_button__' + name + '_icon' );
309 iconElement.setAttribute( 'style', iconStyle );
310 iconElement.setStyle( 'float', 'none' );
311
312 return iconElement;
313 };
314
315 /**
316 * Create and return button element
317 *
318 * @param {String} text
319 * @param {String} cssClasses
320 * @returns {CKEDITOR.dom.element}
321 */
322 FullToolbarEditor.createButton = function( text, cssClasses ) {
323 var $button = new CKEDITOR.dom.element( 'button' );
324
325 $button.addClass( 'button-a' );
326
327 $button.setAttribute( 'type', 'button' );
328
329 if ( typeof cssClasses == 'string' ) {
330 cssClasses = cssClasses.split( ' ' );
331
332 var i = cssClasses.length;
333 while ( i-- ) {
334 $button.addClass( cssClasses[ i ] );
335 }
336 }
337
338 $button.setHtml( text );
339
340 return $button;
341 };
342
343 /**
344 * @static
345 * @param {Object} group
346 * @returns {Array} representing HTML buttons for view
347 */
348 FullToolbarEditor.groupToButtons = function( group ) {
349 var buttons = [],
350 items = group.items;
351
352 var max = items ? items.length : 0;
353 for ( var i = 0; i < max; i += 1 ) {
354 var item = items[ i ];
355
356 if ( item instanceof CKEDITOR.ui.button || CKEDITOR.ui.richCombo && item instanceof CKEDITOR.ui.richCombo ) {
357 item.$ = FullToolbarEditor.createToolbarButton( item );
358 buttons.push( item );
359 }
360 }
361
362 return buttons;
363 };
364
365} )();
diff --git a/sources/samples/toolbarconfigurator/js/toolbarmodifier.js b/sources/samples/toolbarconfigurator/js/toolbarmodifier.js
new file mode 100644
index 0000000..bd33d24
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/js/toolbarmodifier.js
@@ -0,0 +1,1366 @@
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
diff --git a/sources/samples/toolbarconfigurator/js/toolbartextmodifier.js b/sources/samples/toolbarconfigurator/js/toolbartextmodifier.js
new file mode 100644
index 0000000..4c14dd2
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/js/toolbartextmodifier.js
@@ -0,0 +1,623 @@
1/* global CodeMirror, ToolbarConfigurator */
2
3'use strict';
4
5( function() {
6 var AbstractToolbarModifier = ToolbarConfigurator.AbstractToolbarModifier,
7 FullToolbarEditor = ToolbarConfigurator.FullToolbarEditor;
8
9 /**
10 * @class ToolbarConfigurator.ToolbarTextModifier
11 * @param {String} editorId An id of modified editor
12 * @extends AbstractToolbarModifier
13 * @constructor
14 */
15 function ToolbarTextModifier( editorId ) {
16 AbstractToolbarModifier.call( this, editorId );
17
18 this.codeContainer = null;
19 this.hintContainer = null;
20 }
21
22 // Expose the class.
23 ToolbarConfigurator.ToolbarTextModifier = ToolbarTextModifier;
24
25 ToolbarTextModifier.prototype = Object.create( AbstractToolbarModifier.prototype );
26
27 /**
28 * @param {Function} callback
29 * @param {String} [config]
30 * @private
31 */
32 ToolbarTextModifier.prototype._onInit = function( callback, config ) {
33 AbstractToolbarModifier.prototype._onInit.call( this, undefined, config );
34
35 this._createModifier( config ? this.actualConfig : undefined );
36
37 if ( typeof callback === 'function' )
38 callback( this.mainContainer );
39 };
40
41 /**
42 * Creates HTML main container of modifier.
43 *
44 * @param {String} cfg
45 * @returns {CKEDITOR.dom.element}
46 * @private
47 */
48 ToolbarTextModifier.prototype._createModifier = function( cfg ) {
49 var that = this;
50
51 this._createToolbar();
52
53 if ( this.toolbarContainer ) {
54 this.mainContainer.append( this.toolbarContainer );
55 }
56
57 AbstractToolbarModifier.prototype._createModifier.call( this );
58
59 this._setupActualConfig( cfg );
60
61 var toolbarCfg = this.actualConfig.toolbar,
62 cfgValue;
63
64 if ( CKEDITOR.tools.isArray( toolbarCfg ) ) {
65 var stringifiedToolbar = '[\n\t\t' + FullToolbarEditor.map( toolbarCfg, function( json ) {
66 return AbstractToolbarModifier.stringifyJSONintoOneLine( json, {
67 addSpaces: true,
68 noQuotesOnKey: true,
69 singleQuotes: true
70 } );
71 } ).join( ',\n\t\t' ) + '\n\t]';
72
73 cfgValue = '\tconfig.toolbar = ' + stringifiedToolbar + ';';
74 } else {
75 cfgValue = 'config.toolbar = [];';
76 }
77
78 cfgValue = [
79 'CKEDITOR.editorConfig = function( config ) {\n',
80 cfgValue,
81 '\n};'
82 ].join( '' );
83
84 function hint( cm ) {
85 var data = setupData( cm );
86
87 if ( data.charsBetween === null ) {
88 return;
89 }
90
91 var unused = that.getUnusedButtonsArray( that.actualConfig.toolbar, true, data.charsBetween ),
92 to = cm.getCursor(),
93 from = CodeMirror.Pos( to.line, ( to.ch - ( data.charsBetween.length ) ) ),
94 token = cm.getTokenAt( to ),
95 prevToken = cm.getTokenAt( { line: to.line, ch: token.start } );
96
97 // determine that we are at beginning of group,
98 // so first key is "name"
99 if ( prevToken.string === '{' )
100 unused = [ 'name' ];
101
102 // preventing close with special character and move cursor forward
103 // when no autocomplete
104 if ( unused.length === 0 )
105 return;
106
107 return new HintData( from, to, unused );
108 }
109
110 function HintData( from, to, list ) {
111 this.from = from;
112 this.to = to;
113 this.list = list;
114 this._handlers = [];
115 }
116
117 function setupData( cm, character ) {
118 var result = {};
119
120 result.cur = cm.getCursor();
121 result.tok = cm.getTokenAt( result.cur );
122
123 result[ 'char' ] = character || result.tok.string.charAt( result.tok.string.length - 1 );
124
125 // Getting string between begin of line and cursor.
126 var curLineTillCur = cm.getRange( CodeMirror.Pos( result.cur.line, 0 ), result.cur );
127
128 // Reverse string.
129 var currLineTillCurReversed = curLineTillCur.split( '' ).reverse().join( '' );
130
131 // Removing proper string definitions :
132 // FROM:
133 // R' ,'odeR' ,'odnU' [ :smeti{
134 // ^^^^^^ ^^^^^^
135 // TO:
136 // R' , [ :smeti{
137 currLineTillCurReversed = currLineTillCurReversed.replace( /(['|"]\w*['|"])/g, '' );
138
139 // Matching letters till ' or " character and end string char.
140 // R' , [ :smeti{
141 // ^
142 result.charsBetween = currLineTillCurReversed.match( /(^\w*)(['|"])/ );
143
144 if ( result.charsBetween ) {
145 result.endChar = result.charsBetween[ 2 ];
146
147 // And reverse string (bring to original state).
148 result.charsBetween = result.charsBetween[ 1 ].split( '' ).reverse().join( '' );
149 }
150
151 return result;
152 }
153
154 function complete( cm ) {
155 setTimeout( function() {
156 if ( !cm.state.completionActive ) {
157 CodeMirror.showHint( cm, hint, {
158 hintsClass: 'toolbar-modifier',
159 completeSingle: false
160 } );
161 }
162 }, 100 );
163
164 return CodeMirror.Pass;
165 }
166
167 var codeMirrorWrapper = new CKEDITOR.dom.element( 'div' );
168 codeMirrorWrapper.addClass( 'codemirror-wrapper' );
169 this.modifyContainer.append( codeMirrorWrapper );
170 this.codeContainer = CodeMirror( codeMirrorWrapper.$, {
171 mode: { name: 'javascript', json: true },
172 // For some reason (most likely CM's bug) gutter breaks CM's height.
173 // Refreshing CM does not help.
174 lineNumbers: false,
175 lineWrapping: true,
176 // Trick to make CM autogrow. http://codemirror.net/demo/resize.html
177 viewportMargin: Infinity,
178 value: cfgValue,
179 smartIndent: false,
180 indentWithTabs: true,
181 indentUnit: 4,
182 tabSize: 4,
183 theme: 'neo',
184 extraKeys: {
185 'Left': complete,
186 'Right': complete,
187 "'''": complete,
188 "'\"'": complete,
189 Backspace: complete,
190 Delete: complete,
191 'Shift-Tab': 'indentLess'
192 }
193 } );
194
195 this.codeContainer.on( 'endCompletion', function( cm, completionData ) {
196 var data = setupData( cm );
197
198 // preventing close with special character and move cursor forward
199 // when no autocomplete
200 if ( completionData === undefined )
201 return;
202
203 cm.replaceSelection( data.endChar );
204 } );
205
206 this.codeContainer.on( 'change', function() {
207 var value = that.codeContainer.getValue();
208
209 value = that._evaluateValue( value );
210
211 if ( value !== null ) {
212 that.actualConfig.toolbar = ( value.toolbar ? value.toolbar : that.actualConfig.toolbar );
213
214 that._fillHintByUnusedElements();
215 that._refreshEditor();
216
217 that.mainContainer.removeClass( 'invalid' );
218 } else {
219 that.mainContainer.addClass( 'invalid' );
220 }
221 } );
222
223 this.hintContainer = new CKEDITOR.dom.element( 'div' );
224 this.hintContainer.addClass( 'toolbarModifier-hints' );
225
226 this._fillHintByUnusedElements();
227 this.hintContainer.insertBefore( codeMirrorWrapper );
228 };
229
230 /**
231 * Create DOM string and set to hint container,
232 * show proper information when no unused element left.
233 *
234 * @private
235 */
236 ToolbarTextModifier.prototype._fillHintByUnusedElements = function() {
237 var unused = this.getUnusedButtonsArray( this.actualConfig.toolbar, true );
238 unused = this.groupButtonNamesByGroup( unused );
239
240 var unusedElements = FullToolbarEditor.map( unused, function( elem ) {
241 var buttonsList = FullToolbarEditor.map( elem.buttons, function( buttonName ) {
242 return '<code>' + buttonName + '</code> ';
243 } ).join( '' );
244
245 return [
246 '<dt>',
247 '<code>', elem.name, '</code>',
248 '</dt>',
249 '<dd>',
250 buttonsList,
251 '</dd>'
252 ].join( '' );
253 } ).join( ' ' );
254
255 var listHeader = [
256 '<dt class="list-header">Toolbar group</dt>',
257 '<dd class="list-header">Unused items</dd>'
258 ].join( '' );
259
260 var header = '<h3>Unused toolbar items</h3>';
261
262 if ( !unused.length ) {
263 listHeader = '<p>All items are in use.</p>';
264 }
265
266 this.codeContainer.refresh();
267
268 this.hintContainer.setHtml( header + '<dl>' + listHeader + unusedElements + '</dl>' );
269 };
270
271 /**
272 * @param {String} buttonName
273 * @returns {String}
274 */
275 ToolbarTextModifier.prototype.getToolbarGroupByButtonName = function( buttonName ) {
276 var buttonNames = this.fullToolbarEditor.buttonNamesByGroup;
277
278 for ( var groupName in buttonNames ) {
279 var buttons = buttonNames[ groupName ];
280
281 var i = buttons.length;
282 while ( i-- ) {
283 if ( buttonName === buttons[ i ] ) {
284 return groupName;
285 }
286 }
287
288 }
289
290 return null;
291 };
292
293 /**
294 * Filter all available toolbar elements by array of elements provided in first argument.
295 * Returns elements which are not used.
296 *
297 * @param {Object} toolbar
298 * @param {Boolean} [sorted=false]
299 * @param {String} prefix
300 * @returns {Array}
301 */
302 ToolbarTextModifier.prototype.getUnusedButtonsArray = function( toolbar, sorted, prefix ) {
303 sorted = ( sorted === true ? true : false );
304 var providedElements = ToolbarTextModifier.mapToolbarCfgToElementsList( toolbar ),
305 allElements = Object.keys( this.fullToolbarEditor.editorInstance.ui.items );
306
307 // get rid of "-" elements
308 allElements = FullToolbarEditor.filter( allElements, function( elem ) {
309 var isSeparator = ( elem === '-' ),
310 matchPrefix = ( prefix === undefined || elem.toLowerCase().indexOf( prefix.toLowerCase() ) === 0 );
311
312 return !isSeparator && matchPrefix;
313 } );
314
315 var elementsNotUsed = FullToolbarEditor.filter( allElements, function( elem ) {
316 return CKEDITOR.tools.indexOf( providedElements, elem ) == -1;
317 } );
318
319 if ( sorted )
320 elementsNotUsed.sort();
321
322 return elementsNotUsed;
323 };
324
325 /**
326 *
327 * @param {Array} buttons
328 * @returns {Array}
329 */
330 ToolbarTextModifier.prototype.groupButtonNamesByGroup = function( buttons ) {
331 var result = [],
332 groupedBtns = JSON.parse( JSON.stringify( this.fullToolbarEditor.buttonNamesByGroup ) );
333
334 for ( var groupName in groupedBtns ) {
335 var currGroup = groupedBtns[ groupName ];
336 currGroup = FullToolbarEditor.filter( currGroup, function( btnName ) {
337 return CKEDITOR.tools.indexOf( buttons, btnName ) !== -1;
338 } );
339
340 if ( currGroup.length ) {
341 result.push( {
342 name: groupName,
343 buttons: currGroup
344 } );
345 }
346
347 }
348
349 return result;
350 };
351
352 /**
353 * Map toolbar config value to flat items list.
354 *
355 * input:
356 * [
357 * { name: "basicstyles", items: ["Bold", "Italic"] },
358 * { name: "advancedstyles", items: ["Bold", "Outdent", "Indent"] }
359 * ]
360 *
361 * output:
362 * ["Bold", "Italic", "Outdent", "Indent"]
363 *
364 * @param {Object} toolbar
365 * @returns {Array}
366 */
367 ToolbarTextModifier.mapToolbarCfgToElementsList = function( toolbar ) {
368 var elements = [];
369
370 var max = toolbar.length;
371 for ( var i = 0; i < max; i += 1 ) {
372 if ( !toolbar[ i ] || typeof toolbar[ i ] === 'string' )
373 continue;
374
375 elements = elements.concat( FullToolbarEditor.filter( toolbar[ i ].items, checker ) );
376 }
377
378 function checker( elem ) {
379 return elem !== '-';
380 }
381
382 return elements;
383 };
384
385 /**
386 * @param {String} cfg
387 * @private
388 */
389 ToolbarTextModifier.prototype._setupActualConfig = function( cfg ) {
390 cfg = cfg || this.editorInstance.config;
391
392 // if toolbar already exists in config, there is nothing to do
393 if ( CKEDITOR.tools.isArray( cfg.toolbar ) )
394 return;
395
396 // if toolbar group not present, we need to pick them from full toolbar instance
397 if ( !cfg.toolbarGroups )
398 cfg.toolbarGroups = this.fullToolbarEditor.getFullToolbarGroupsConfig( true );
399
400 this._fixGroups( cfg );
401
402 cfg.toolbar = this._mapToolbarGroupsToToolbar( cfg.toolbarGroups, this.actualConfig.removeButtons );
403
404 this.actualConfig.toolbar = cfg.toolbar;
405 this.actualConfig.removeButtons = '';
406 };
407
408 /**
409 * **Please note:** This method modify element provided in first argument.
410 *
411 * @param {Array} toolbarGroups
412 * @returns {Array}
413 * @private
414 */
415 ToolbarTextModifier.prototype._mapToolbarGroupsToToolbar = function( toolbarGroups, removedBtns ) {
416 removedBtns = removedBtns || this.editorInstance.config.removedBtns;
417 removedBtns = typeof removedBtns == 'string' ? removedBtns.split( ',' ) : [];
418
419 // from the end, because array indexes may change
420 var i = toolbarGroups.length;
421 while ( i-- ) {
422 var mappedSubgroup = this._mapToolbarSubgroup( toolbarGroups[ i ], removedBtns );
423
424 if ( toolbarGroups[ i ].type === 'separator' ) {
425 toolbarGroups[ i ] = '/';
426 continue;
427 }
428
429 // don't want empty groups
430 if ( CKEDITOR.tools.isArray( mappedSubgroup ) && mappedSubgroup.length === 0 ) {
431 toolbarGroups.splice( i, 1 );
432 continue;
433 }
434
435 if ( typeof mappedSubgroup == 'string' )
436 toolbarGroups[ i ] = mappedSubgroup;
437 else {
438 toolbarGroups[ i ] = {
439 name: toolbarGroups[ i ].name,
440 items: mappedSubgroup
441 };
442 }
443 }
444
445 return toolbarGroups;
446 };
447
448 /**
449 *
450 * @param {String|Object} group
451 * @param {Array} removedBtns
452 * @returns {Array}
453 * @private
454 */
455 ToolbarTextModifier.prototype._mapToolbarSubgroup = function( group, removedBtns ) {
456 var totalBtns = 0;
457 if ( typeof group == 'string' )
458 return group;
459
460 var max = group.groups ? group.groups.length : 0,
461 result = [];
462 for ( var i = 0; i < max; i += 1 ) {
463 var currSubgroup = group.groups[ i ];
464
465 var buttons = this.fullToolbarEditor.buttonsByGroup[ typeof currSubgroup === 'string' ? currSubgroup : currSubgroup.name ] || [];
466 buttons = this._mapButtonsToButtonsNames( buttons, removedBtns );
467 var currTotalBtns = buttons.length;
468 totalBtns += currTotalBtns;
469 result = result.concat( buttons );
470
471 if ( currTotalBtns )
472 result.push( '-' );
473 }
474
475 if ( result[ result.length - 1 ] == '-' )
476 result.pop();
477
478 return result;
479 };
480
481 /**
482 *
483 * @param {Array} buttons
484 * @param {Array} removedBtns
485 * @returns {Array}
486 * @private
487 */
488 ToolbarTextModifier.prototype._mapButtonsToButtonsNames = function( buttons, removedBtns ) {
489 var i = buttons.length;
490 while ( i-- ) {
491 var currBtn = buttons[ i ],
492 camelCasedName;
493
494 if ( typeof currBtn === 'string' ) {
495 camelCasedName = currBtn;
496 } else {
497 camelCasedName = this.fullToolbarEditor.getCamelCasedButtonName( currBtn.name );
498 }
499
500 if ( CKEDITOR.tools.indexOf( removedBtns, camelCasedName ) !== -1 ) {
501 buttons.splice( i, 1 );
502 continue;
503 }
504
505 buttons[ i ] = camelCasedName;
506 }
507
508 return buttons;
509 };
510
511 /**
512 * @param {String} val
513 * @returns {Object}
514 * @private
515 */
516 ToolbarTextModifier.prototype._evaluateValue = function( val ) {
517 var parsed;
518
519 try {
520 var config = {};
521 ( function() {
522 var CKEDITOR = Function( 'var CKEDITOR = {}; ' + val + '; return CKEDITOR;' )();
523
524 CKEDITOR.editorConfig( config );
525 parsed = config;
526 } )();
527
528 // CKEditor does not handle empty arrays in configuration files
529 // on IE8
530 var i = parsed.toolbar.length;
531 while ( i-- )
532 if ( !parsed.toolbar[ i ] ) parsed.toolbar.splice( i, 1 );
533
534 } catch ( e ) {
535 parsed = null;
536 }
537
538 return parsed;
539 };
540
541 /**
542 * @param {Array} toolbar
543 * @returns {{toolbarGroups: Array, removeButtons: string}}
544 */
545 ToolbarTextModifier.prototype.mapToolbarToToolbarGroups = function( toolbar ) {
546 var usedGroups = {},
547 removeButtons = [],
548 toolbarGroups = [];
549
550 var max = toolbar.length;
551 for ( var i = 0; i < max; i++ ) {
552 if ( toolbar[ i ] === '/' ) {
553 toolbarGroups.push( '/' );
554 continue;
555 }
556
557 var items = toolbar[ i ].items;
558
559 var toolbarGroup = {};
560 toolbarGroup.name = toolbar[ i ].name;
561 toolbarGroup.groups = [];
562
563 var max2 = items.length;
564 for ( var j = 0; j < max2; j++ ) {
565 var item = items[ j ];
566
567 if ( item === '-' ) {
568 continue;
569 }
570
571 var groupName = this.getToolbarGroupByButtonName( item );
572
573 var groupIndex = toolbarGroup.groups.indexOf( groupName );
574 if ( groupIndex === -1 ) {
575 toolbarGroup.groups.push( groupName );
576 }
577
578 usedGroups[ groupName ] = usedGroups[ groupName ] || {};
579
580 var buttons = ( usedGroups[ groupName ].buttons = usedGroups[ groupName ].buttons || {} );
581
582 buttons[ item ] = buttons[ item ] || { used: 0, origin: toolbarGroup.name };
583 buttons[ item ].used++;
584 }
585
586 toolbarGroups.push( toolbarGroup );
587 }
588
589 // Handling removed buttons
590 removeButtons = prepareRemovedButtons( usedGroups, this.fullToolbarEditor.buttonNamesByGroup );
591
592 function prepareRemovedButtons( usedGroups, buttonNames ) {
593 var removed = [];
594
595 for ( var groupName in usedGroups ) {
596 var group = usedGroups[ groupName ];
597 var allButtonsInGroup = buttonNames[ groupName ].slice();
598
599 removed = removed.concat( removeStuffFromArray( allButtonsInGroup, Object.keys( group.buttons ) ) );
600 }
601
602 return removed;
603 }
604
605 function removeStuffFromArray( array, stuff ) {
606 array = array.slice();
607 var i = stuff.length;
608
609 while ( i-- ) {
610 var atIndex = array.indexOf( stuff[ i ] );
611 if ( atIndex !== -1 ) {
612 array.splice( atIndex, 1 );
613 }
614 }
615
616 return array;
617 }
618
619 return { toolbarGroups: toolbarGroups, removeButtons: removeButtons.join( ',' ) };
620 };
621
622 return ToolbarTextModifier;
623} )();