From 329c75e308ee4879ff19e4bce81bb06a19295b45 Mon Sep 17 00:00:00 2001 From: "Louis (loco)" Date: Tue, 21 Jan 2025 09:54:13 +0100 Subject: [PATCH] Steps option --- .../src/builder/builder_components/utils.js | 2 +- ...der_option.xml => border_configurator.xml} | 0 .../builder/options/process_steps_option.js | 257 ++++++++++++++++++ .../builder/options/process_steps_option.xml | 20 ++ .../tests/options/steps_options.test.js | 16 ++ .../static/src/main/font/color_plugin.js | 9 +- 6 files changed, 298 insertions(+), 6 deletions(-) rename addons/html_builder/static/src/builder/options/{global_border_option.xml => border_configurator.xml} (100%) create mode 100644 addons/html_builder/static/src/builder/options/process_steps_option.js create mode 100644 addons/html_builder/static/src/builder/options/process_steps_option.xml create mode 100644 addons/html_builder/static/tests/options/steps_options.test.js diff --git a/addons/html_builder/static/src/builder/builder_components/utils.js b/addons/html_builder/static/src/builder/builder_components/utils.js index 64a7ea3a6e6ac..da3b282d4748d 100644 --- a/addons/html_builder/static/src/builder/builder_components/utils.js +++ b/addons/html_builder/static/src/builder/builder_components/utils.js @@ -230,7 +230,7 @@ export function useClickableBuilderComponent() { const { getAllActions, callOperation } = getAllActionsAndOperations(comp); const getAction = comp.env.editor.shared.builderActions.getAction; const applyOperation = comp.env.editor.shared.history.makePreviewableOperation(callApply); - const shouldToggle = !comp.env.actionBus; + const shouldToggle = !comp.env.selectableContext; const hasPreview = comp.props.preview === true || (comp.props.preview === undefined && comp.env.weContext.preview !== false); diff --git a/addons/html_builder/static/src/builder/options/global_border_option.xml b/addons/html_builder/static/src/builder/options/border_configurator.xml similarity index 100% rename from addons/html_builder/static/src/builder/options/global_border_option.xml rename to addons/html_builder/static/src/builder/options/border_configurator.xml diff --git a/addons/html_builder/static/src/builder/options/process_steps_option.js b/addons/html_builder/static/src/builder/options/process_steps_option.js new file mode 100644 index 0000000000000..7eab16081b447 --- /dev/null +++ b/addons/html_builder/static/src/builder/options/process_steps_option.js @@ -0,0 +1,257 @@ +import { defaultBuilderComponents } from "../builder_components/default_builder_components"; +import { registry } from "@web/core/registry"; +import { coreBuilderActions } from "@html_builder/builder/core_builder_action_plugin"; +import { Plugin } from "@html_editor/plugin"; +import { Component } from "@odoo/owl"; + +class ProcessStepsOptionPlugin extends Plugin { + static id = "ProcessStepsOption"; + selector = ".s_process_steps"; + resources = { + builder_options: { + OptionComponent: ProcessStepsOption, + // template: "html_builder.ProcessStepsOption", + selector: this.selector, + }, + builder_actions: this.getActions(), + }; + getActions() { + return { + changeConnector: { + 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)); + }, + isApplied: coreBuilderActions.classAction.isApplied, + clean: coreBuilderActions.classAction.clean, + }, + }; + } +} + +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. + * + * @private + */ +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. + * + * @private + * @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. + * + * @private + * @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. + * + * @private + * @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 ""; +} diff --git a/addons/html_builder/static/src/builder/options/process_steps_option.xml b/addons/html_builder/static/src/builder/options/process_steps_option.xml new file mode 100644 index 0000000000000..58a883dc1c2d3 --- /dev/null +++ b/addons/html_builder/static/src/builder/options/process_steps_option.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + diff --git a/addons/html_builder/static/tests/options/steps_options.test.js b/addons/html_builder/static/tests/options/steps_options.test.js new file mode 100644 index 0000000000000..1f17b636b6e4c --- /dev/null +++ b/addons/html_builder/static/tests/options/steps_options.test.js @@ -0,0 +1,16 @@ +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("
"); + 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='o-color-1']").click(); + expect(":iframe .s_process_steps .s_process_step path").toHaveClass("bg-o-color-1"); +}); diff --git a/addons/html_editor/static/src/main/font/color_plugin.js b/addons/html_editor/static/src/main/font/color_plugin.js index b47b510dbb40d..bac8643994f8e 100644 --- a/addons/html_editor/static/src/main/font/color_plugin.js +++ b/addons/html_editor/static/src/main/font/color_plugin.js @@ -226,8 +226,7 @@ export class ColorPlugin extends Plugin { .filter(Boolean) ); - const getFonts = (selectedNodes) => { - return selectedNodes.flatMap((node) => { + const getFonts = (selectedNodes) => selectedNodes.flatMap((node) => { let font = closestElement(node, "font") || closestElement(node, "span"); const children = font && descendants(font); if ( @@ -284,7 +283,6 @@ export class ColorPlugin extends Plugin { } return font; }); - }; for (const fieldNode of selectedFieldNodes) { this.colorElement(fieldNode, color, mode); @@ -337,11 +335,12 @@ export class ColorPlugin extends Plugin { * @param {'color'|'backgroundColor'} mode 'color' or 'backgroundColor' */ colorElement(element, color, mode) { - const newClassName = element.className + const oldClassName = element.getAttribute("class") || ""; + const newClassName = oldClassName .replace(mode === "color" ? TEXT_CLASSES_REGEX : BG_CLASSES_REGEX, "") .replace(/\btext-gradient\b/g, "") // cannot be combined with setting a background .replace(/\s+/, " "); - element.className !== newClassName && (element.className = newClassName); + oldClassName !== newClassName && element.setAttribute("class", newClassName); element.style["background-image"] = ""; if (mode === "backgroundColor") { element.style["background"] = "";