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