/**
* @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() {
CKEDITOR.plugins.add( 'chart', {
// Required plugins
requires: 'widget,dialog',
// Name of the file in the "icons" folder
icons: 'chart',
// Supported languages
lang: 'en,es,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' ) ) {
this.setData( 'values', JSON.parse( this.element.data( 'chart-value' ) ) );
}
// 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,
// Feature detection (editor.getSelectedHtml) to check if CKEditor 4.5+ is used.
// CKEditor < 4.5 and CKEditor 4.5+ require different code due to https://dev.ckeditor.com/ticket/13105
'data-chart-value': editor.getSelectedHtml ? JSON.stringify( data ) : CKEDITOR.tools.htmlEncodeAttr( JSON.stringify( 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
*/
|