plugin.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. /**
  2. * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or http://ckeditor.com/license
  4. */
  5. /**
  6. * @fileOverview Charts for CKEditor using Chart.js.
  7. */
  8. /* global alert:false, Chart:false */
  9. 'use strict';
  10. // TODO IE8 fallback to a table maybe?
  11. // TODO a11y http://www.w3.org/html/wg/wiki/Correct_Hidden_Attribute_Section_v4
  12. ( function() {
  13. CKEDITOR.plugins.add( 'chart', {
  14. // Required plugins
  15. requires: 'widget,dialog',
  16. // Name of the file in the "icons" folder
  17. icons: 'chart',
  18. // Supported languages
  19. lang: 'en,pl',
  20. // Load library that renders charts inside CKEditor, if Chart object is not already available.
  21. afterInit: function() {
  22. var plugin = this;
  23. if ( typeof Chart === 'undefined' ) {
  24. // Chart library is loaded asynchronously, so we can draw anything only once it's loaded.
  25. CKEDITOR.scriptLoader.load( CKEDITOR.getUrl( plugin.path + 'lib/chart.min.js' ), function() {
  26. plugin.drawCharts();
  27. } );
  28. }
  29. },
  30. // Function called on initialization of every editor instance created in the page.
  31. init: function( editor ) {
  32. var plugin = this;
  33. var chartDefaultHeight = editor.config.chart_height || 300;
  34. // Default hardcoded values used if config.chart_colors is not provided.
  35. var colors = editor.config.chart_colors ||
  36. {
  37. // Colors for Bar/Line chart: http://www.chartjs.org/docs/#bar-chart-data-structure
  38. fillColor: 'rgba(151,187,205,0.5)',
  39. strokeColor: 'rgba(151,187,205,0.8)',
  40. highlightFill: 'rgba(151,187,205,0.75)',
  41. highlightStroke: 'rgba(151,187,205,1)',
  42. // Colors for Doughnut/Pie/PolarArea charts: http://www.chartjs.org/docs/#doughnut-pie-chart-data-structure
  43. data: [ '#B33131', '#B66F2D', '#00b5b0', '#71B232', '#33B22D', '#31B272', '#2DB5B5', '#3172B6', '#3232B6', '#6E31B2', '#B434AF', '#B53071' ]
  44. };
  45. var config = {
  46. Bar: editor.config.chart_configBar || { animation: false },
  47. Doughnut: editor.config.chart_configDoughnut || { animateRotate: false },
  48. Line: editor.config.chart_configLine || { animation: false },
  49. Pie: editor.config.chart_configPie || { animateRotate: false },
  50. PolarArea: editor.config.chart_configPolarArea || { animateRotate: false }
  51. };
  52. // The number of rows in Edit Chart dialog window.
  53. var inputRows = editor.config.chart_maxItems || 8;
  54. // Inject required CSS stylesheet to classic editors because the <iframe> needs it.
  55. // Inline editors will ignore this, the developer is supposed to load chart.css directly on a page.
  56. // "this.path" is a path to the current plugin.
  57. editor.addContentsCss( CKEDITOR.getUrl( plugin.path + 'chart.css' ) );
  58. // A little bit of magic to support "Preview" feature in CKEditor (in a popup).
  59. // In order to transform downcasted widgets into nice charts we need to:
  60. // 1. Pass color settings and charts configuration through JSON.
  61. // 2. Load the Chart.js library
  62. // 3. Load a helper script that will "upcast" widgets and initiate charts.
  63. editor.on( 'contentPreview', function( evt ) {
  64. evt.data.dataValue = evt.data.dataValue.replace( /<\/head>/,
  65. '<script>var chartjs_colors_json = "' + JSON.stringify( colors ).replace( /\"/g, '\\"' ) + '";<\/script>' +
  66. '<script>var chartjs_config_json = "' + JSON.stringify( config ).replace( /\"/g, '\\"' ) + '";<\/script>' +
  67. '<script src="' + CKEDITOR.getUrl( plugin.path + 'lib/chart.min.js' ) + '"><\/script>' +
  68. '<script src="' + CKEDITOR.getUrl( plugin.path + 'widget2chart.js' ) + '"><\/script><\/head>' );
  69. } );
  70. // The dialog window to insert / edit a chart.
  71. CKEDITOR.dialog.add( 'chart', function( editor ) {
  72. var dialog = {
  73. title: editor.lang.chart.dialogTitle,
  74. minWidth: 200,
  75. minHeight: 100,
  76. // Executed every time a dialog is shown.
  77. onShow: function() {
  78. var widget = editor.widgets.focused;
  79. if ( !widget )
  80. return;
  81. // We edit an existing widget, so we have already some data and should set input values accordingly.
  82. // The dialog consists of multiple rows with two input elements each.
  83. // We could use "setup" callbacks for each UI element, but the we'd end up with lots of data properties.
  84. // So instead we merge all the values into a single object, ending with an array like:
  85. // [ {"value":45,"label":"Yes"}, {}, .... ]
  86. // to make it easier to pass it to Chart.js later.
  87. for ( var j = 0; j < inputRows; j++ ) {
  88. if ( widget.data.values[j] ) {
  89. // toString() is used here to set correctly zero values.
  90. this.setValueOf( 'data', 'value' + j, widget.data.values[j].value.toString() );
  91. this.setValueOf( 'data', 'label' + j, widget.data.values[j].label );
  92. }
  93. }
  94. },
  95. // Executed every time a dialog is closed (OK is pressed).
  96. onOk: function() {
  97. // ATTENTION: this.widget is not available here in CKEditor by default.
  98. // We added this in the "init" function of a widget ("Pass the reference to this widget to the dialog."),
  99. var widget = this.widget,
  100. values = [], value;
  101. // We could use "commit" callbacks in every input element to set widget data.
  102. // But we decided to keep multiple values in a single object (see comment in "onShow" for more details).
  103. for ( var j = 0; j < inputRows; j++ ) {
  104. value = this.getValueOf( 'data', 'value' + j );
  105. if ( value )
  106. values.push( { value: parseFloat( this.getValueOf( 'data', 'value' + j ) ), label: this.getValueOf( 'data', 'label' + j ) } );
  107. }
  108. widget.setData( 'values', values );
  109. widget.setData( 'chart', this.getValueOf( 'data', 'chart' ) );
  110. widget.setData( 'height', this.getValueOf( 'data', 'height' ) );
  111. },
  112. // Define elements in a dialog window.
  113. contents: [
  114. {
  115. id: 'data',
  116. elements: [
  117. {
  118. type: 'hbox',
  119. children:
  120. [
  121. {
  122. id: 'chart',
  123. type: 'select',
  124. label: editor.lang.chart.chartType,
  125. labelLayout: 'horizontal',
  126. // Align vertically, otherwise labels are a bit misplaced.
  127. labelStyle: 'display:block;padding: 4px 6px;',
  128. items: [
  129. [ editor.lang.chart.bar, 'bar' ],
  130. [ editor.lang.chart.line, 'line' ],
  131. [ editor.lang.chart.pie, 'pie' ],
  132. [ editor.lang.chart.polar, 'polar' ],
  133. [ editor.lang.chart.doughnut, 'doughnut' ]
  134. ],
  135. style: 'margin-bottom:10px',
  136. setup: function( widget ) {
  137. // Set radios to the correct value based on the widget type.
  138. this.setValue( widget.data.chart );
  139. }
  140. },
  141. {
  142. id: 'height',
  143. type: 'text',
  144. label: editor.lang.chart.height,
  145. labelLayout: 'horizontal',
  146. // Align vertically, otherwise labels are a bit misplaced.
  147. labelStyle: 'display:block;padding: 4px 6px;',
  148. width: '50px',
  149. setup: function( widget ) {
  150. this.setValue( widget.data.height );
  151. },
  152. validate: function() {
  153. var value = this.getValue(),
  154. pass = ( !value || !!( CKEDITOR.dialog.validate.number( value ) && value >= 0 ) );
  155. if ( !pass ) {
  156. alert( editor.lang.common.validateNumberFailed );
  157. this.select();
  158. }
  159. return pass;
  160. }
  161. }
  162. ]
  163. }
  164. ]
  165. }
  166. ]
  167. };
  168. // Rarely elements in dialog definitions are generated in loops.
  169. // Here we decided to make the number of "data" rows configurable, so a loop is handy.
  170. for ( var i = 0; i < inputRows; i++ ) {
  171. dialog.contents[0].elements.push( {
  172. type: 'hbox',
  173. children:
  174. [
  175. {
  176. id: 'value' + i,
  177. type: 'text',
  178. labelLayout: 'horizontal',
  179. label: editor.lang.chart.value,
  180. // Align vertically, otherwise labels are a bit misplaced.
  181. labelStyle: 'display:block;padding: 4px 6px;',
  182. width: '50px',
  183. validate: function() {
  184. var value = this.getValue(),
  185. pass = ( !value || !!( CKEDITOR.dialog.validate.number( value ) && value >= 0 ) );
  186. if ( !pass ) {
  187. alert( editor.lang.common.validateNumberFailed );
  188. this.select();
  189. }
  190. return pass;
  191. }
  192. },
  193. {
  194. id: 'label' + i,
  195. type: 'text',
  196. label: editor.lang.chart.label,
  197. labelLayout: 'horizontal',
  198. // Align vertically, otherwise labels are a bit misplaced.
  199. labelStyle: 'display:block;padding: 4px 6px;',
  200. width: '200px'
  201. }
  202. ]
  203. } );
  204. }
  205. return dialog;
  206. } );
  207. // Helper function that we'd like to run in case Chart.js library was loaded asynchronously.
  208. this.drawCharts = function() {
  209. // All available widgets are stored in an object, not an array.
  210. for ( var id in editor.widgets.instances ) {
  211. // The name was provided in editor.widgets.add()
  212. if ( editor.widgets.instances[id].name == 'chart' ) {
  213. // Our "data" callback draws widgets, so let's call it.
  214. editor.widgets.instances[id].fire( 'data' );
  215. }
  216. }
  217. };
  218. function renderChart( canvas, data, legend ) {
  219. var values = data.values,
  220. chartType = data.chart;
  221. // The code below is the same as in widget2chart.js.
  222. // ########## RENDER CHART START ##########
  223. // Prepare canvas and chart instance.
  224. var i, ctx = canvas.getContext( '2d' ),
  225. chart = new Chart( ctx ); // jshint ignore:line
  226. // Set some extra required colors by Pie/Doughnut charts.
  227. // Ugly charts will be drawn if colors are not provided for each data.
  228. // http://www.chartjs.org/docs/#doughnut-pie-chart-data-structure
  229. if ( chartType != 'bar' ) {
  230. for ( i = 0; i < values.length; i++ ) {
  231. values[i].color = colors.data[i];
  232. values[i].highlight = colors.data[i];
  233. }
  234. }
  235. // Prepare data for bar/line charts.
  236. if ( chartType == 'bar' || chartType == 'line' ) {
  237. var data = {
  238. // Chart.js supports multiple datasets.
  239. // http://www.chartjs.org/docs/#bar-chart-data-structure
  240. // This plugin is simple, so it supports just one.
  241. // Need more features? Create a Pull Request :-)
  242. datasets: [
  243. {
  244. label: '',
  245. fillColor: colors.fillColor,
  246. strokeColor: colors.strokeColor,
  247. highlightFill: colors.highlightFill,
  248. highlightStroke: colors.highlightStroke,
  249. data: []
  250. } ],
  251. labels: []
  252. };
  253. // Bar charts accept different data format than Pie/Doughnut.
  254. // We need to pass values inside datasets[0].data.
  255. for ( i = 0; i < values.length; i++ ) {
  256. if ( values[i].value ) {
  257. data.labels.push( values[i].label );
  258. data.datasets[0].data.push( values[i].value );
  259. }
  260. }
  261. // Legend makes sense only with more than one dataset.
  262. legend.innerHTML = '';
  263. }
  264. // Render Bar chart.
  265. if ( chartType == 'bar' ) {
  266. chart.Bar( data, config.Bar );
  267. }
  268. // Render Line chart.
  269. else if ( chartType == 'line' ) {
  270. chart.Line( data, config.Line );
  271. }
  272. // Render Line chart.
  273. else if ( chartType == 'polar' ) {
  274. //chart.PolarArea( values );
  275. legend.innerHTML = chart.PolarArea( values, config.PolarArea ).generateLegend();
  276. }
  277. // Render Pie chart and legend.
  278. else if ( chartType == 'pie' ) {
  279. legend.innerHTML = chart.Pie( values, config.Pie ).generateLegend();
  280. }
  281. // Render Doughnut chart and legend.
  282. else {
  283. legend.innerHTML = chart.Doughnut( values, config.Doughnut ).generateLegend();
  284. }
  285. // ########## RENDER CHART END ##########
  286. }
  287. // Here we define the widget itself.
  288. editor.widgets.add( 'chart', {
  289. // The *label* for the button. The button *name* is assigned automatically based on the widget name.
  290. button: editor.lang.chart.chart,
  291. // Connect widget with a dialog defined earlier. So our toolbar button will open a dialog window.
  292. dialog: 'chart',
  293. // Based on this template a widget will be created automatically once user exits the dialog window.
  294. template: '<div class="chartjs" data-chart="bar" data-chart-height="' + chartDefaultHeight + '"><canvas height="' + chartDefaultHeight + '"></canvas><div class="chartjs-legend"></div></div>',
  295. // In order to provide styles (classes) for this widget through config.stylesSet we need to explicitly define the stylable elements.
  296. styleableElements: 'div',
  297. // Name to be displayed in the elements path (at the bottom of CKEditor),
  298. pathName: 'chart',
  299. // Run when initializing widget (thank you, captain obvious!).
  300. // It is common to use the init method to populate widget data with information loaded from the DOM.
  301. init: function() {
  302. // When an empty widget is initialized after clicking a button in the toolbar, we do not have yet chart values.
  303. if ( this.element.data( 'chart-value' ) ) {
  304. this.setData( 'values', JSON.parse( this.element.data( 'chart-value' ) ) );
  305. }
  306. // Chart is specified in a template, so it is available even in an empty widget.
  307. this.setData( 'chart', this.element.data( 'chart' ) );
  308. // Height is specified in a template, so it is available even in an empty widget.
  309. this.setData( 'height', this.element.data( 'chart-height' ) );
  310. // Pass the reference to this widget to the dialog. See "onOk" in the dialog definition, we needed widget there.
  311. this.on( 'dialog', function( evt ) {
  312. evt.data.widget = this;
  313. }, this );
  314. },
  315. // Run when widget data is changed (widget is rendered for the first time, inserted, changed).
  316. data: function() {
  317. // Just in case Chart.js was loaded asynchronously and is not available yet.
  318. if ( typeof Chart === 'undefined' )
  319. return;
  320. // It's hard to draw a chart without numbers.
  321. if ( !this.data.values )
  322. return;
  323. // It looks like Chart.js does not handle well updating charts.
  324. // When hovering over updated canvas old data is picked up sometimes, so we need to always replace an old canvas.
  325. var canvas = editor.document.createElement( 'canvas', { attributes: { height: this.data.height } } );
  326. canvas.replace( this.element.getChild( 0 ) );
  327. // Unify variable names with the one used in widget2chart.js.
  328. var legend = this.element.getChild( 1 ).$;
  329. canvas = canvas.$;
  330. // IE8 can't handle the next part (without the help of excanvas etc.).
  331. if ( !canvas.getContext )
  332. return;
  333. var data = this.data;
  334. // Without timeout the chart does not render immediately after inserting into the editing area.
  335. setTimeout( function() { renderChart( canvas, data, legend ) }, 0 );
  336. },
  337. // ACF settings. Without allowing elements introduced by this plugin, CKEditor built-in filter would remove it.
  338. allowedContent: 'div(!chartjs)[data-*];',
  339. requiredContent: 'div(chartjs)[data-chart-value,data-chart,data-chart-height]',
  340. // Executed when CKEditor loads content, when switching from source to wysiwyg mode. Makes HTML content a widget.
  341. upcast: function( element ) {
  342. if ( element.name == 'div' && element.hasClass( 'chartjs' ) ) {
  343. // Downcasted <div> could have contained some text like "chart" or &nbsp; which was there just to prevent <div>s from being deleted.
  344. // Get rid of it when upcasting.
  345. element.setHtml( '' );
  346. // Chart.js work on canvas elements, Prepare one.
  347. var canvas = new CKEDITOR.htmlParser.element( 'canvas', { height: element.attributes[ 'data-chart-height' ] } );
  348. element.add( canvas );
  349. // And make place for a legend.
  350. var div = new CKEDITOR.htmlParser.element( 'div', { 'class': 'chartjs-legend' } );
  351. element.add( div );
  352. return element;
  353. }
  354. },
  355. // Executed when CKEditor returns content, when switching from wysiwyg to source mode. Transforms a widget back to a downcasted form.
  356. downcast: function( element ) {
  357. var data = [];
  358. // Should not happen unless someone has accidentally messed up ACF rules.
  359. if ( !this.data.values )
  360. return;
  361. for ( var i = 0; i < this.data.values.length; i++ ) {
  362. // Get data from widget into an object in order to save it as data-chart-value attribute.
  363. // We could simply save this.data.values, but it contains some additional temporary data which we want to skip (like colors).
  364. data.push( {
  365. value: this.data.values[i].value,
  366. label: this.data.values[i].label
  367. } );
  368. }
  369. // Create the downcasted form of a widget (a simple <div>).
  370. var el = new CKEDITOR.htmlParser.element( 'div', {
  371. // We could pass here hardcoded "chartjs" class, but this way we would lose here all the classes applied through the Styles dropdown.
  372. // (In case someone defined his own styles for the chart widget)
  373. 'class': element.attributes['class'],
  374. 'data-chart': this.data.chart,
  375. 'data-chart-height': this.data.height,
  376. // Feature detection (editor.getSelectedHtml) to check if CKEditor 4.5+ is used.
  377. // CKEditor < 4.5 and CKEditor 4.5+ require different code due to https://dev.ckeditor.com/ticket/13105
  378. 'data-chart-value': editor.getSelectedHtml ? JSON.stringify( data ) : CKEDITOR.tools.htmlEncodeAttr( JSON.stringify( data ) )
  379. } );
  380. return el;
  381. }
  382. } );
  383. }
  384. } );
  385. } )();
  386. /**
  387. * The default chart height (in pixels) in the Edit Chart dialog window.
  388. *
  389. * // Set default height to 400px.
  390. * config.chart_height = 400;
  391. *
  392. * @cfg {Integer} [chart_height=300]
  393. * @member CKEDITOR.config
  394. */
  395. /**
  396. * The number of rows (items to enter) in the Edit Chart dialog window.
  397. *
  398. * // Set number of rows to 12.
  399. * config.chart_maxItems = 12;
  400. *
  401. * @cfg {Integer} [chart_maxItems=12]
  402. * @member CKEDITOR.config
  403. */
  404. /**
  405. * Colors used to draw charts. See <a href="http://www.chartjs.org/docs/#bar-chart-data-structure">Bar chart data structure</a> and
  406. * <a href="http://www.chartjs.org/docs/#doughnut-pie-chart-data-structure">Pie chart data structure</a>.
  407. *
  408. * config.chart_colors =
  409. * {
  410. * // Colors for Bar/Line chart.
  411. * fillColor: 'rgba(151,187,205,0.5)',
  412. * strokeColor: 'rgba(151,187,205,0.8)',
  413. * highlightFill: 'rgba(151,187,205,0.75)',
  414. * highlightStroke: 'rgba(151,187,205,1)',
  415. * // Colors for Doughnut/Pie/PolarArea charts.
  416. * data: [ '#B33131', '#B66F2D', '#B6B330', '#71B232', '#33B22D', '#31B272', '#2DB5B5', '#3172B6', '#3232B6', '#6E31B2', '#B434AF', '#B53071' ]
  417. * }
  418. *
  419. * @cfg {Array} chart_colors
  420. * @member CKEDITOR.config
  421. */
  422. /**
  423. * Chart.js configuration to use for Bar charts.
  424. *
  425. * @cfg {Object} [chart_configBar={ animation: false }]
  426. * @member CKEDITOR.config
  427. */
  428. /**
  429. * Chart.js configuration to use for Doughnut charts.
  430. *
  431. * @cfg {Object} [chart_configDoughnut={ animateRotate: false }]
  432. * @member CKEDITOR.config
  433. */
  434. /**
  435. * Chart.js configuration to use for Line charts.
  436. *
  437. * @cfg {Object} [chart_configLine={ animation: false }]
  438. * @member CKEDITOR.config
  439. */
  440. /**
  441. * Chart.js configuration to use for Pie charts.
  442. *
  443. * @cfg {Object} [chart_configPie={ animateRotate: false }]
  444. * @member CKEDITOR.config
  445. */
  446. /**
  447. * Chart.js configuration to use for PolarArea charts.
  448. *
  449. * @cfg {Object} [chart_configPolarArea={ animateRotate: false }]
  450. * @member CKEDITOR.config
  451. */