diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js b/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js
index 04a21c79378..a4881e1b139 100644
--- a/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js
+++ b/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js
@@ -158,16 +158,32 @@ const ColorPaletteWidget = Widget.extend({
return ColorpickerDialog.normalizeCSSColor(el.style.backgroundColor);
})
));
+ this.trigger_up('get_custom_colors', {
+ onSuccess: (colors) => {
+ colors.forEach(color => {
+ this._addCustomColor(existingColors, color);
+ });
+ },
+ });
_.each(this.options.$editable.find('[style*="color"]'), el => {
for (const colorProp of ['color', 'backgroundColor']) {
- const color = ColorpickerDialog.normalizeCSSColor(el.style[colorProp]);
- if (color && !existingColors.has(color)) {
- this._addCustomColorButton(color);
- existingColors.add(color);
- }
+ this._addCustomColor(existingColors, el.style[colorProp]);
}
});
},
+ /**
+ * Add the color to the custom color section if it is not in the existingColors.
+ *
+ * @param {string[]} existingColors Colors currently in the colorpicker
+ * @param {string} color Color to add to the cuustom colors
+ */
+ _addCustomColor: function (existingColors, color) {
+ const normColor = ColorpickerDialog.normalizeCSSColor(color);
+ if (color && !existingColors.has(normColor)) {
+ this._addCustomColorButton(normColor);
+ existingColors.add(normColor);
+ }
+ },
/**
* Add a custom button in the coresponding section.
*
diff --git a/addons/web_editor/static/src/scss/wysiwyg_snippets.scss b/addons/web_editor/static/src/scss/wysiwyg_snippets.scss
index 019fb32fd3f..6f76811a704 100644
--- a/addons/web_editor/static/src/scss/wysiwyg_snippets.scss
+++ b/addons/web_editor/static/src/scss/wysiwyg_snippets.scss
@@ -408,6 +408,47 @@ body.editor_enable.editor_has_snippets {
}
}
+ // Chart Snippet
+ we-matrix {
+ overflow-y: auto;
+
+ table {
+ table-layout: fixed;
+ width: 100%;
+
+ td, th {
+ text-align: center;
+ we-button {
+ display: inline-block;
+ color: white;
+ height: 100%;
+
+ &.o_we_matrix_remove_col, &.o_we_matrix_remove_row {
+ display: none;
+ }
+ }
+ input {
+ border: 1px solid var(--o-we-bg-color-dark);
+ background-color: var(--o-we-bg-color-dark);
+ color: var(--o-we-color);
+ font-size: 12px;
+ width: 100%;
+ }
+ &:last-child {
+ width: 28px;
+ }
+ }
+ &.o_we_matrix_five_col {
+ width: auto;
+ td, th {
+ input {
+ width: 43px;
+ }
+ }
+ }
+ }
+ }
+
//----------------------------------------------------------------------
// Layout Utils
//----------------------------------------------------------------------
diff --git a/addons/website/static/src/img/snippets_thumbs/s_chart.png b/addons/website/static/src/img/snippets_thumbs/s_chart.png
new file mode 100644
index 00000000000..aa1d88d85d8
Binary files /dev/null and b/addons/website/static/src/img/snippets_thumbs/s_chart.png differ
diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js
index 8cb4da6efe9..ff90fa478e8 100644
--- a/addons/website/static/src/js/content/snippets.animation.js
+++ b/addons/website/static/src/js/content/snippets.animation.js
@@ -1561,6 +1561,130 @@ registry.countdown = 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
+ },
+ }],
+ };
+ }
+ 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/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js
index 75a83a1ae15..3a0cfa72c6d 100644
--- a/addons/website/static/src/js/editor/snippets.options.js
+++ b/addons/website/static/src/js/editor/snippets.options.js
@@ -2,6 +2,7 @@ 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');
@@ -2072,6 +2073,459 @@ 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 () {
+ // Get the 2 color pickers to hide in updateUI
+ this.backSelectEl = this.el.querySelector('we-select[data-attribute-name="backgroundColor"]');
+ this.borderSelectEl = this.el.querySelector('we-select[data-attribute-name="borderColor"]');
+
+ // 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: 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();
+ }
+ 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.el.querySelector('[data-attribute-name="stacked"]').classList.toggle('d-none', !this._isStackableChart() || this._getColumnCount() === 1);
+ // Disable color on inputs that don't use them
+ const notDisplayed = !this.colorPaletteSelectedInput;
+ this.backSelectEl.classList.toggle('d-none', notDisplayed);
+ this.borderSelectEl.classList.toggle('d-none', notDisplayed);
+ 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: function (previewMode, widgetValue, params) {
+ if (widgetValue) {
+ this.colorPaletteSelectedInput.dataset[params.attributeName] = widgetValue;
+ } else {
+ delete this.colorPaletteSelectedInput.dataset[params.attributeName];
+ }
+ this._reloadGraph();
+ // To focus back the input that is edited we have to wait for the color picker to be fully reloaded.
+ setTimeout(() => this.lastEditableSelectedInput.focus());
+ },
+ /**
+ * @override
+ */
+ selectDataAttribute: function (previewMode, widgetValue, params) {
+ this._super(...arguments);
+ // Data might change if going from or to a pieChart.
+ if (params.attributeName === 'type') {
+ this._setDefaultSelectedInput();
+ this._reloadGraph();
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ _computeWidgetState: function (methodName, params) {
+ if (methodName === 'colorChange') {
+ return this.colorPaletteSelectedInput && this.colorPaletteSelectedInput.dataset[params.attributeName] || '';
+ }
+ return this._super(...arguments);
+ },
+ /**
+ * Sets and reloads the data on the canvas if it has changed.
+ * Used in matrix related method.
+ *
+ * @private
+ */
+ _reloadGraph: function () {
+ const jsonValue = this._matrixToChartData();
+ if (this.$target[0].dataset.data !== jsonValue) {
+ this.$target[0].dataset.data = jsonValue;
+ 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);
+ },
+ /**
+ * @private
+ * @return {boolean}
+ */
+ _isStackableChart: function () {
+ return ['bar', 'horizontalBar'].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();
+ 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();
+ 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();
+ 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();
+ 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/views/snippets.xml b/addons/website/views/snippets.xml
index 9fc9117e499..c29dcbcf877 100644
--- a/addons/website/views/snippets.xml
+++ b/addons/website/views/snippets.xml
@@ -747,6 +747,24 @@
+
+
+
A Chart Title
+
+
+
+
@@ -965,6 +983,7 @@
+
@@ -1345,8 +1364,8 @@
+
+
+
+
+
+ Bar Vertical
+ Bar Horizontal
+ Line
+ Pie
+ Doughnut
+ Radar
+
+
+
+ None
+ Top
+ Left
+ Bottom
+ Right
+
+
+
+
+
+
+
+
+