]> git.immae.eu Git - perso/Immae/Projets/packagist/ludivine-ckeditor-component.git/blame - sources/samples/toolbarconfigurator/js/toolbartextmodifier.js
Validation initiale
[perso/Immae/Projets/packagist/ludivine-ckeditor-component.git] / sources / samples / toolbarconfigurator / js / toolbartextmodifier.js
CommitLineData
c63493c8
IB
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} )();