From aba31e9f2d8a44ce1586403f2a621a6caeed57b4 Mon Sep 17 00:00:00 2001 From: Younn Olivier Date: Mon, 12 Apr 2021 13:32:29 +0000 Subject: [PATCH] [IMP] web_editor, website: add connector options to steps snippet A new option StepsConnector is added to the steps snippets, allowing to pick the type of the steps connector and its color. The option listens to changes in the UI or in the columns size to recompute the width and position of the arrows. The width of the connector is computed based on the widths of the current step and the next step, which are defined in bootstrap columns. The s_process_steps snippet CSS version is now 001. task-2463009 closes odoo/odoo#77282 Signed-off-by: Quentin Smetz (qsm) Co-authored-by: Matthieu Stockbauer --- .../static/src/js/editor/snippets.options.js | 16 ++ addons/website/__manifest__.py | 1 + .../src/snippets/s_process_steps/000.scss | 2 +- .../src/snippets/s_process_steps/001.scss | 58 +++++++ .../src/snippets/s_process_steps/options.js | 161 ++++++++++++++++++ .../views/snippets/s_process_steps.xml | 73 ++++++-- 6 files changed, 293 insertions(+), 18 deletions(-) create mode 100644 addons/website/static/src/snippets/s_process_steps/001.scss create mode 100644 addons/website/static/src/snippets/s_process_steps/options.js diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js index 9223a88d37a..6f14c8cf48d 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -4398,6 +4398,10 @@ registry['sizing_x'] = registry.sizing.extend({ this.$target.addClass('offset-lg-' + offset); } } + this.trigger_up('option_update', { + optionName: 'StepsConnector', + name: 'change_column_size', + }); this._super.apply(this, arguments); }, }); @@ -4518,6 +4522,10 @@ registry.layout_column = SnippetOptionWidget.extend({ } else if (previousNbColumns === 0) { this.trigger_up('activate_snippet', {$snippet: this.$('> .row').children().first()}); } + this.trigger_up('option_update', { + optionName: 'StepsConnector', + name: 'change_columns', + }); }, //-------------------------------------------------------------------------- @@ -4650,6 +4658,10 @@ registry.SnippetMove = SnippetOptionWidget.extend({ duration: 550, }); } + this.trigger_up('option_update', { + optionName: 'StepsConnector', + name: 'move_snippet', + }); }, }); @@ -6621,6 +6633,10 @@ registry.ContainerWidth = SnippetOptionWidget.extend({ } else if (previewMode) { this.$target.addClass('o_container_preview'); } + this.trigger_up('option_update', { + optionName: 'StepsConnector', + name: 'change_container_width', + }); }, }); diff --git a/addons/website/__manifest__.py b/addons/website/__manifest__.py index 45ab701bf5e..a7b7e338b3d 100644 --- a/addons/website/__manifest__.py +++ b/addons/website/__manifest__.py @@ -205,6 +205,7 @@ 'website/static/src/snippets/s_embed_code/options.js', 'website/static/src/snippets/s_website_form/options.js', 'website/static/src/snippets/s_searchbar/options.js', + 'website/static/src/snippets/s_process_steps/options.js', 'website/static/src/js/editor/wysiwyg.js', 'website/static/src/js/editor/widget_link.js', 'website/static/src/js/widgets/media.js', diff --git a/addons/website/static/src/snippets/s_process_steps/000.scss b/addons/website/static/src/snippets/s_process_steps/000.scss index 3daae1d92c9..8642f2c0cac 100644 --- a/addons/website/static/src/snippets/s_process_steps/000.scss +++ b/addons/website/static/src/snippets/s_process_steps/000.scss @@ -1,4 +1,4 @@ -.s_process_steps { +.s_process_steps:not([data-vcss]) { .s_process_step_icon { margin: $grid-gutter-width 0; span { diff --git a/addons/website/static/src/snippets/s_process_steps/001.scss b/addons/website/static/src/snippets/s_process_steps/001.scss new file mode 100644 index 00000000000..b0686c7c500 --- /dev/null +++ b/addons/website/static/src/snippets/s_process_steps/001.scss @@ -0,0 +1,58 @@ +.s_process_steps[data-vcss='001'] { + $process-step-icon-size: 5rem; + + .s_process_step { + .s_process_step_icon { + position: relative; + margin: $grid-gutter-width 0; + + .fa { + display: block; + height: $process-step-icon-size; + width: $process-step-icon-size; + line-height: $process-step-icon-size; + } + } + + .s_process_step_content { + padding: 0 $grid-gutter-width/2; + text-align: center; + } + + .s_process_step_connector { + position: absolute; + z-index: 1; + height: $process-step-icon-size; + width: calc(100% - #{$process-step-icon-size}); + left: calc(50% + #{$process-step-icon-size / 2}); + margin: $grid-gutter-width 0; + + path { + stroke: gray('600'); + stroke-width: 2; + fill: transparent; + } + } + + &:last-child .s_process_step_connector { + display: none; + } + } + + &.s_process_steps_connector_curved_arrow { + .s_process_step:nth-child(odd) .s_process_step_connector { + transform: scale(1, -1); + } + } + + .s_process_steps_arrow_head path { + fill: gray('600'); + stroke: transparent; + } + + @include media-breakpoint-down(md) { + .s_process_step_connector { + display: none; + } + } +} diff --git a/addons/website/static/src/snippets/s_process_steps/options.js b/addons/website/static/src/snippets/s_process_steps/options.js new file mode 100644 index 00000000000..c62160a351c --- /dev/null +++ b/addons/website/static/src/snippets/s_process_steps/options.js @@ -0,0 +1,161 @@ +/** @odoo-module **/ + +import options from 'web_editor.snippets.options'; +import weUtils from 'web_editor.utils'; + +options.registry.StepsConnector = options.Class.extend({ + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * @override + */ + selectClass: function (previewMode, value, params) { + this._super(...arguments); + if (params.name === 'connector_type') { + this._reloadConnectors(); + let markerEnd = ''; + if (['s_process_steps_connector_arrow', 's_process_steps_connector_curved_arrow'].includes(value)) { + const arrowHeadEl = this.$target[0].querySelector('.s_process_steps_arrow_head'); + // The arrowhead id is set here so that they are different per snippet. + if (!arrowHeadEl.id) { + arrowHeadEl.id = 's_process_steps_arrow_head' + Date.now(); + } + markerEnd = `url(#${arrowHeadEl.id})`; + } + this.$target[0].querySelectorAll('.s_process_step_connector path').forEach(path => path.setAttribute('marker-end', markerEnd)); + } + }, + /** + * Changes arrow heads' fill color. + * + * @see this.selectClass for parameters + */ + changeColor(previewMode, widgetValue, params) { + const htmlPropColor = weUtils.getCSSVariableValue(widgetValue); + const arrowHeadEl = this.$target[0].closest('.s_process_steps').querySelector('.s_process_steps_arrow_head'); + arrowHeadEl.querySelector('path').style.fill = htmlPropColor || widgetValue; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + notify(name) { + if (['change_column_size', 'change_container_width', 'change_columns', 'move_snippet'].includes(name)) { + this._reloadConnectors(); + } else { + this._super(...arguments); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Width and position of the connectors should be updated when one of the + * steps is modified. + * + * @private + */ + _reloadConnectors() { + const possibleTypes = this._requestUserValueWidgets('connector_type')[0].getMethodsParams().optionsPossibleValues.selectClass; + const type = possibleTypes.find(possibleType => possibleType && this.$target[0].classList.contains(possibleType)) || ''; + const steps = this.$target[0].querySelectorAll('.s_process_step'); + + for (let i = 0; i < steps.length - 1; i++) { + const connectorEl = steps[i].querySelector('.s_process_step_connector'); + const stepMainElementRect = this._getStepMainElementRect(steps[i]); + const nextStepMainElementRect = this._getStepMainElementRect(steps[i + 1]); + const stepSize = this._getStepColSize(steps[i]); + const nextStepSize = this._getStepColSize(steps[i + 1]); + const nextStepPadding = this._getStepColPadding(steps[i + 1]); + + connectorEl.style.left = `calc(50% + ${stepMainElementRect.width / 2}px)`; + connectorEl.style.height = `${stepMainElementRect.height}px`; + connectorEl.style.width = `calc(${100 * (stepSize / 2 + nextStepPadding + nextStepSize / 2) / stepSize}% - ${stepMainElementRect.width / 2}px - ${nextStepMainElementRect.width / 2}px)`; + connectorEl.classList.toggle('d-none', nextStepMainElementRect.top > stepMainElementRect.bottom); + const {height, width} = connectorEl.getBoundingClientRect(); + connectorEl.setAttribute('viewBox', `0 0 ${width} ${height}`); + connectorEl.querySelector('path').setAttribute('d', this._getPath(type, width, height)); + } + }, + /** + * Returns the step's icon or content bounding rectangle. + * + * @private + * @param {HTMLElement} + * @returns {object} + */ + _getStepMainElementRect(stepEl) { + const iconEl = stepEl.querySelector('i'); + if (iconEl) { + return iconEl.getBoundingClientRect(); + } + const contentEls = stepEl.querySelectorAll('.s_process_step_content > *'); + // If there is no icon, the biggest text bloc in the content container + // will be chosen. + if (contentEls.length) { + const contentRects = [...contentEls].map(contentEl => { + const range = document.createRange(); + range.selectNodeContents(contentEl); + return range.getBoundingClientRect(); + }); + return contentRects.reduce((previous, current) => { + return current.width > previous.width ? current : previous; + }); + } + return {}; + }, + /** + * Returns the size of the step, as a number of bootstrap lg-col. + * + * @private + * @param {HTMLElement} + * @returns {integer} + */ + _getStepColSize(stepEl) { + const colClass = stepEl.className.split(' ').find(cl => cl.startsWith('col-lg')); + return parseInt(colClass[colClass.length - 1]); + }, + /** + * Returns the padding of the step, as a number of bootstrap lg-col. + * + * @private + * @param {HTMLElement} + * @returns {integer} + */ + _getStepColPadding(stepEl) { + const paddingClass = stepEl.className.split(' ').find(cl => cl.startsWith('offset-lg')); + return paddingClass ? parseInt(paddingClass[paddingClass.length - 1]) : 0; + }, + /** + * Returns the svg path based on the type of connector. + * + * @private + * @param {string} type + * @param {integer} width + * @param {integer} height + * @returns {string} + */ + _getPath(type, width, height) { + const hHeight = height / 2; + switch (type) { + case 's_process_steps_connector_line': { + return `M 0 ${hHeight} L ${width} ${hHeight}`; + } + case 's_process_steps_connector_arrow': { + return `M ${0.05 * width} ${hHeight} L ${0.95 * width - 6} ${hHeight}`; + } + case 's_process_steps_connector_curved_arrow': { + return `M ${0.05 * width} ${hHeight * 1.2} Q ${width / 2} ${hHeight * 1.8}, ${0.95 * width - 6} ${hHeight * 1.2}`; + } + } + }, +}); diff --git a/addons/website/views/snippets/s_process_steps.xml b/addons/website/views/snippets/s_process_steps.xml index c0690367c05..d3fc0ae7397 100644 --- a/addons/website/views/snippets/s_process_steps.xml +++ b/addons/website/views/snippets/s_process_steps.xml @@ -2,49 +2,60 @@ + + Process steps 000 SCSS web.assets_frontend website/static/src/snippets/s_process_steps/000.scss + + + + + Process steps 001 SCSS + web.assets_frontend + website/static/src/snippets/s_process_steps/001.scss