diff options
Diffstat (limited to 'sources/samples/toolbarconfigurator/js')
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 | ||
6 | if ( 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. | ||
26 | CKEDITOR.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 | ||
64 | if ( !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 | |||
6 | window.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 | } )(); | ||