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