toolbartextmodifier.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  1. /* global CodeMirror, ToolbarConfigurator */
  2. 'use strict';
  3. ( function() {
  4. var AbstractToolbarModifier = ToolbarConfigurator.AbstractToolbarModifier,
  5. FullToolbarEditor = ToolbarConfigurator.FullToolbarEditor;
  6. /**
  7. * @class ToolbarConfigurator.ToolbarTextModifier
  8. * @param {String} editorId An id of modified editor
  9. * @extends AbstractToolbarModifier
  10. * @constructor
  11. */
  12. function ToolbarTextModifier( editorId ) {
  13. AbstractToolbarModifier.call( this, editorId );
  14. this.codeContainer = null;
  15. this.hintContainer = null;
  16. }
  17. // Expose the class.
  18. ToolbarConfigurator.ToolbarTextModifier = ToolbarTextModifier;
  19. ToolbarTextModifier.prototype = Object.create( AbstractToolbarModifier.prototype );
  20. /**
  21. * @param {Function} callback
  22. * @param {String} [config]
  23. * @private
  24. */
  25. ToolbarTextModifier.prototype._onInit = function( callback, config ) {
  26. AbstractToolbarModifier.prototype._onInit.call( this, undefined, config );
  27. this._createModifier( config ? this.actualConfig : undefined );
  28. if ( typeof callback === 'function' )
  29. callback( this.mainContainer );
  30. };
  31. /**
  32. * Creates HTML main container of modifier.
  33. *
  34. * @param {String} cfg
  35. * @returns {CKEDITOR.dom.element}
  36. * @private
  37. */
  38. ToolbarTextModifier.prototype._createModifier = function( cfg ) {
  39. var that = this;
  40. this._createToolbar();
  41. if ( this.toolbarContainer ) {
  42. this.mainContainer.append( this.toolbarContainer );
  43. }
  44. AbstractToolbarModifier.prototype._createModifier.call( this );
  45. this._setupActualConfig( cfg );
  46. var toolbarCfg = this.actualConfig.toolbar,
  47. cfgValue;
  48. if ( CKEDITOR.tools.isArray( toolbarCfg ) ) {
  49. var stringifiedToolbar = '[\n\t\t' + FullToolbarEditor.map( toolbarCfg, function( json ) {
  50. return AbstractToolbarModifier.stringifyJSONintoOneLine( json, {
  51. addSpaces: true,
  52. noQuotesOnKey: true,
  53. singleQuotes: true
  54. } );
  55. } ).join( ',\n\t\t' ) + '\n\t]';
  56. cfgValue = '\tconfig.toolbar = ' + stringifiedToolbar + ';';
  57. } else {
  58. cfgValue = 'config.toolbar = [];';
  59. }
  60. cfgValue = [
  61. 'CKEDITOR.editorConfig = function( config ) {\n',
  62. cfgValue,
  63. '\n};'
  64. ].join( '' );
  65. function hint( cm ) {
  66. var data = setupData( cm );
  67. if ( data.charsBetween === null ) {
  68. return;
  69. }
  70. var unused = that.getUnusedButtonsArray( that.actualConfig.toolbar, true, data.charsBetween ),
  71. to = cm.getCursor(),
  72. from = CodeMirror.Pos( to.line, ( to.ch - ( data.charsBetween.length ) ) ),
  73. token = cm.getTokenAt( to ),
  74. prevToken = cm.getTokenAt( { line: to.line, ch: token.start } );
  75. // determine that we are at beginning of group,
  76. // so first key is "name"
  77. if ( prevToken.string === '{' )
  78. unused = [ 'name' ];
  79. // preventing close with special character and move cursor forward
  80. // when no autocomplete
  81. if ( unused.length === 0 )
  82. return;
  83. return new HintData( from, to, unused );
  84. }
  85. function HintData( from, to, list ) {
  86. this.from = from;
  87. this.to = to;
  88. this.list = list;
  89. this._handlers = [];
  90. }
  91. function setupData( cm, character ) {
  92. var result = {};
  93. result.cur = cm.getCursor();
  94. result.tok = cm.getTokenAt( result.cur );
  95. result[ 'char' ] = character || result.tok.string.charAt( result.tok.string.length - 1 );
  96. // Getting string between begin of line and cursor.
  97. var curLineTillCur = cm.getRange( CodeMirror.Pos( result.cur.line, 0 ), result.cur );
  98. // Reverse string.
  99. var currLineTillCurReversed = curLineTillCur.split( '' ).reverse().join( '' );
  100. // Removing proper string definitions :
  101. // FROM:
  102. // R' ,'odeR' ,'odnU' [ :smeti{
  103. // ^^^^^^ ^^^^^^
  104. // TO:
  105. // R' , [ :smeti{
  106. currLineTillCurReversed = currLineTillCurReversed.replace( /(['|"]\w*['|"])/g, '' );
  107. // Matching letters till ' or " character and end string char.
  108. // R' , [ :smeti{
  109. // ^
  110. result.charsBetween = currLineTillCurReversed.match( /(^\w*)(['|"])/ );
  111. if ( result.charsBetween ) {
  112. result.endChar = result.charsBetween[ 2 ];
  113. // And reverse string (bring to original state).
  114. result.charsBetween = result.charsBetween[ 1 ].split( '' ).reverse().join( '' );
  115. }
  116. return result;
  117. }
  118. function complete( cm ) {
  119. setTimeout( function() {
  120. if ( !cm.state.completionActive ) {
  121. CodeMirror.showHint( cm, hint, {
  122. hintsClass: 'toolbar-modifier',
  123. completeSingle: false
  124. } );
  125. }
  126. }, 100 );
  127. return CodeMirror.Pass;
  128. }
  129. var codeMirrorWrapper = new CKEDITOR.dom.element( 'div' );
  130. codeMirrorWrapper.addClass( 'codemirror-wrapper' );
  131. this.modifyContainer.append( codeMirrorWrapper );
  132. this.codeContainer = CodeMirror( codeMirrorWrapper.$, {
  133. mode: { name: 'javascript', json: true },
  134. // For some reason (most likely CM's bug) gutter breaks CM's height.
  135. // Refreshing CM does not help.
  136. lineNumbers: false,
  137. lineWrapping: true,
  138. // Trick to make CM autogrow. http://codemirror.net/demo/resize.html
  139. viewportMargin: Infinity,
  140. value: cfgValue,
  141. smartIndent: false,
  142. indentWithTabs: true,
  143. indentUnit: 4,
  144. tabSize: 4,
  145. theme: 'neo',
  146. extraKeys: {
  147. 'Left': complete,
  148. 'Right': complete,
  149. "'''": complete,
  150. "'\"'": complete,
  151. Backspace: complete,
  152. Delete: complete,
  153. 'Shift-Tab': 'indentLess'
  154. }
  155. } );
  156. this.codeContainer.on( 'endCompletion', function( cm, completionData ) {
  157. var data = setupData( cm );
  158. // preventing close with special character and move cursor forward
  159. // when no autocomplete
  160. if ( completionData === undefined )
  161. return;
  162. cm.replaceSelection( data.endChar );
  163. } );
  164. this.codeContainer.on( 'change', function() {
  165. var value = that.codeContainer.getValue();
  166. value = that._evaluateValue( value );
  167. if ( value !== null ) {
  168. that.actualConfig.toolbar = ( value.toolbar ? value.toolbar : that.actualConfig.toolbar );
  169. that._fillHintByUnusedElements();
  170. that._refreshEditor();
  171. that.mainContainer.removeClass( 'invalid' );
  172. } else {
  173. that.mainContainer.addClass( 'invalid' );
  174. }
  175. } );
  176. this.hintContainer = new CKEDITOR.dom.element( 'div' );
  177. this.hintContainer.addClass( 'toolbarModifier-hints' );
  178. this._fillHintByUnusedElements();
  179. this.hintContainer.insertBefore( codeMirrorWrapper );
  180. };
  181. /**
  182. * Create DOM string and set to hint container,
  183. * show proper information when no unused element left.
  184. *
  185. * @private
  186. */
  187. ToolbarTextModifier.prototype._fillHintByUnusedElements = function() {
  188. var unused = this.getUnusedButtonsArray( this.actualConfig.toolbar, true );
  189. unused = this.groupButtonNamesByGroup( unused );
  190. var unusedElements = FullToolbarEditor.map( unused, function( elem ) {
  191. var buttonsList = FullToolbarEditor.map( elem.buttons, function( buttonName ) {
  192. return '<code>' + buttonName + '</code> ';
  193. } ).join( '' );
  194. return [
  195. '<dt>',
  196. '<code>', elem.name, '</code>',
  197. '</dt>',
  198. '<dd>',
  199. buttonsList,
  200. '</dd>'
  201. ].join( '' );
  202. } ).join( ' ' );
  203. var listHeader = [
  204. '<dt class="list-header">Toolbar group</dt>',
  205. '<dd class="list-header">Unused items</dd>'
  206. ].join( '' );
  207. var header = '<h3>Unused toolbar items</h3>';
  208. if ( !unused.length ) {
  209. listHeader = '<p>All items are in use.</p>';
  210. }
  211. this.codeContainer.refresh();
  212. this.hintContainer.setHtml( header + '<dl>' + listHeader + unusedElements + '</dl>' );
  213. };
  214. /**
  215. * @param {String} buttonName
  216. * @returns {String}
  217. */
  218. ToolbarTextModifier.prototype.getToolbarGroupByButtonName = function( buttonName ) {
  219. var buttonNames = this.fullToolbarEditor.buttonNamesByGroup;
  220. for ( var groupName in buttonNames ) {
  221. var buttons = buttonNames[ groupName ];
  222. var i = buttons.length;
  223. while ( i-- ) {
  224. if ( buttonName === buttons[ i ] ) {
  225. return groupName;
  226. }
  227. }
  228. }
  229. return null;
  230. };
  231. /**
  232. * Filter all available toolbar elements by array of elements provided in first argument.
  233. * Returns elements which are not used.
  234. *
  235. * @param {Object} toolbar
  236. * @param {Boolean} [sorted=false]
  237. * @param {String} prefix
  238. * @returns {Array}
  239. */
  240. ToolbarTextModifier.prototype.getUnusedButtonsArray = function( toolbar, sorted, prefix ) {
  241. sorted = ( sorted === true ? true : false );
  242. var providedElements = ToolbarTextModifier.mapToolbarCfgToElementsList( toolbar ),
  243. allElements = Object.keys( this.fullToolbarEditor.editorInstance.ui.items );
  244. // get rid of "-" elements
  245. allElements = FullToolbarEditor.filter( allElements, function( elem ) {
  246. var isSeparator = ( elem === '-' ),
  247. matchPrefix = ( prefix === undefined || elem.toLowerCase().indexOf( prefix.toLowerCase() ) === 0 );
  248. return !isSeparator && matchPrefix;
  249. } );
  250. var elementsNotUsed = FullToolbarEditor.filter( allElements, function( elem ) {
  251. return CKEDITOR.tools.indexOf( providedElements, elem ) == -1;
  252. } );
  253. if ( sorted )
  254. elementsNotUsed.sort();
  255. return elementsNotUsed;
  256. };
  257. /**
  258. *
  259. * @param {Array} buttons
  260. * @returns {Array}
  261. */
  262. ToolbarTextModifier.prototype.groupButtonNamesByGroup = function( buttons ) {
  263. var result = [],
  264. groupedBtns = JSON.parse( JSON.stringify( this.fullToolbarEditor.buttonNamesByGroup ) );
  265. for ( var groupName in groupedBtns ) {
  266. var currGroup = groupedBtns[ groupName ];
  267. currGroup = FullToolbarEditor.filter( currGroup, function( btnName ) {
  268. return CKEDITOR.tools.indexOf( buttons, btnName ) !== -1;
  269. } );
  270. if ( currGroup.length ) {
  271. result.push( {
  272. name: groupName,
  273. buttons: currGroup
  274. } );
  275. }
  276. }
  277. return result;
  278. };
  279. /**
  280. * Map toolbar config value to flat items list.
  281. *
  282. * input:
  283. * [
  284. * { name: "basicstyles", items: ["Bold", "Italic"] },
  285. * { name: "advancedstyles", items: ["Bold", "Outdent", "Indent"] }
  286. * ]
  287. *
  288. * output:
  289. * ["Bold", "Italic", "Outdent", "Indent"]
  290. *
  291. * @param {Object} toolbar
  292. * @returns {Array}
  293. */
  294. ToolbarTextModifier.mapToolbarCfgToElementsList = function( toolbar ) {
  295. var elements = [];
  296. var max = toolbar.length;
  297. for ( var i = 0; i < max; i += 1 ) {
  298. if ( !toolbar[ i ] || typeof toolbar[ i ] === 'string' )
  299. continue;
  300. elements = elements.concat( FullToolbarEditor.filter( toolbar[ i ].items, checker ) );
  301. }
  302. function checker( elem ) {
  303. return elem !== '-';
  304. }
  305. return elements;
  306. };
  307. /**
  308. * @param {String} cfg
  309. * @private
  310. */
  311. ToolbarTextModifier.prototype._setupActualConfig = function( cfg ) {
  312. cfg = cfg || this.editorInstance.config;
  313. // if toolbar already exists in config, there is nothing to do
  314. if ( CKEDITOR.tools.isArray( cfg.toolbar ) )
  315. return;
  316. // if toolbar group not present, we need to pick them from full toolbar instance
  317. if ( !cfg.toolbarGroups )
  318. cfg.toolbarGroups = this.fullToolbarEditor.getFullToolbarGroupsConfig( true );
  319. this._fixGroups( cfg );
  320. cfg.toolbar = this._mapToolbarGroupsToToolbar( cfg.toolbarGroups, this.actualConfig.removeButtons );
  321. this.actualConfig.toolbar = cfg.toolbar;
  322. this.actualConfig.removeButtons = '';
  323. };
  324. /**
  325. * **Please note:** This method modify element provided in first argument.
  326. *
  327. * @param {Array} toolbarGroups
  328. * @returns {Array}
  329. * @private
  330. */
  331. ToolbarTextModifier.prototype._mapToolbarGroupsToToolbar = function( toolbarGroups, removedBtns ) {
  332. removedBtns = removedBtns || this.editorInstance.config.removedBtns;
  333. removedBtns = typeof removedBtns == 'string' ? removedBtns.split( ',' ) : [];
  334. // from the end, because array indexes may change
  335. var i = toolbarGroups.length;
  336. while ( i-- ) {
  337. var mappedSubgroup = this._mapToolbarSubgroup( toolbarGroups[ i ], removedBtns );
  338. if ( toolbarGroups[ i ].type === 'separator' ) {
  339. toolbarGroups[ i ] = '/';
  340. continue;
  341. }
  342. // don't want empty groups
  343. if ( CKEDITOR.tools.isArray( mappedSubgroup ) && mappedSubgroup.length === 0 ) {
  344. toolbarGroups.splice( i, 1 );
  345. continue;
  346. }
  347. if ( typeof mappedSubgroup == 'string' )
  348. toolbarGroups[ i ] = mappedSubgroup;
  349. else {
  350. toolbarGroups[ i ] = {
  351. name: toolbarGroups[ i ].name,
  352. items: mappedSubgroup
  353. };
  354. }
  355. }
  356. return toolbarGroups;
  357. };
  358. /**
  359. *
  360. * @param {String|Object} group
  361. * @param {Array} removedBtns
  362. * @returns {Array}
  363. * @private
  364. */
  365. ToolbarTextModifier.prototype._mapToolbarSubgroup = function( group, removedBtns ) {
  366. var totalBtns = 0;
  367. if ( typeof group == 'string' )
  368. return group;
  369. var max = group.groups ? group.groups.length : 0,
  370. result = [];
  371. for ( var i = 0; i < max; i += 1 ) {
  372. var currSubgroup = group.groups[ i ];
  373. var buttons = this.fullToolbarEditor.buttonsByGroup[ typeof currSubgroup === 'string' ? currSubgroup : currSubgroup.name ] || [];
  374. buttons = this._mapButtonsToButtonsNames( buttons, removedBtns );
  375. var currTotalBtns = buttons.length;
  376. totalBtns += currTotalBtns;
  377. result = result.concat( buttons );
  378. if ( currTotalBtns )
  379. result.push( '-' );
  380. }
  381. if ( result[ result.length - 1 ] == '-' )
  382. result.pop();
  383. return result;
  384. };
  385. /**
  386. *
  387. * @param {Array} buttons
  388. * @param {Array} removedBtns
  389. * @returns {Array}
  390. * @private
  391. */
  392. ToolbarTextModifier.prototype._mapButtonsToButtonsNames = function( buttons, removedBtns ) {
  393. var i = buttons.length;
  394. while ( i-- ) {
  395. var currBtn = buttons[ i ],
  396. camelCasedName;
  397. if ( typeof currBtn === 'string' ) {
  398. camelCasedName = currBtn;
  399. } else {
  400. camelCasedName = this.fullToolbarEditor.getCamelCasedButtonName( currBtn.name );
  401. }
  402. if ( CKEDITOR.tools.indexOf( removedBtns, camelCasedName ) !== -1 ) {
  403. buttons.splice( i, 1 );
  404. continue;
  405. }
  406. buttons[ i ] = camelCasedName;
  407. }
  408. return buttons;
  409. };
  410. /**
  411. * @param {String} val
  412. * @returns {Object}
  413. * @private
  414. */
  415. ToolbarTextModifier.prototype._evaluateValue = function( val ) {
  416. var parsed;
  417. try {
  418. var config = {};
  419. ( function() {
  420. var CKEDITOR = Function( 'var CKEDITOR = {}; ' + val + '; return CKEDITOR;' )();
  421. CKEDITOR.editorConfig( config );
  422. parsed = config;
  423. } )();
  424. // CKEditor does not handle empty arrays in configuration files
  425. // on IE8
  426. var i = parsed.toolbar.length;
  427. while ( i-- )
  428. if ( !parsed.toolbar[ i ] ) parsed.toolbar.splice( i, 1 );
  429. } catch ( e ) {
  430. parsed = null;
  431. }
  432. return parsed;
  433. };
  434. /**
  435. * @param {Array} toolbar
  436. * @returns {{toolbarGroups: Array, removeButtons: string}}
  437. */
  438. ToolbarTextModifier.prototype.mapToolbarToToolbarGroups = function( toolbar ) {
  439. var usedGroups = {},
  440. removeButtons = [],
  441. toolbarGroups = [];
  442. var max = toolbar.length;
  443. for ( var i = 0; i < max; i++ ) {
  444. if ( toolbar[ i ] === '/' ) {
  445. toolbarGroups.push( '/' );
  446. continue;
  447. }
  448. var items = toolbar[ i ].items;
  449. var toolbarGroup = {};
  450. toolbarGroup.name = toolbar[ i ].name;
  451. toolbarGroup.groups = [];
  452. var max2 = items.length;
  453. for ( var j = 0; j < max2; j++ ) {
  454. var item = items[ j ];
  455. if ( item === '-' ) {
  456. continue;
  457. }
  458. var groupName = this.getToolbarGroupByButtonName( item );
  459. var groupIndex = toolbarGroup.groups.indexOf( groupName );
  460. if ( groupIndex === -1 ) {
  461. toolbarGroup.groups.push( groupName );
  462. }
  463. usedGroups[ groupName ] = usedGroups[ groupName ] || {};
  464. var buttons = ( usedGroups[ groupName ].buttons = usedGroups[ groupName ].buttons || {} );
  465. buttons[ item ] = buttons[ item ] || { used: 0, origin: toolbarGroup.name };
  466. buttons[ item ].used++;
  467. }
  468. toolbarGroups.push( toolbarGroup );
  469. }
  470. // Handling removed buttons
  471. removeButtons = prepareRemovedButtons( usedGroups, this.fullToolbarEditor.buttonNamesByGroup );
  472. function prepareRemovedButtons( usedGroups, buttonNames ) {
  473. var removed = [];
  474. for ( var groupName in usedGroups ) {
  475. var group = usedGroups[ groupName ];
  476. var allButtonsInGroup = buttonNames[ groupName ].slice();
  477. removed = removed.concat( removeStuffFromArray( allButtonsInGroup, Object.keys( group.buttons ) ) );
  478. }
  479. return removed;
  480. }
  481. function removeStuffFromArray( array, stuff ) {
  482. array = array.slice();
  483. var i = stuff.length;
  484. while ( i-- ) {
  485. var atIndex = array.indexOf( stuff[ i ] );
  486. if ( atIndex !== -1 ) {
  487. array.splice( atIndex, 1 );
  488. }
  489. }
  490. return array;
  491. }
  492. return { toolbarGroups: toolbarGroups, removeButtons: removeButtons.join( ',' ) };
  493. };
  494. return ToolbarTextModifier;
  495. } )();