diff --git a/addons/website/__manifest__.py b/addons/website/__manifest__.py
index 1ceed0edec6..0e16b446046 100644
--- a/addons/website/__manifest__.py
+++ b/addons/website/__manifest__.py
@@ -53,6 +53,7 @@
'views/snippets/s_faq_collapse.xml',
'views/snippets/s_features_grid.xml',
'views/snippets/s_tabs.xml',
+ 'views/snippets/s_chart.xml',
'views/website_views.xml',
'views/website_visitor_views.xml',
'views/res_config_settings_views.xml',
diff --git a/temp_js b/addons/website/static/src/js/content/snippets.animation.js
similarity index 87%
rename from temp_js
rename to addons/website/static/src/js/content/snippets.animation.js
index b9ca31ee248..70581af2540 100644
--- a/temp_js
+++ b/addons/website/static/src/js/content/snippets.animation.js
@@ -6,7 +6,6 @@ odoo.define('website.content.snippets.animation', function (require) {
*/
var Class = require('web.Class');
-const ColorpickerDialog = require('web.ColorpickerDialog');
var config = require('web.config');
var core = require('web.core');
var mixins = require('web.mixins');
@@ -14,7 +13,6 @@ var publicWidget = require('web.public.widget');
var utils = require('web.utils');
var qweb = core.qweb;
-const _t = core._t;
// Initialize fallbacks for the use of requestAnimationFrame,
// cancelAnimationFrame and performance.now()
@@ -919,138 +917,6 @@ registry.anchorSlide = publicWidget.Widget.extend({
},
});
-registry.chart = publicWidget.Widget.extend({
- selector: '.s_chart',
- disabledInEditableMode: false,
- jsLibs: [
- '/web/static/lib/Chart/Chart.js',
- ],
-
- /**
- * @override
- * @param {Object} parent
- * @param {Object} options The default value of the chartbar.
- */
- init: function (parent, options) {
- this._super.apply(this, arguments);
- this.style = window.getComputedStyle(document.documentElement);
- },
- /**
- * @override
- */
- start: function () {
- // Convert Theme colors to css color
- const data = JSON.parse(this.el.dataset.data);
- data.datasets.forEach(el => {
- if (Array.isArray(el.backgroundColor)) {
- el.backgroundColor = el.backgroundColor.map(el => this._convertToCssColor(el));
- el.borderColor = el.borderColor.map(el => this._convertToCssColor(el));
- } else {
- el.backgroundColor = this._convertToCssColor(el.backgroundColor);
- el.borderColor = this._convertToCssColor(el.borderColor);
- }
- el.borderWidth = this.el.dataset.borderWidth;
- });
-
- // Make chart data
- const chartData = {
- type: this.el.dataset.type,
- data: data,
- options: {
- legend: {
- display: this.el.dataset.legendPosition !== 'none',
- position: this.el.dataset.legendPosition,
- },
- tooltips: {
- enabled: this.el.dataset.tooltipDisplay === 'true',
- },
- title: {
- display: !!this.el.dataset.title,
- text: this.el.dataset.title,
- },
- },
- };
-
- // Add type specific options
- if (this.el.dataset.type === 'radar') {
- chartData.options.scale = {
- ticks: {
- beginAtZero: true,
- }
- };
- } else if (['pie', 'doughnut'].includes(this.el.dataset.type)) {
- chartData.options.tooltips.callbacks = {
- label: (tooltipItem, data) => {
- const label = data.datasets[tooltipItem.datasetIndex].label;
- const secondLabel = data.labels[tooltipItem.index];
- let final = label;
- if (label) {
- if (secondLabel) {
- final = label + ' - ' + secondLabel;
- }
- } else if (secondLabel) {
- final = secondLabel;
- }
- return final + ':' + data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
- },
- };
- } else {
- chartData.options.scales = {
- xAxes: [{
- stacked: this.el.dataset.stacked === 'true',
- ticks: {
- beginAtZero: true
- },
- }],
- yAxes: [{
- stacked: this.el.dataset.stacked === 'true',
- ticks: {
- beginAtZero: true
- },
- }],
- };
- }
-
- // Disable animation in edit mode
- if (this.editableMode) {
- chartData.options.animation = {
- duration: 0,
- };
- }
-
- const canvas = this.el.querySelector('canvas');
- this.chart = new window.Chart(canvas, chartData);
- return this._super.apply(this, arguments);
- },
- /**
- * @override
- * Discard all library changes to reset the state of the Html.
- */
- destroy: function () {
- if (this.chart) { // The widget can be destroyed before start has completed
- this.chart.destroy();
- this.el.querySelectorAll('.chartjs-size-monitor').forEach(el => el.remove());
- }
- this._super.apply(this, arguments);
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * @private
- * @param {string} color A css color or theme color string
- * @returns {string} Css color
- */
- _convertToCssColor: function (color) {
- if (!color) {
- return 'transparent';
- }
- return this.style.getPropertyValue(`--${color}`).trim() || color;
- },
-});
-
return {
Widget: publicWidget.Widget,
Animation: Animation,
diff --git a/temp_options b/addons/website/static/src/js/editor/snippets.options.js
similarity index 70%
rename from temp_options
rename to addons/website/static/src/js/editor/snippets.options.js
index 6c22382a0f2..182e6fc8b52 100644
--- a/temp_options
+++ b/addons/website/static/src/js/editor/snippets.options.js
@@ -2,7 +2,6 @@ odoo.define('website.editor.snippets.options', function (require) {
'use strict';
var core = require('web.core');
-const ColorpickerDialog = require('web.ColorpickerDialog');
var Dialog = require('web.Dialog');
var weWidgets = require('wysiwyg.widgets');
const wUtils = require('website.utils');
@@ -1268,473 +1267,6 @@ options.registry.SnippetMove = options.Class.extend({
},
});
-options.registry.InnerChart = options.Class.extend({
- custom_events: _.extend({}, options.Class.prototype.custom_events, {
- 'get_custom_colors': '_onGetCustomColors',
- }),
- events: _.extend({}, options.Class.prototype.events, {
- 'click we-button.add_column': '_onAddColumnClick',
- 'click we-button.add_row': '_onAddRowClick',
- 'click we-button.o_we_matrix_remove_col': '_onRemoveColumnClick',
- 'click we-button.o_we_matrix_remove_row': '_onRemoveRowClick',
- 'blur we-matrix input': '_onMatrixInputFocusOut',
- 'focus we-matrix input': '_onMatrixInputFocus',
- }),
-
- /**
- * @override
- */
- init: function () {
- this._super.apply(this, arguments);
- this.themeArray = ['alpha', 'beta', 'gamma', 'delta', 'epsilon'];
- this.style = window.getComputedStyle(document.documentElement);
- },
- /**
- * @override
- */
- start: function () {
- this.backSelectEl = this.el.querySelector('[data-name="chart_bg_color_opt"]');
- this.borderSelectEl = this.el.querySelector('[data-name="chart_border_color_opt"]');
-
- // Build matrix content
- this.tableEl = this.el.querySelector('we-matrix table');
- const data = JSON.parse(this.$target[0].dataset.data);
- data.labels.forEach(el => {
- this._addRow(el);
- });
- data.datasets.forEach((el, i) => {
- if (this._isPieChart()) {
- // Add header colors in case the user changes the type of graph
- const headerBackgroundColor = this.themeArray[i] || this._randomColor();
- const headerBorderColor = this.themeArray[i] || this._randomColor();
- this._addColumn(el.label, el.data, headerBackgroundColor, headerBorderColor, el.backgroundColor, el.borderColor);
- } else {
- this._addColumn(el.label, el.data, el.backgroundColor, el.borderColor);
- }
- });
- this._displayRemoveColButton();
- this._displayRemoveRowButton();
- this._setDefaultSelectedInput();
- return this._super(...arguments);
- },
- /**
- * @override
- */
- updateUI: async function () {
- // Selected input might not be in dom anymore if col/row removed
- // Done before _super because _computeWidgetState of colorChange
- if (!this.lastEditableSelectedInput.closest('table') || this.colorPaletteSelectedInput && !this.colorPaletteSelectedInput.closest('table')) {
- this._setDefaultSelectedInput();
- }
-
- await this._super(...arguments);
-
- // prevent the columns from becoming too small.
- this.tableEl.classList.toggle('o_we_matrix_five_col', this.tableEl.querySelectorAll('tr:first-child th').length > 5);
-
- this.backSelectEl.querySelector('we-title').textContent = this._isPieChart() ? _t("Data Background Color") : _t("Dataset Background Color");
- this.borderSelectEl.querySelector('we-title').textContent = this._isPieChart() ? _t("Data Border Color") : _t("Dataset Border Color");
-
- // Dataset/Cell color
- this.tableEl.querySelectorAll('input').forEach(el => el.style.border = '');
- const selector = this._isPieChart() ? 'td input' : 'tr:first-child input';
- this.tableEl.querySelectorAll(selector).forEach(el => {
- const color = el.dataset.backgroundColor || el.dataset.borderColor;
- if (color) {
- el.style.border = '2px solid';
- el.style.borderColor = ColorpickerDialog.isCSSColor(color) ? color : this.style.getPropertyValue(`--${color}`).trim();
- }
- });
- },
-
- //--------------------------------------------------------------------------
- // Options
- //--------------------------------------------------------------------------
-
- /**
- * Set the color on the selected input.
- */
- colorChange: async function (previewMode, widgetValue, params) {
- if (widgetValue) {
- this.colorPaletteSelectedInput.dataset[params.attributeName] = widgetValue;
- } else {
- delete this.colorPaletteSelectedInput.dataset[params.attributeName];
- }
- await this._reloadGraph();
- // To focus back the input that is edited we have to wait for the color
- // picker to be fully reloaded.
- await new Promise(resolve => setTimeout(() => {
- this.lastEditableSelectedInput.focus();
- resolve();
- }));
- },
- /**
- * @override
- */
- selectDataAttribute: async function (previewMode, widgetValue, params) {
- await this._super(...arguments);
- // Data might change if going from or to a pieChart.
- if (params.attributeName === 'type') {
- this._setDefaultSelectedInput();
- await this._reloadGraph();
- }
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * @override
- */
- _computeWidgetState: function (methodName, params) {
- if (methodName === 'colorChange') {
- return this.colorPaletteSelectedInput && this.colorPaletteSelectedInput.dataset[params.attributeName] || '';
- }
- return this._super(...arguments);
- },
- /**
- * @override
- */
- _computeWidgetVisibility: function (widgetName, params) {
- switch (widgetName) {
- case 'stacked_chart_opt': {
- return this._getColumnCount() > 1;
- }
- case 'chart_bg_color_opt':
- case 'chart_border_color_opt': {
- return !!this.colorPaletteSelectedInput;
- }
- }
- return this._super(...arguments);
- },
- /**
- * Sets and reloads the data on the canvas if it has changed.
- * Used in matrix related method.
- *
- * @private
- */
- _reloadGraph: async function () {
- const jsonValue = this._matrixToChartData();
- if (this.$target[0].dataset.data !== jsonValue) {
- this.$target[0].dataset.data = jsonValue;
- await this._refreshPublicWidgets();
- }
- },
- /**
- * Return a stringifyed chart.js data object from the matrix
- * Pie charts have one color per data while other charts have one color per dataset.
- *
- * @private
- */
- _matrixToChartData: function () {
- const data = {
- labels: [],
- datasets: [],
- };
- this.tableEl.querySelectorAll('tr:first-child input').forEach(el => {
- data.datasets.push({
- label: el.value || '',
- data: [],
- backgroundColor: this._isPieChart() ? [] : el.dataset.backgroundColor || '',
- borderColor: this._isPieChart() ? [] : el.dataset.borderColor || '',
- });
- });
- this.tableEl.querySelectorAll('tr:not(:first-child):not(:last-child)').forEach((el) => {
- const title = el.querySelector('th input').value || '';
- data.labels.push(title);
- el.querySelectorAll('td input').forEach((el, i) => {
- data.datasets[i].data.push(el.value || 0);
- if (this._isPieChart()) {
- data.datasets[i].backgroundColor.push(el.dataset.backgroundColor || '');
- data.datasets[i].borderColor.push(el.dataset.borderColor || '');
- }
- });
- });
- return JSON.stringify(data);
- },
- /**
- * Return a td containing a we-button with minus icon
- *
- * @param {...string} classes Classes to add to the we-button
- * @returns {HTMLElement}
- */
- _makeDeleteButton: function (...classes) {
- const rmbuttonEl = options.buildElement('we-button', null, {
- classes: ['fa', 'fa-fw', 'fa-minus', ...classes],
- });
- const newEl = document.createElement('td');
- newEl.appendChild(rmbuttonEl);
- return newEl;
- },
- /**
- * Add a column to the matrix
- * The th (dataset label) of a column hold the colors for the entire dataset if the graph is not a pie chart
- * If the graph is a pie chart the color of the td (data) are used.
- *
- * @private
- * @param {String} title The title of the column
- * @param {Array} values The values of the column input
- * @param {String} heardeBackgroundColor The background color of the dataset
- * @param {String} headerBorderColor The border color of the dataset
- * @param {string[]} cellBackgroundColors The background colors of the datas inputs, random color if missing
- * @param {string[]} cellBorderColors The border color of the datas inputs, no color if missing
- */
- _addColumn: function (title, values, heardeBackgroundColor, headerBorderColor, cellBackgroundColors = [], cellBorderColors = []) {
- const firstRow = this.tableEl.querySelector('tr:first-child');
- const headerInput = this._makeCell('th', title, heardeBackgroundColor, headerBorderColor);
- firstRow.insertBefore(headerInput, firstRow.lastElementChild);
-
- this.tableEl.querySelectorAll('tr:not(:first-child):not(:last-child)').forEach((el, i) => {
- const newCell = this._makeCell('td', values ? values[i] : null, cellBackgroundColors[i] || this._randomColor(), cellBorderColors[i - 1]);
- el.insertBefore(newCell, el.lastElementChild);
- });
-
- const lastRow = this.tableEl.querySelector('tr:last-child');
- const removeButton = this._makeDeleteButton('o_we_matrix_remove_col');
- lastRow.appendChild(removeButton);
- },
- /**
- * Add a row to the matrix
- * The background color of the datas are random
- *
- * @private
- * @param {String} tilte The title of the row
- */
- _addRow: function (tilte) {
- const trEl = document.createElement('tr');
- trEl.appendChild(this._makeCell('th', tilte));
- this.tableEl.querySelectorAll('tr:first-child input').forEach(() => {
- trEl.appendChild(this._makeCell('td', null, this._randomColor()));
- });
- trEl.appendChild(this._makeDeleteButton('o_we_matrix_remove_row'));
- const tbody = this.tableEl.querySelector('tbody');
- tbody.insertBefore(trEl, tbody.lastElementChild);
- },
- /**
- * @private
- * @param {string} tag tag of the HTML Element (td/th)
- * @param {string} value The current value of the cell input
- * @param {string} backgroundColor The background Color of the data on the graph
- * @param {string} borderColor The border Color of the the data on the graph
- * @returns {HTMLElement}
- */
- _makeCell: function (tag, value, backgroundColor, borderColor) {
- const newEl = document.createElement(tag);
- const contentEl = document.createElement('input');
- contentEl.type = 'text';
- contentEl.value = value || '';
- if (backgroundColor) {
- contentEl.dataset.backgroundColor = backgroundColor;
- }
- if (borderColor) {
- contentEl.dataset.borderColor = borderColor;
- }
- newEl.appendChild(contentEl);
- return newEl;
- },
- /**
- * Display the remove button coresponding to the colIndex
- *
- * @private
- * @param {Int} colIndex Can be undefined, if so the last remove button of the column will be shown
- */
- _displayRemoveColButton: function (colIndex) {
- if (this._getColumnCount() > 1) {
- this._displayRemoveButton(colIndex, 'o_we_matrix_remove_col');
- }
- },
- /**
- * Display the remove button coresponding to the rowIndex
- *
- * @private
- * @param {Int} rowIndex Can be undefined, if so the last remove button of the row will be shown
- */
- _displayRemoveRowButton: function (rowIndex) {
- //Nbr of row minus header and button
- const rowCount = this.tableEl.rows.length - 2;
- if (rowCount > 1) {
- this._displayRemoveButton(rowIndex, 'o_we_matrix_remove_row');
- }
- },
- /**
- * @private
- * @param {Int} tdIndex Can be undefined, if so the last remove button will be shown
- * @param {String} btnClass Either o_we_matrix_remove_col or o_we_matrix_remove_row
- */
- _displayRemoveButton: function (tdIndex, btnClass) {
- const removeBtn = this.tableEl.querySelectorAll(`td we-button.${btnClass}`);
- removeBtn.forEach(el => el.style.display = ''); //hide all
- const index = tdIndex < removeBtn.length ? tdIndex : removeBtn.length - 1;
- removeBtn[index].style.display = 'inline-block';
- },
- /**
- * @private
- * @return {boolean}
- */
- _isPieChart: function () {
- return ['pie', 'doughnut'].includes(this.$target[0].dataset.type);
- },
- /**
- * Return the number of column minus header and button
- * @private
- * @return {integer}
- */
- _getColumnCount: function () {
- return this.tableEl.rows[0].cells.length - 2;
- },
- /**
- * Select the first data input
- *
- * @private
- */
- _setDefaultSelectedInput: function () {
- this.lastEditableSelectedInput = this.tableEl.querySelector('td input');
- if (this._isPieChart()) {
- this.colorPaletteSelectedInput = this.lastEditableSelectedInput;
- } else {
- this.colorPaletteSelectedInput = this.tableEl.querySelector('th input');
- }
- },
- /**
- * Return a random hexadecimal color.
- *
- * @private
- * @return {string}
- */
- _randomColor: function () {
- return '#' + ('00000' + (Math.random() * (1 << 24) | 0).toString(16)).slice(-6).toUpperCase();
- },
-
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
-
- /**
- * Used by colorPalette to retrieve the custom colors used on the chart
- * Make an array with all the custom colors used on the chart
- * and apply it to the onSuccess method provided by the trigger_up.
- *
- * @private
- */
- _onGetCustomColors: function (ev) {
- const data = JSON.parse(this.$target[0].dataset.data || '');
- let customColors = [];
- data.datasets.forEach(el => {
- if (this._isPieChart()) {
- customColors = customColors.concat(el.backgroundColor).concat(el.borderColor);
- } else {
- customColors.push(el.backgroundColor);
- customColors.push(el.borderColor);
- }
- });
- customColors = customColors.filter((el, i, array) => {
- return !this.style.getPropertyValue(`--${el}`) && array.indexOf(el) === i && el !== ''; // unique non class not transparent
- });
- ev.data.onSuccess(customColors);
- },
- /**
- * Add a row at the end of the matrix and display it's remove button
- * Choose the color of the column from the theme array or a random color if they are already used
- *
- * @private
- */
- _onAddColumnClick: function () {
- const usedColor = Array.from(this.tableEl.querySelectorAll('tr:first-child input')).map(el => el.dataset.backgroundColor);
- const color = this.themeArray.filter(el => !usedColor.includes(el))[0] || this._randomColor();
- this._addColumn(null, null, color, color);
- this._reloadGraph().then(() => {
- this._displayRemoveColButton();
- this.updateUI();
- });
- },
- /**
- * Add a column at the end of the matrix and display it's remove button
- *
- * @private
- */
- _onAddRowClick: function () {
- this._addRow();
- this._reloadGraph().then(() => {
- this._displayRemoveRowButton();
- this.updateUI();
- });
- },
- /**
- * Remove the column and show the remove button of the next column or the last if no next.
- *
- * @private
- * @param {Event} ev
- */
- _onRemoveColumnClick: function (ev) {
- const cell = ev.currentTarget.parentElement;
- const cellIndex = cell.cellIndex;
- this.tableEl.querySelectorAll('tr').forEach((el) => {
- el.children[cellIndex].remove();
- });
- this._displayRemoveColButton(cellIndex - 1);
- this._reloadGraph().then(() => {
- this.updateUI();
- });
- },
- /**
- * Remove the row and show the remove button of the next row or the last if no next.
- *
- * @private
- * @param {Event} ev
- */
- _onRemoveRowClick: function (ev) {
- const row = ev.currentTarget.parentElement.parentElement;
- const rowIndex = row.rowIndex;
- row.remove();
- this._displayRemoveRowButton(rowIndex - 1);
- this._reloadGraph().then(() => {
- this.updateUI();
- });
- },
- /**
- * @private
- * @param {Event} ev
- */
- _onMatrixInputFocusOut: function (ev) {
- // Sometimes, an input is focusout for internal reason (like an undo
- // recording) then focused again manually in the same JS stack
- // execution. In that case, the blur should not trigger an option
- // selection as the user did not leave the input. We thus defer the blur
- // handling to then check that the target is indeed still blurred before
- // executing the actual option selection.
- setTimeout(() => {
- if (ev.currentTarget === document.activeElement) {
- return;
- }
- this._reloadGraph();
- });
- },
- /**
- * Set the selected cell/header and display the related remove button
- *
- * @private
- * @param {Event} ev
- */
- _onMatrixInputFocus: function (ev) {
- this.lastEditableSelectedInput = ev.target;
- const col = ev.target.parentElement.cellIndex;
- const row = ev.target.parentElement.parentElement.rowIndex;
- if (this._isPieChart()) {
- this.colorPaletteSelectedInput = ev.target.parentNode.tagName === 'TD' ? ev.target : null;
- } else {
- this.colorPaletteSelectedInput = this.tableEl.querySelector(`tr:first-child th:nth-of-type(${col + 1}) input`);
- }
- if (col > 0) {
- this._displayRemoveColButton(col - 1);
- }
- if (row > 0) {
- this._displayRemoveRowButton(row - 1);
- }
- this.updateUI();
- },
-});
-
const InputUserValueWidget = options.userValueWidgetsRegistry['we-input'];
const UrlPickerUserValueWidget = InputUserValueWidget.extend({
custom_events: _.extend({}, InputUserValueWidget.prototype.custom_events || {}, {
diff --git a/addons/website/static/src/snippets/s_chart/000.js b/addons/website/static/src/snippets/s_chart/000.js
new file mode 100644
index 00000000000..865190fc81b
--- /dev/null
+++ b/addons/website/static/src/snippets/s_chart/000.js
@@ -0,0 +1,141 @@
+odoo.define('website.s_chart', function (require) {
+'use strict';
+
+const publicWidget = require('web.public.widget');
+
+const ChartWidget = publicWidget.Widget.extend({
+ selector: '.s_chart',
+ disabledInEditableMode: false,
+ jsLibs: [
+ '/web/static/lib/Chart/Chart.js',
+ ],
+
+ /**
+ * @override
+ * @param {Object} parent
+ * @param {Object} options The default value of the chartbar.
+ */
+ init: function (parent, options) {
+ this._super.apply(this, arguments);
+ this.style = window.getComputedStyle(document.documentElement);
+ },
+ /**
+ * @override
+ */
+ start: function () {
+ // Convert Theme colors to css color
+ const data = JSON.parse(this.el.dataset.data);
+ data.datasets.forEach(el => {
+ if (Array.isArray(el.backgroundColor)) {
+ el.backgroundColor = el.backgroundColor.map(el => this._convertToCssColor(el));
+ el.borderColor = el.borderColor.map(el => this._convertToCssColor(el));
+ } else {
+ el.backgroundColor = this._convertToCssColor(el.backgroundColor);
+ el.borderColor = this._convertToCssColor(el.borderColor);
+ }
+ el.borderWidth = this.el.dataset.borderWidth;
+ });
+
+ // Make chart data
+ const chartData = {
+ type: this.el.dataset.type,
+ data: data,
+ options: {
+ legend: {
+ display: this.el.dataset.legendPosition !== 'none',
+ position: this.el.dataset.legendPosition,
+ },
+ tooltips: {
+ enabled: this.el.dataset.tooltipDisplay === 'true',
+ },
+ title: {
+ display: !!this.el.dataset.title,
+ text: this.el.dataset.title,
+ },
+ },
+ };
+
+ // Add type specific options
+ if (this.el.dataset.type === 'radar') {
+ chartData.options.scale = {
+ ticks: {
+ beginAtZero: true,
+ }
+ };
+ } else if (['pie', 'doughnut'].includes(this.el.dataset.type)) {
+ chartData.options.tooltips.callbacks = {
+ label: (tooltipItem, data) => {
+ const label = data.datasets[tooltipItem.datasetIndex].label;
+ const secondLabel = data.labels[tooltipItem.index];
+ let final = label;
+ if (label) {
+ if (secondLabel) {
+ final = label + ' - ' + secondLabel;
+ }
+ } else if (secondLabel) {
+ final = secondLabel;
+ }
+ return final + ':' + data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
+ },
+ };
+ } else {
+ chartData.options.scales = {
+ xAxes: [{
+ stacked: this.el.dataset.stacked === 'true',
+ ticks: {
+ beginAtZero: true
+ },
+ }],
+ yAxes: [{
+ stacked: this.el.dataset.stacked === 'true',
+ ticks: {
+ beginAtZero: true
+ },
+ }],
+ };
+ }
+
+ // Disable animation in edit mode
+ if (this.editableMode) {
+ chartData.options.animation = {
+ duration: 0,
+ };
+ }
+
+ const canvas = this.el.querySelector('canvas');
+ this.chart = new window.Chart(canvas, chartData);
+ return this._super.apply(this, arguments);
+ },
+ /**
+ * @override
+ * Discard all library changes to reset the state of the Html.
+ */
+ destroy: function () {
+ if (this.chart) { // The widget can be destroyed before start has completed
+ this.chart.destroy();
+ this.el.querySelectorAll('.chartjs-size-monitor').forEach(el => el.remove());
+ }
+ this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {string} color A css color or theme color string
+ * @returns {string} Css color
+ */
+ _convertToCssColor: function (color) {
+ if (!color) {
+ return 'transparent';
+ }
+ return this.style.getPropertyValue(`--${color}`).trim() || color;
+ },
+});
+
+publicWidget.registry.chart = ChartWidget;
+
+return ChartWidget;
+});
diff --git a/addons/website/static/src/snippets/s_chart/options.js b/addons/website/static/src/snippets/s_chart/options.js
new file mode 100644
index 00000000000..a9429008fee
--- /dev/null
+++ b/addons/website/static/src/snippets/s_chart/options.js
@@ -0,0 +1,476 @@
+odoo.define('website.s_chart_options', function (require) {
+'use strict';
+
+var core = require('web.core');
+const ColorpickerDialog = require('web.ColorpickerDialog');
+var options = require('web_editor.snippets.options');
+
+var _t = core._t;
+
+options.registry.InnerChart = options.Class.extend({
+ custom_events: _.extend({}, options.Class.prototype.custom_events, {
+ 'get_custom_colors': '_onGetCustomColors',
+ }),
+ events: _.extend({}, options.Class.prototype.events, {
+ 'click we-button.add_column': '_onAddColumnClick',
+ 'click we-button.add_row': '_onAddRowClick',
+ 'click we-button.o_we_matrix_remove_col': '_onRemoveColumnClick',
+ 'click we-button.o_we_matrix_remove_row': '_onRemoveRowClick',
+ 'blur we-matrix input': '_onMatrixInputFocusOut',
+ 'focus we-matrix input': '_onMatrixInputFocus',
+ }),
+
+ /**
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ this.themeArray = ['alpha', 'beta', 'gamma', 'delta', 'epsilon'];
+ this.style = window.getComputedStyle(document.documentElement);
+ },
+ /**
+ * @override
+ */
+ start: function () {
+ this.backSelectEl = this.el.querySelector('[data-name="chart_bg_color_opt"]');
+ this.borderSelectEl = this.el.querySelector('[data-name="chart_border_color_opt"]');
+
+ // Build matrix content
+ this.tableEl = this.el.querySelector('we-matrix table');
+ const data = JSON.parse(this.$target[0].dataset.data);
+ data.labels.forEach(el => {
+ this._addRow(el);
+ });
+ data.datasets.forEach((el, i) => {
+ if (this._isPieChart()) {
+ // Add header colors in case the user changes the type of graph
+ const headerBackgroundColor = this.themeArray[i] || this._randomColor();
+ const headerBorderColor = this.themeArray[i] || this._randomColor();
+ this._addColumn(el.label, el.data, headerBackgroundColor, headerBorderColor, el.backgroundColor, el.borderColor);
+ } else {
+ this._addColumn(el.label, el.data, el.backgroundColor, el.borderColor);
+ }
+ });
+ this._displayRemoveColButton();
+ this._displayRemoveRowButton();
+ this._setDefaultSelectedInput();
+ return this._super(...arguments);
+ },
+ /**
+ * @override
+ */
+ updateUI: async function () {
+ // Selected input might not be in dom anymore if col/row removed
+ // Done before _super because _computeWidgetState of colorChange
+ if (!this.lastEditableSelectedInput.closest('table') || this.colorPaletteSelectedInput && !this.colorPaletteSelectedInput.closest('table')) {
+ this._setDefaultSelectedInput();
+ }
+
+ await this._super(...arguments);
+
+ // prevent the columns from becoming too small.
+ this.tableEl.classList.toggle('o_we_matrix_five_col', this.tableEl.querySelectorAll('tr:first-child th').length > 5);
+
+ this.backSelectEl.querySelector('we-title').textContent = this._isPieChart() ? _t("Data Background Color") : _t("Dataset Background Color");
+ this.borderSelectEl.querySelector('we-title').textContent = this._isPieChart() ? _t("Data Border Color") : _t("Dataset Border Color");
+
+ // Dataset/Cell color
+ this.tableEl.querySelectorAll('input').forEach(el => el.style.border = '');
+ const selector = this._isPieChart() ? 'td input' : 'tr:first-child input';
+ this.tableEl.querySelectorAll(selector).forEach(el => {
+ const color = el.dataset.backgroundColor || el.dataset.borderColor;
+ if (color) {
+ el.style.border = '2px solid';
+ el.style.borderColor = ColorpickerDialog.isCSSColor(color) ? color : this.style.getPropertyValue(`--${color}`).trim();
+ }
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Options
+ //--------------------------------------------------------------------------
+
+ /**
+ * Set the color on the selected input.
+ */
+ colorChange: async function (previewMode, widgetValue, params) {
+ if (widgetValue) {
+ this.colorPaletteSelectedInput.dataset[params.attributeName] = widgetValue;
+ } else {
+ delete this.colorPaletteSelectedInput.dataset[params.attributeName];
+ }
+ await this._reloadGraph();
+ // To focus back the input that is edited we have to wait for the color
+ // picker to be fully reloaded.
+ await new Promise(resolve => setTimeout(() => {
+ this.lastEditableSelectedInput.focus();
+ resolve();
+ }));
+ },
+ /**
+ * @override
+ */
+ selectDataAttribute: async function (previewMode, widgetValue, params) {
+ await this._super(...arguments);
+ // Data might change if going from or to a pieChart.
+ if (params.attributeName === 'type') {
+ this._setDefaultSelectedInput();
+ await this._reloadGraph();
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ _computeWidgetState: function (methodName, params) {
+ if (methodName === 'colorChange') {
+ return this.colorPaletteSelectedInput && this.colorPaletteSelectedInput.dataset[params.attributeName] || '';
+ }
+ return this._super(...arguments);
+ },
+ /**
+ * @override
+ */
+ _computeWidgetVisibility: function (widgetName, params) {
+ switch (widgetName) {
+ case 'stacked_chart_opt': {
+ return this._getColumnCount() > 1;
+ }
+ case 'chart_bg_color_opt':
+ case 'chart_border_color_opt': {
+ return !!this.colorPaletteSelectedInput;
+ }
+ }
+ return this._super(...arguments);
+ },
+ /**
+ * Sets and reloads the data on the canvas if it has changed.
+ * Used in matrix related method.
+ *
+ * @private
+ */
+ _reloadGraph: async function () {
+ const jsonValue = this._matrixToChartData();
+ if (this.$target[0].dataset.data !== jsonValue) {
+ this.$target[0].dataset.data = jsonValue;
+ await this._refreshPublicWidgets();
+ }
+ },
+ /**
+ * Return a stringifyed chart.js data object from the matrix
+ * Pie charts have one color per data while other charts have one color per dataset.
+ *
+ * @private
+ */
+ _matrixToChartData: function () {
+ const data = {
+ labels: [],
+ datasets: [],
+ };
+ this.tableEl.querySelectorAll('tr:first-child input').forEach(el => {
+ data.datasets.push({
+ label: el.value || '',
+ data: [],
+ backgroundColor: this._isPieChart() ? [] : el.dataset.backgroundColor || '',
+ borderColor: this._isPieChart() ? [] : el.dataset.borderColor || '',
+ });
+ });
+ this.tableEl.querySelectorAll('tr:not(:first-child):not(:last-child)').forEach((el) => {
+ const title = el.querySelector('th input').value || '';
+ data.labels.push(title);
+ el.querySelectorAll('td input').forEach((el, i) => {
+ data.datasets[i].data.push(el.value || 0);
+ if (this._isPieChart()) {
+ data.datasets[i].backgroundColor.push(el.dataset.backgroundColor || '');
+ data.datasets[i].borderColor.push(el.dataset.borderColor || '');
+ }
+ });
+ });
+ return JSON.stringify(data);
+ },
+ /**
+ * Return a td containing a we-button with minus icon
+ *
+ * @param {...string} classes Classes to add to the we-button
+ * @returns {HTMLElement}
+ */
+ _makeDeleteButton: function (...classes) {
+ const rmbuttonEl = options.buildElement('we-button', null, {
+ classes: ['fa', 'fa-fw', 'fa-minus', ...classes],
+ });
+ const newEl = document.createElement('td');
+ newEl.appendChild(rmbuttonEl);
+ return newEl;
+ },
+ /**
+ * Add a column to the matrix
+ * The th (dataset label) of a column hold the colors for the entire dataset if the graph is not a pie chart
+ * If the graph is a pie chart the color of the td (data) are used.
+ *
+ * @private
+ * @param {String} title The title of the column
+ * @param {Array} values The values of the column input
+ * @param {String} heardeBackgroundColor The background color of the dataset
+ * @param {String} headerBorderColor The border color of the dataset
+ * @param {string[]} cellBackgroundColors The background colors of the datas inputs, random color if missing
+ * @param {string[]} cellBorderColors The border color of the datas inputs, no color if missing
+ */
+ _addColumn: function (title, values, heardeBackgroundColor, headerBorderColor, cellBackgroundColors = [], cellBorderColors = []) {
+ const firstRow = this.tableEl.querySelector('tr:first-child');
+ const headerInput = this._makeCell('th', title, heardeBackgroundColor, headerBorderColor);
+ firstRow.insertBefore(headerInput, firstRow.lastElementChild);
+
+ this.tableEl.querySelectorAll('tr:not(:first-child):not(:last-child)').forEach((el, i) => {
+ const newCell = this._makeCell('td', values ? values[i] : null, cellBackgroundColors[i] || this._randomColor(), cellBorderColors[i - 1]);
+ el.insertBefore(newCell, el.lastElementChild);
+ });
+
+ const lastRow = this.tableEl.querySelector('tr:last-child');
+ const removeButton = this._makeDeleteButton('o_we_matrix_remove_col');
+ lastRow.appendChild(removeButton);
+ },
+ /**
+ * Add a row to the matrix
+ * The background color of the datas are random
+ *
+ * @private
+ * @param {String} tilte The title of the row
+ */
+ _addRow: function (tilte) {
+ const trEl = document.createElement('tr');
+ trEl.appendChild(this._makeCell('th', tilte));
+ this.tableEl.querySelectorAll('tr:first-child input').forEach(() => {
+ trEl.appendChild(this._makeCell('td', null, this._randomColor()));
+ });
+ trEl.appendChild(this._makeDeleteButton('o_we_matrix_remove_row'));
+ const tbody = this.tableEl.querySelector('tbody');
+ tbody.insertBefore(trEl, tbody.lastElementChild);
+ },
+ /**
+ * @private
+ * @param {string} tag tag of the HTML Element (td/th)
+ * @param {string} value The current value of the cell input
+ * @param {string} backgroundColor The background Color of the data on the graph
+ * @param {string} borderColor The border Color of the the data on the graph
+ * @returns {HTMLElement}
+ */
+ _makeCell: function (tag, value, backgroundColor, borderColor) {
+ const newEl = document.createElement(tag);
+ const contentEl = document.createElement('input');
+ contentEl.type = 'text';
+ contentEl.value = value || '';
+ if (backgroundColor) {
+ contentEl.dataset.backgroundColor = backgroundColor;
+ }
+ if (borderColor) {
+ contentEl.dataset.borderColor = borderColor;
+ }
+ newEl.appendChild(contentEl);
+ return newEl;
+ },
+ /**
+ * Display the remove button coresponding to the colIndex
+ *
+ * @private
+ * @param {Int} colIndex Can be undefined, if so the last remove button of the column will be shown
+ */
+ _displayRemoveColButton: function (colIndex) {
+ if (this._getColumnCount() > 1) {
+ this._displayRemoveButton(colIndex, 'o_we_matrix_remove_col');
+ }
+ },
+ /**
+ * Display the remove button coresponding to the rowIndex
+ *
+ * @private
+ * @param {Int} rowIndex Can be undefined, if so the last remove button of the row will be shown
+ */
+ _displayRemoveRowButton: function (rowIndex) {
+ //Nbr of row minus header and button
+ const rowCount = this.tableEl.rows.length - 2;
+ if (rowCount > 1) {
+ this._displayRemoveButton(rowIndex, 'o_we_matrix_remove_row');
+ }
+ },
+ /**
+ * @private
+ * @param {Int} tdIndex Can be undefined, if so the last remove button will be shown
+ * @param {String} btnClass Either o_we_matrix_remove_col or o_we_matrix_remove_row
+ */
+ _displayRemoveButton: function (tdIndex, btnClass) {
+ const removeBtn = this.tableEl.querySelectorAll(`td we-button.${btnClass}`);
+ removeBtn.forEach(el => el.style.display = ''); //hide all
+ const index = tdIndex < removeBtn.length ? tdIndex : removeBtn.length - 1;
+ removeBtn[index].style.display = 'inline-block';
+ },
+ /**
+ * @private
+ * @return {boolean}
+ */
+ _isPieChart: function () {
+ return ['pie', 'doughnut'].includes(this.$target[0].dataset.type);
+ },
+ /**
+ * Return the number of column minus header and button
+ * @private
+ * @return {integer}
+ */
+ _getColumnCount: function () {
+ return this.tableEl.rows[0].cells.length - 2;
+ },
+ /**
+ * Select the first data input
+ *
+ * @private
+ */
+ _setDefaultSelectedInput: function () {
+ this.lastEditableSelectedInput = this.tableEl.querySelector('td input');
+ if (this._isPieChart()) {
+ this.colorPaletteSelectedInput = this.lastEditableSelectedInput;
+ } else {
+ this.colorPaletteSelectedInput = this.tableEl.querySelector('th input');
+ }
+ },
+ /**
+ * Return a random hexadecimal color.
+ *
+ * @private
+ * @return {string}
+ */
+ _randomColor: function () {
+ return '#' + ('00000' + (Math.random() * (1 << 24) | 0).toString(16)).slice(-6).toUpperCase();
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Used by colorPalette to retrieve the custom colors used on the chart
+ * Make an array with all the custom colors used on the chart
+ * and apply it to the onSuccess method provided by the trigger_up.
+ *
+ * @private
+ */
+ _onGetCustomColors: function (ev) {
+ const data = JSON.parse(this.$target[0].dataset.data || '');
+ let customColors = [];
+ data.datasets.forEach(el => {
+ if (this._isPieChart()) {
+ customColors = customColors.concat(el.backgroundColor).concat(el.borderColor);
+ } else {
+ customColors.push(el.backgroundColor);
+ customColors.push(el.borderColor);
+ }
+ });
+ customColors = customColors.filter((el, i, array) => {
+ return !this.style.getPropertyValue(`--${el}`) && array.indexOf(el) === i && el !== ''; // unique non class not transparent
+ });
+ ev.data.onSuccess(customColors);
+ },
+ /**
+ * Add a row at the end of the matrix and display it's remove button
+ * Choose the color of the column from the theme array or a random color if they are already used
+ *
+ * @private
+ */
+ _onAddColumnClick: function () {
+ const usedColor = Array.from(this.tableEl.querySelectorAll('tr:first-child input')).map(el => el.dataset.backgroundColor);
+ const color = this.themeArray.filter(el => !usedColor.includes(el))[0] || this._randomColor();
+ this._addColumn(null, null, color, color);
+ this._reloadGraph().then(() => {
+ this._displayRemoveColButton();
+ this.updateUI();
+ });
+ },
+ /**
+ * Add a column at the end of the matrix and display it's remove button
+ *
+ * @private
+ */
+ _onAddRowClick: function () {
+ this._addRow();
+ this._reloadGraph().then(() => {
+ this._displayRemoveRowButton();
+ this.updateUI();
+ });
+ },
+ /**
+ * Remove the column and show the remove button of the next column or the last if no next.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onRemoveColumnClick: function (ev) {
+ const cell = ev.currentTarget.parentElement;
+ const cellIndex = cell.cellIndex;
+ this.tableEl.querySelectorAll('tr').forEach((el) => {
+ el.children[cellIndex].remove();
+ });
+ this._displayRemoveColButton(cellIndex - 1);
+ this._reloadGraph().then(() => {
+ this.updateUI();
+ });
+ },
+ /**
+ * Remove the row and show the remove button of the next row or the last if no next.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onRemoveRowClick: function (ev) {
+ const row = ev.currentTarget.parentElement.parentElement;
+ const rowIndex = row.rowIndex;
+ row.remove();
+ this._displayRemoveRowButton(rowIndex - 1);
+ this._reloadGraph().then(() => {
+ this.updateUI();
+ });
+ },
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onMatrixInputFocusOut: function (ev) {
+ // Sometimes, an input is focusout for internal reason (like an undo
+ // recording) then focused again manually in the same JS stack
+ // execution. In that case, the blur should not trigger an option
+ // selection as the user did not leave the input. We thus defer the blur
+ // handling to then check that the target is indeed still blurred before
+ // executing the actual option selection.
+ setTimeout(() => {
+ if (ev.currentTarget === document.activeElement) {
+ return;
+ }
+ this._reloadGraph();
+ });
+ },
+ /**
+ * Set the selected cell/header and display the related remove button
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onMatrixInputFocus: function (ev) {
+ this.lastEditableSelectedInput = ev.target;
+ const col = ev.target.parentElement.cellIndex;
+ const row = ev.target.parentElement.parentElement.rowIndex;
+ if (this._isPieChart()) {
+ this.colorPaletteSelectedInput = ev.target.parentNode.tagName === 'TD' ? ev.target : null;
+ } else {
+ this.colorPaletteSelectedInput = this.tableEl.querySelector(`tr:first-child th:nth-of-type(${col + 1}) input`);
+ }
+ if (col > 0) {
+ this._displayRemoveColButton(col - 1);
+ }
+ if (row > 0) {
+ this._displayRemoveRowButton(row - 1);
+ }
+ this.updateUI();
+ },
+});
+});
diff --git a/addons/website/views/assets.xml b/addons/website/views/assets.xml
index 39137714e4c..5b2d8c6c0c6 100644
--- a/addons/website/views/assets.xml
+++ b/addons/website/views/assets.xml
@@ -111,6 +111,7 @@
+
diff --git a/addons/website/views/snippets/s_chart.xml b/addons/website/views/snippets/s_chart.xml
new file mode 100644
index 00000000000..574e78a9c52
--- /dev/null
+++ b/addons/website/views/snippets/s_chart.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
A Chart Title
+
+
+
+
+
+
+
+
+
+ Bar Vertical
+ Bar Horizontal
+ Line
+ Pie
+ Doughnut
+ Radar
+
+
+
+ None
+ Top
+ Left
+ Bottom
+ Right
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/temp_xml b/addons/website/views/snippets/snippets.xml
similarity index 89%
rename from temp_xml
rename to addons/website/views/snippets/snippets.xml
index b762b2a235f..e7abb71f560 100644
--- a/temp_xml
+++ b/addons/website/views/snippets/snippets.xml
@@ -1,24 +1,6 @@
-
-
-
A Chart Title
-
-
-
-
@@ -576,52 +558,5 @@
data-target="> .container, > .container-fluid">
-
-
-
-
-
- Bar Vertical
- Bar Horizontal
- Line
- Pie
- Doughnut
- Radar
-
-
-
- None
- Top
- Left
- Bottom
- Right
-
-
-
-
-
-
-
-
-