Skip to content

Commit

Permalink
Steps option
Browse files Browse the repository at this point in the history
  • Loading branch information
loco-odoo committed Jan 21, 2025
1 parent 76220f9 commit 329c75e
Show file tree
Hide file tree
Showing 6 changed files with 298 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
257 changes: 257 additions & 0 deletions addons/html_builder/static/src/builder/options/process_steps_option.js
Original file line number Diff line number Diff line change
@@ -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 "";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?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'"/>
</BuilderRow>
<!-- TODO: call snippet_options_background_options (probably SectionBackgroundOption)-->
</t>

</templates>
16 changes: 16 additions & 0 deletions addons/html_builder/static/tests/options/steps_options.test.js
Original file line number Diff line number Diff line change
@@ -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("<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='o-color-1']").click();
expect(":iframe .s_process_steps .s_process_step path").toHaveClass("bg-o-color-1");
});
9 changes: 4 additions & 5 deletions addons/html_editor/static/src/main/font/color_plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -284,7 +283,6 @@ export class ColorPlugin extends Plugin {
}
return font;
});
};

for (const fieldNode of selectedFieldNodes) {
this.colorElement(fieldNode, color, mode);
Expand Down Expand Up @@ -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"] = "";
Expand Down

0 comments on commit 329c75e

Please sign in to comment.