[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) <qsm@odoo.com>
Co-authored-by: Matthieu Stockbauer <tsm@odoo.com>
This commit is contained in:
Younn Olivier
2021-11-18 16:00:40 +00:00
committed by qsm-odoo
co-authored by Matthieu Stockbauer
parent 48f8aad0b5
commit aba31e9f2d
6 changed files with 293 additions and 18 deletions
@@ -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',
});
},
});
+1
View File
@@ -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',
@@ -1,4 +1,4 @@
.s_process_steps {
.s_process_steps:not([data-vcss]) {
.s_process_step_icon {
margin: $grid-gutter-width 0;
span {
@@ -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;
}
}
}
@@ -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}`;
}
}
},
});
@@ -2,49 +2,60 @@
<odoo>
<template name="Steps" id="s_process_steps">
<section class="s_process_steps pt24 pb24">
<section class="s_process_steps pt24 pb24 s_process_steps_connector_line" data-vcss="001">
<svg class="s_process_step_svg_defs position-absolute">
<defs>
<marker class="s_process_steps_arrow_head" markerWidth="15" markerHeight="10" refX="6" refY="6" orient="auto">
<path d="M 2,2 L10,6 L2,10 L6,6 L2,2" vector-effect="non-scaling-size"/>
</marker>
</defs>
</svg>
<div class="container">
<div class="row no-gutters">
<div class="col-lg-3 s_process_step pt24 pb24">
<svg class="s_process_step_connector" viewBox="0 0 100 20" preserveAspectRatio="none">
<path d="M 0 10 L 100 10" vector-effect="non-scaling-stroke"/>
</svg>
<div class="s_process_step_icon">
<span>
<i class="fa fa-shopping-basket fa-2x mx-auto rounded-circle bg-primary"/>
</span>
<i class="fa fa-shopping-basket fa-2x mx-auto rounded-circle bg-primary"/>
</div>
<div class="s_process_step_content text-center">
<div class="s_process_step_content">
<h2>Add to cart</h2>
<p>Let your customers follow <br/>and understand your process.</p>
</div>
</div>
<div class="col-lg-3 s_process_step pt24 pb24">
<svg class="s_process_step_connector" viewBox="0 0 100 20" preserveAspectRatio="none">
<path d="M 0 10 L 100 10" vector-effect="non-scaling-stroke"/>
</svg>
<div class="s_process_step_icon">
<span>
<i class="fa fa-unlock-alt fa-2x mx-auto rounded-circle bg-o-color-5"/>
</span>
<i class="fa fa-unlock-alt fa-2x mx-auto rounded-circle bg-o-color-5"/>
</div>
<div class="s_process_step_content text-center">
<div class="s_process_step_content">
<h2>Sign in</h2>
<p>Click on the icon to adapt it <br/>to your purpose.</p>
</div>
</div>
<div class="col-lg-3 s_process_step pt24 pb24">
<svg class="s_process_step_connector" viewBox="0 0 100 20" preserveAspectRatio="none">
<path d="M 0 10 L 100 10" vector-effect="non-scaling-stroke"/>
</svg>
<div class="s_process_step_icon">
<span>
<i class="fa fa-paypal fa-2x mx-auto rounded-circle bg-secondary"/>
</span>
<i class="fa fa-paypal fa-2x mx-auto rounded-circle bg-secondary"/>
</div>
<div class="s_process_step_content text-center">
<div class="s_process_step_content">
<h2>Pay</h2>
<p>Duplicate blocks <br/>to add more steps.</p>
</div>
</div>
<div class="col-lg-3 s_process_step pt24 pb24">
<svg class="s_process_step_connector" viewBox="0 0 100 20" preserveAspectRatio="none">
<path d="M 0 10 L 100 10" vector-effect="non-scaling-stroke"/>
</svg>
<div class="s_process_step_icon">
<span>
<i class="fa fa-plane fa-2x mx-auto rounded-circle bg-o-color-3"/>
</span>
<i class="fa fa-plane fa-2x mx-auto rounded-circle bg-o-color-3"/>
</div>
<div class="s_process_step_content text-center">
<div class="s_process_step_content">
<h2>Get Delivered</h2>
<p>Select and delete blocks <br/>to remove some steps.</p>
</div>
@@ -54,10 +65,38 @@
</section>
</template>
<template id="s_process_steps_options" inherit_id="website.snippet_options">
<xpath expr="." position="inside">
<div data-js="StepsConnector" data-selector=".s_process_steps">
<we-row string="Connector">
<we-select data-name="connector_type">
<we-button data-select-class="" data-name="no_connector_opt">None</we-button>
<we-button data-select-class="s_process_steps_connector_line">Line</we-button>
<we-button data-select-class="s_process_steps_connector_arrow">Straight arrow</we-button>
<we-button data-select-class="s_process_steps_connector_curved_arrow">Curved arrow</we-button>
</we-select>
<we-colorpicker
data-select-style="true"
data-name="connector_color_opt"
data-dependencies="!no_connector_opt"
data-apply-to=".s_process_step_connector path"
data-css-property="stroke" data-change-color="true"/>
</we-row>
</div>
</xpath>
</template>
<record id="website.s_process_steps_000_scss" model="ir.asset">
<field name="name">Process steps 000 SCSS</field>
<field name="bundle">web.assets_frontend</field>
<field name="path">website/static/src/snippets/s_process_steps/000.scss</field>
<field name="active" eval="False"/>
</record>
<record id="website.s_process_steps_001_scss" model="ir.asset">
<field name="name">Process steps 001 SCSS</field>
<field name="bundle">web.assets_frontend</field>
<field name="path">website/static/src/snippets/s_process_steps/001.scss</field>
</record>
</odoo>