-
Notifications
You must be signed in to change notification settings - Fork 115
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0cef7d7
commit fd9ed7e
Showing
7 changed files
with
335 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
264 changes: 264 additions & 0 deletions
264
addons/html_builder/static/src/builder/options/process_steps_option.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,264 @@ | ||
import { defaultBuilderComponents } from "../builder_components/default_builder_components"; | ||
import { coreBuilderActions } from "@html_builder/builder/core_builder_action_plugin"; | ||
import { applyFunDependOnSelectorAndExclude } from "@html_builder/builder/options/utils"; | ||
import { getCSSVariableValue } from "@html_builder/builder/utils/utils_css"; | ||
import { Plugin } from "@html_editor/plugin"; | ||
import { Component } from "@odoo/owl"; | ||
import { registry } from "@web/core/registry"; | ||
|
||
class ProcessStepsOptionPlugin extends Plugin { | ||
static id = "ProcessStepsOption"; | ||
selector = ".s_process_steps"; | ||
resources = { | ||
builder_options: { | ||
OptionComponent: ProcessStepsOption, | ||
selector: this.selector, | ||
}, | ||
builder_actions: this.getActions(), | ||
content_updated_handlers: (rootEl) => | ||
applyFunDependOnSelectorAndExclude(reloadConnectors, rootEl, this.selector), | ||
}; | ||
getActions() { | ||
return { | ||
changeConnector: { | ||
...coreBuilderActions.classAction, | ||
apply: ({ editingElement, param: className }) => { | ||
coreBuilderActions.classAction.apply({ | ||
editingElement: editingElement, | ||
param: className, | ||
}); | ||
reloadConnectors(editingElement); | ||
let markerEnd = ""; | ||
if ( | ||
[ | ||
"s_process_steps_connector_arrow", | ||
"s_process_steps_connector_curved_arrow", | ||
].includes(className) | ||
) { | ||
const arrowHeadEl = editingElement.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})`; | ||
} | ||
editingElement | ||
.querySelectorAll(".s_process_step_connector path") | ||
.forEach((path) => path.setAttribute("marker-end", markerEnd)); | ||
}, | ||
}, | ||
changeArrowColor: { | ||
apply: ({ editingElement, param: colorValue }) => { | ||
const htmlPropColor = getCSSVariableValue(colorValue); | ||
const arrowHeadEl = editingElement | ||
.closest(".s_process_steps") | ||
.querySelector(".s_process_steps_arrow_head"); | ||
arrowHeadEl.querySelector("path").style.fill = htmlPropColor || colorValue; | ||
}, | ||
}, | ||
}; | ||
} | ||
} | ||
|
||
const connectorOptionParams = [ | ||
{ key: "", param: "None" }, | ||
{ key: "s_process_steps_connector_line", param: "Line" }, | ||
{ key: "s_process_steps_connector_arrow", param: "Straight arrow" }, | ||
{ key: "s_process_steps_connector_curved_arrow", param: "Curved arrow" }, | ||
]; | ||
|
||
export class ProcessStepsOption extends Component { | ||
static template = "html_builder.ProcessStepsOption"; | ||
static components = { ...defaultBuilderComponents }; | ||
static props = {}; | ||
|
||
setup() { | ||
this.connectorOptionParams = connectorOptionParams; | ||
} | ||
|
||
getConnectorId(connectorOptionParamKey) { | ||
return !connectorOptionParamKey ? "no_connector_opt" : ""; | ||
} | ||
} | ||
registry.category("website-plugins").add(ProcessStepsOptionPlugin.id, ProcessStepsOptionPlugin); | ||
|
||
/** | ||
* Width and position of the connectors should be updated when one of the | ||
* steps is modified. | ||
* | ||
*/ | ||
function reloadConnectors(editingElement) { | ||
const connectorOptionClasses = connectorOptionParams.map( | ||
(connectorOptionParam) => connectorOptionParam.key | ||
); | ||
const type = | ||
connectorOptionClasses.find( | ||
(connectorOptionClass) => | ||
connectorOptionClass && editingElement.classList.contains(connectorOptionClass) | ||
) || ""; | ||
// As the connectors are only visible in desktop, we can ignore the | ||
// steps that are only visible in mobile. | ||
const stepsEls = editingElement.querySelectorAll( | ||
".s_process_step:not(.o_snippet_desktop_invisible)" | ||
); | ||
const nbBootstrapCols = 12; | ||
let colsInRow = 0; | ||
|
||
for (let i = 0; i < stepsEls.length - 1; i++) { | ||
const connectorEl = stepsEls[i].querySelector(".s_process_step_connector"); | ||
const stepMainElementRect = getStepMainElementRect(stepsEls[i]); | ||
const nextStepMainElementRect = getStepMainElementRect(stepsEls[i + 1]); | ||
const stepSize = getClassSuffixedInteger(stepsEls[i], "col-lg-"); | ||
const nextStepSize = getClassSuffixedInteger(stepsEls[i + 1], "col-lg-"); | ||
const stepOffset = getClassSuffixedInteger(stepsEls[i], "offset-lg-"); | ||
const nextStepOffset = getClassSuffixedInteger(stepsEls[i + 1], "offset-lg-"); | ||
const stepPaddingTop = getClassSuffixedInteger(stepsEls[i], "pt"); | ||
const nextStepPaddingTop = getClassSuffixedInteger(stepsEls[i + 1], "pt"); | ||
const stepHeightDifference = stepPaddingTop - nextStepPaddingTop; | ||
const hCurrentStepIconHeight = stepMainElementRect.height / 2; | ||
const hNextStepIconHeight = nextStepMainElementRect.height / 2; | ||
|
||
connectorEl.style.left = `calc(50% + ${stepMainElementRect.width / 2}px + 16px)`; | ||
connectorEl.style.height = `${ | ||
stepMainElementRect.height + Math.abs(stepHeightDifference) | ||
}px`; | ||
connectorEl.style.width = `calc(${ | ||
(100 * (stepSize / 2 + nextStepOffset + nextStepSize / 2)) / stepSize | ||
}% - ${stepMainElementRect.width / 2}px - ${nextStepMainElementRect.width / 2}px - 32px)`; | ||
|
||
const marginType = stepHeightDifference < 0 ? "marginBottom" : "marginTop"; | ||
connectorEl.style[marginType] = `${0 - Math.abs(stepHeightDifference)}px`; | ||
|
||
const isTheLastColOfRow = | ||
nbBootstrapCols < colsInRow + stepSize + stepOffset + nextStepSize + nextStepOffset; | ||
connectorEl.classList.toggle("d-none", isTheLastColOfRow); | ||
colsInRow = isTheLastColOfRow ? 0 : colsInRow + stepSize + stepOffset; | ||
// When we are mobile view, the connector is not visible, here we | ||
// display it quickly just to have its size. | ||
connectorEl.style.display = "block"; | ||
const { height, width } = connectorEl.getBoundingClientRect(); | ||
connectorEl.style.removeProperty("display"); | ||
if (type === "s_process_steps_connector_curved_arrow" && i % 2 === 0) { | ||
connectorEl.style.transform = stepHeightDifference ? "unset" : "scale(1, -1)"; | ||
} else { | ||
connectorEl.style.transform = "unset"; | ||
} | ||
connectorEl.setAttribute("viewBox", `0 0 ${width} ${height}`); | ||
connectorEl | ||
.querySelector("path") | ||
.setAttribute( | ||
"d", | ||
getPath( | ||
type, | ||
width, | ||
height, | ||
stepHeightDifference, | ||
hCurrentStepIconHeight, | ||
hNextStepIconHeight | ||
) | ||
); | ||
} | ||
} | ||
/** | ||
* Returns the number suffixed to the class given in parameter. | ||
* | ||
* @param {HTMLElement} el | ||
* @param {String} classNamePrefix | ||
* @returns {Integer} | ||
*/ | ||
function getClassSuffixedInteger(el, classNamePrefix) { | ||
const className = [...el.classList].find((cl) => cl.startsWith(classNamePrefix)); | ||
return className ? parseInt(className.replace(classNamePrefix, "")) : 0; | ||
} | ||
/** | ||
* Returns the step's icon or content bounding rectangle. | ||
* | ||
* @param {HTMLElement} | ||
* @returns {object} | ||
*/ | ||
function getStepMainElementRect(stepEl) { | ||
const iconEl = stepEl.querySelector(".s_process_step_number"); | ||
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) => | ||
current.width > previous.width ? current : previous | ||
); | ||
} | ||
return {}; | ||
} | ||
/** | ||
* Returns the svg path based on the type of connector. | ||
* | ||
* @param {string} type | ||
* @param {integer} width | ||
* @param {integer} height | ||
* @returns {string} | ||
*/ | ||
function getPath( | ||
type, | ||
width, | ||
height, | ||
stepHeightDifference, | ||
hCurrentStepIconHeight, | ||
hNextStepIconHeight | ||
) { | ||
const hHeight = height / 2; | ||
switch (type) { | ||
case "s_process_steps_connector_line": { | ||
const verticalPaddingFactor = Math.abs(stepHeightDifference) / 8; | ||
if (stepHeightDifference >= 0) { | ||
return `M 0 ${ | ||
stepHeightDifference + hCurrentStepIconHeight - verticalPaddingFactor | ||
} L ${width} ${hNextStepIconHeight + verticalPaddingFactor}`; | ||
} | ||
return `M 0 ${hCurrentStepIconHeight + verticalPaddingFactor} L ${width} ${ | ||
hNextStepIconHeight - stepHeightDifference - verticalPaddingFactor | ||
}`; | ||
} | ||
case "s_process_steps_connector_arrow": { | ||
// When someone plays with the y-axis, it adds the padding in | ||
// multiple of 8px. so here we devide it by 8 to calculate the | ||
// number of padding steps has been added. | ||
const verticalPaddingFactor = (Math.abs(stepHeightDifference) / 8) * 1.5; | ||
if (stepHeightDifference >= 0) { | ||
return `M ${0.05 * width} ${ | ||
stepHeightDifference + hCurrentStepIconHeight - verticalPaddingFactor | ||
} L ${0.95 * width - 6} ${hNextStepIconHeight + verticalPaddingFactor}`; | ||
} | ||
return `M ${0.05 * width} ${hCurrentStepIconHeight + verticalPaddingFactor} L ${ | ||
0.95 * width - 6 | ||
} ${Math.abs(stepHeightDifference) + hNextStepIconHeight - verticalPaddingFactor}`; | ||
} | ||
case "s_process_steps_connector_curved_arrow": { | ||
if (stepHeightDifference == 0) { | ||
return `M ${0.05 * width} ${hHeight * 1.2} Q ${width / 2} ${hHeight * 1.8} ${ | ||
0.95 * width - 6 | ||
} ${hHeight * 1.2}`; | ||
} else if (stepHeightDifference > 0) { | ||
return `M ${0.05 * width} ${stepHeightDifference + hCurrentStepIconHeight} Q ${ | ||
width * 0.75 | ||
} ${height * 0.75} ${0.5 * width - 6} ${hHeight} T ${ | ||
0.95 * width - 6 | ||
} ${hNextStepIconHeight}`; | ||
} | ||
return `M ${0.05 * width} ${hCurrentStepIconHeight} Q ${width * 0.75} ${ | ||
height * 0.005 | ||
} ${0.5 * width - 6} ${hHeight} T ${0.95 * width - 6} ${ | ||
Math.abs(stepHeightDifference) + hNextStepIconHeight | ||
}`; | ||
} | ||
} | ||
return ""; | ||
} |
29 changes: 29 additions & 0 deletions
29
addons/html_builder/static/src/builder/options/process_steps_option.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<templates xml:space="preserve"> | ||
|
||
<t t-name="html_builder.ProcessStepsOption"> | ||
<BuilderRow label.translate="Connector"> | ||
<BuilderSelect> | ||
<t t-foreach="connectorOptionParams" t-as="connectorOptionParam" t-key="connectorOptionParam_index"> | ||
<BuilderSelectItem action="'changeConnector'" id="getConnectorId(connectorOptionParam.key)" actionParam="connectorOptionParam.key"><t t-out="connectorOptionParam.param"/></BuilderSelectItem> | ||
</t> | ||
</BuilderSelect> | ||
<BuilderColorPicker | ||
styleAction="'stroke'" | ||
dependencies="'!no_connector_opt'" | ||
applyTo="'.s_process_step_connector path'" | ||
action="'changeArrowColor'"/> | ||
</BuilderRow> | ||
<!-- TODO: | ||
<t t-call="website.snippet_options_background_options"> | ||
<t t-set="selector" t-value="'.s_process_step .s_process_step_number'"/> | ||
<t t-set="with_colors" t-value="True"/> | ||
<t t-set="with_images" t-value="False"/> | ||
<t t-set="with_color_combinations" t-value="False"/> | ||
<t t-set="with_gradients" t-value="True"/> | ||
</t> | ||
(probably SectionBackgroundOption in the current implementation) | ||
--> | ||
</t> | ||
|
||
</templates> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
export function applyFunDependOnSelectorAndExclude(fn, rootEl, selector, exclude = "") { | ||
const closestSelector = rootEl.closest(selector); | ||
const selectorEls = closestSelector | ||
? [closestSelector] | ||
: [...rootEl.querySelectorAll(selector)]; | ||
const editingEls = selectorEls.filter((selectorEl) => !selectorEl.matches(exclude)); | ||
for (const editingEl of editingEls) { | ||
fn(editingEl); | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
addons/html_builder/static/tests/options/steps_options.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { expect, test } from "@odoo/hoot"; | ||
import { contains } from "@web/../tests/web_test_helpers"; | ||
import { defineWebsiteModels, setupWebsiteBuilder } from "../helpers"; | ||
import { insertStructureSnippet } from "./helpers"; | ||
|
||
defineWebsiteModels(); | ||
|
||
test("modify the steps color", async () => { | ||
const { getEditor } = await setupWebsiteBuilder("<div></div>"); | ||
const editor = getEditor(); | ||
await insertStructureSnippet(editor, "s_process_steps"); | ||
await contains(":iframe .s_process_steps").click(); | ||
await contains("[data-label='Connector'] .o_we_color_preview").click(); | ||
await contains(".o-overlay-item [data-color='#FF0000']").click(); | ||
expect(":iframe .s_process_steps .s_process_step path").toHaveStyle({ | ||
stroke: "rgb(255, 0, 0)", | ||
}); | ||
expect(":iframe marker.s_process_steps_arrow_head").toHaveStyle({ | ||
fill: "rgb(255, 0, 0)", | ||
}); | ||
}); |
Oops, something went wrong.