From a517db25fd3bd054c47f9fb4e768423fa07a9768 Mon Sep 17 00:00:00 2001 From: "Louis (loco)" Date: Mon, 3 Feb 2025 14:23:18 +0100 Subject: [PATCH] Begin of the shapes background --- .../builder_components/builder_colorpicker.js | 1 + .../builder_colorpicker.xml | 2 +- .../static/src/builder_components/utils.js | 65 +-- .../src/builder_sidebar/builder_sidebar.js | 4 +- .../src/builder_sidebar/builder_sidebar.scss | 62 +++ .../src/builder_sidebar/builder_sidebar.xml | 2 +- .../src/builder_sidebar/tabs/customize_tab.js | 6 +- .../components/background_shape_component.js | 181 ++++++++ .../components/background_shape_component.xml | 81 ++++ .../components/global_sidebar_component.js | 5 + .../static/src/components/option_container.js | 23 +- .../src/components/option_container.xml | 3 +- .../static/src/img/options/bg_shape.svg | 11 + .../src/options/background_component.js | 32 ++ .../src/options/background_component.xml | 59 +++ .../static/src/options/background_option.js | 396 ++++++++++++++++++ .../src/options/image_gallery_option.js | 1 + .../src/options/process_steps_option.js | 22 +- .../src/options/section_background_option.js | 15 +- .../static/src/utils/utils_css.js | 13 +- .../static/src/utils/image_processing.js | 14 + addons/web_editor/views/snippets.xml | 1 + 22 files changed, 955 insertions(+), 44 deletions(-) create mode 100644 addons/html_builder/static/src/components/background_shape_component.js create mode 100644 addons/html_builder/static/src/components/background_shape_component.xml create mode 100644 addons/html_builder/static/src/components/global_sidebar_component.js create mode 100644 addons/html_builder/static/src/img/options/bg_shape.svg create mode 100644 addons/html_builder/static/src/options/background_component.js create mode 100644 addons/html_builder/static/src/options/background_component.xml create mode 100644 addons/html_builder/static/src/options/background_option.js diff --git a/addons/html_builder/static/src/builder_components/builder_colorpicker.js b/addons/html_builder/static/src/builder_components/builder_colorpicker.js index 7652d3c164af0..61f124b2fbddb 100644 --- a/addons/html_builder/static/src/builder_components/builder_colorpicker.js +++ b/addons/html_builder/static/src/builder_components/builder_colorpicker.js @@ -13,6 +13,7 @@ export class BuilderColorPicker extends Component { static props = { ...basicContainerBuilderComponentProps, unit: { type: String, optional: true }, + title: { type: String, optional: true }, }; static components = { ColorSelector, diff --git a/addons/html_builder/static/src/builder_components/builder_colorpicker.xml b/addons/html_builder/static/src/builder_components/builder_colorpicker.xml index cf0e3eceff4ae..abe4a0a421b90 100644 --- a/addons/html_builder/static/src/builder_components/builder_colorpicker.xml +++ b/addons/html_builder/static/src/builder_components/builder_colorpicker.xml @@ -3,7 +3,7 @@ -
+
{} } = {}) { export function useClickableBuilderComponent() { useBuilderComponent(); const comp = useComponent(); - const { getAllActions, callOperation } = getAllActionsAndOperations(comp); + const { getAllActions, callOperation, isApplied } = getAllActionsAndOperations(comp); const getAction = comp.env.editor.shared.builderActions.getAction; const applyOperation = comp.env.editor.shared.history.makePreviewableOperation(callApply); const shouldToggle = !comp.env.actionBus; @@ -255,7 +255,7 @@ export function useClickableBuilderComponent() { function callApply(applySpecs) { comp.env.selectableContext?.cleanSelectedItem(applySpecs); const cleans = comp.props.inheritedActions - ?.map((actionId) => comp.env.dependencyManager.get(actionId).cleanSelectedItem) + ?.map((actionId) => comp.env.dependencyManager.get(actionId)?.cleanSelectedItem) .filter(Boolean); for (const clean of new Set(cleans)) { clean(applySpecs); @@ -281,33 +281,6 @@ export function useClickableBuilderComponent() { } } } - function isApplied() { - const editingElements = comp.env.getEditingElements(); - if (!editingElements.length) { - return; - } - const areActionsActiveTabs = getAllActions().map((o) => { - const { actionId, actionParam, actionValue } = o; - // TODO isApplied === first editing el or all ? - const editingElement = editingElements[0]; - const isApplied = getAction(actionId).isApplied?.({ - editingElement, - param: actionParam, - value: actionValue, - }); - return comp.props.inverseAction ? !isApplied : isApplied; - }); - // If there is no `isApplied` method for the widget return false - if (areActionsActiveTabs.every((el) => el === undefined)) { - return false; - } - // If `isApplied` is explicitly false for an action return false - if (areActionsActiveTabs.some((el) => el === false)) { - return false; - } - // `isApplied` is true for at least one action - return true; - } function getPriority() { return ( getAllActions() @@ -546,6 +519,11 @@ export function getAllActionsAndOperations(comp) { if (!applySpec.load) { return; } + const shouldToggle = !comp.env.actionBus; + if (shouldToggle && isApplied()) { + // The element will be cleaned, do not load + return; + } const result = await applySpec.load({ editingElement: applySpec.editingElement, param: applySpec.actionParam, @@ -558,8 +536,37 @@ export function getAllActionsAndOperations(comp) { } ); } + function isApplied() { + const getAction = comp.env.editor.shared.builderActions.getAction; + const editingElements = comp.env.getEditingElements(); + if (!editingElements.length) { + return; + } + const areActionsActiveTabs = getAllActions().map((o) => { + const { actionId, actionParam, actionValue } = o; + // TODO isApplied === first editing el or all ? + const editingElement = editingElements[0]; + const isApplied = getAction(actionId).isApplied?.({ + editingElement, + param: actionParam, + value: actionValue, + }); + return comp.props.inverseAction ? !isApplied : isApplied; + }); + // If there is no `isApplied` method for the widget return false + if (areActionsActiveTabs.every((el) => el === undefined)) { + return false; + } + // If `isApplied` is explicitly false for an action return false + if (areActionsActiveTabs.some((el) => el === false)) { + return false; + } + // `isApplied` is true for at least one action + return true; + } return { getAllActions: getAllActions, callOperation: callOperation, + isApplied: isApplied, }; } diff --git a/addons/html_builder/static/src/builder_sidebar/builder_sidebar.js b/addons/html_builder/static/src/builder_sidebar/builder_sidebar.js index 7276b0d1d2af3..fc26749a7e9cb 100644 --- a/addons/html_builder/static/src/builder_sidebar/builder_sidebar.js +++ b/addons/html_builder/static/src/builder_sidebar/builder_sidebar.js @@ -173,8 +173,8 @@ export class BuilderSidebar extends Component { // Ensure that the iframe is loaded and the editor is created before // instantiating the sub components that potentially need the // editor. - const iframeEl = await this.props.iframeLoaded; - this.editor.attachTo(iframeEl.contentDocument.body.querySelector("#wrapwrap")); + this.iframeEl = await this.props.iframeLoaded; + this.editor.attachTo(this.iframeEl.contentDocument.body.querySelector("#wrapwrap")); }); useSubEnv({ diff --git a/addons/html_builder/static/src/builder_sidebar/builder_sidebar.scss b/addons/html_builder/static/src/builder_sidebar/builder_sidebar.scss index f4942847b1935..57e0ce4d18b51 100644 --- a/addons/html_builder/static/src/builder_sidebar/builder_sidebar.scss +++ b/addons/html_builder/static/src/builder_sidebar/builder_sidebar.scss @@ -100,3 +100,65 @@ border-width: 4px; border-bottom: none !important; } + +.o_pager_container { + overflow-y: scroll; + scroll-behavior: smooth; +} + +.builder_select_page { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: $o-we-item-spacing / 2; + padding: $o-we-item-spacing; + background-color: $o-we-bg-lighter; + + button { + --PreviewAlphaBg-background-size: 16px; + + @extend %o-preview-alpha-background; + padding: $o-we-item-spacing; + background-color: transparent; + } + // For background shapes + .button_shape { + grid-column: span 2; + padding: 0; + + button, div { + width: 100%; + height: 50px; + } + } + // button.active { + // background-color: rgba($o-we-toolbar-color-accent, 0.2); + + // .o_we_shape { + // outline: 4px solid $o-we-toolbar-color-accent; + // outline-offset: -2px; + // } + // } + img { + width: 100%; + aspect-ratio: 1; + object-fit: contain; + } +} + +.o_we_shape_animated_label { + @include o-position-absolute(0, 0); + padding: 0 4px; + background: $o-we-toolbar-color-accent; + color: white; + + > span { + @include o-text-overflow(inline-block); + max-width: 0; + } +} + +.o_pager_nav_angle { + @include button-variant($o-we-bg-light, $o-we-bg-light); + padding: $o-we-item-spacing / 2; + font-size: $o-we-sidebar-font-size * 1.4; +} \ No newline at end of file diff --git a/addons/html_builder/static/src/builder_sidebar/builder_sidebar.xml b/addons/html_builder/static/src/builder_sidebar/builder_sidebar.xml index fa0943f8ea9c7..eb5fbe4e005ec 100644 --- a/addons/html_builder/static/src/builder_sidebar/builder_sidebar.xml +++ b/addons/html_builder/static/src/builder_sidebar/builder_sidebar.xml @@ -30,7 +30,7 @@ - + theme diff --git a/addons/html_builder/static/src/builder_sidebar/tabs/customize_tab.js b/addons/html_builder/static/src/builder_sidebar/tabs/customize_tab.js index f47f5a7930805..113601d991854 100644 --- a/addons/html_builder/static/src/builder_sidebar/tabs/customize_tab.js +++ b/addons/html_builder/static/src/builder_sidebar/tabs/customize_tab.js @@ -1,4 +1,4 @@ -import { Component, useState } from "@odoo/owl"; +import { Component, useState, useSubEnv } from "@odoo/owl"; import { OptionsContainer } from "../../components/option_container"; import { useVisibilityObserver } from "../../builder_components/utils"; @@ -8,6 +8,7 @@ export class CustomizeTab extends Component { static props = { currentOptionsContainers: { type: Array, optional: true }, snippetModel: { type: Object }, + iframeEl: { type: HTMLElement }, }; static defaultProps = { currentOptionsContainers: [], @@ -20,5 +21,8 @@ export class CustomizeTab extends Component { useVisibilityObserver("content", (hasContent) => { this.state.hasContent = hasContent; }); + useSubEnv({ + iframeEl: this.props.iframeEl, + }); } } diff --git a/addons/html_builder/static/src/components/background_shape_component.js b/addons/html_builder/static/src/components/background_shape_component.js new file mode 100644 index 0000000000000..ce2ab4644f40b --- /dev/null +++ b/addons/html_builder/static/src/components/background_shape_component.js @@ -0,0 +1,181 @@ +import { defaultBuilderComponents } from "@html_builder/builder_components/default_builder_components"; +import { Component, useRef } from "@odoo/owl"; + +const connectionShapes = [ + { shape: "web_editor/Connections/01", label: "Connections 01" }, + { shape: "web_editor/Connections/02", label: "Connections 02" }, + { shape: "web_editor/Connections/03", label: "Connections 03" }, + { shape: "web_editor/Connections/04", label: "Connections 04" }, + { shape: "web_editor/Connections/05", label: "Connections 05" }, + { shape: "web_editor/Connections/06", label: "Connections 06" }, + { shape: "web_editor/Connections/07", label: "Connections 07" }, + { shape: "web_editor/Connections/08", label: "Connections 08" }, + { shape: "web_editor/Connections/09", label: "Connections 09" }, + { shape: "web_editor/Connections/10", label: "Connections 10" }, + { shape: "web_editor/Connections/11", label: "Connections 11" }, + { shape: "web_editor/Connections/12", label: "Connections 12" }, + { shape: "web_editor/Connections/13", label: "Connections 13" }, + { shape: "web_editor/Connections/14", label: "Connections 14" }, + { shape: "web_editor/Connections/15", label: "Connections 15" }, + { shape: "web_editor/Connections/16", label: "Connections 16" }, + { shape: "web_editor/Connections/17", label: "Connections 17" }, + { shape: "web_editor/Connections/18", label: "Connections 18" }, + { shape: "web_editor/Connections/19", label: "Connections 19" }, + { shape: "web_editor/Connections/20", label: "Connections 20" }, +]; +const originShapes = [ + { shape: "web_editor/Origins/02_001", label: "Origins 01" }, + { shape: "web_editor/Origins/05", label: "Origins 02" }, + { shape: "web_editor/Origins/06_001", label: "Origins 03" }, + { shape: "web_editor/Origins/07_002", label: "Origins 04" }, + { shape: "web_editor/Origins/09_001", label: "Origins 05" }, + { shape: "web_editor/Origins/16", label: "Origins 06", animated: true }, + { shape: "web_editor/Origins/17", label: "Origins 07", animated: true }, + { shape: "web_editor/Origins/19", label: "Origins 08" }, +]; +const boldShapes = [ + { shape: "web_editor/Bold/01", label: "Bold 01", animated: true }, + { shape: "web_editor/Bold/03", label: "Bold 02" }, + { shape: "web_editor/Bold/04", label: "Bold 03" }, + { shape: "web_editor/Bold/05_001", label: "Bold 04" }, + { shape: "web_editor/Bold/06_001", label: "Bold 05" }, + { shape: "web_editor/Bold/07_001", label: "Bold 06" }, + { shape: "web_editor/Bold/08", label: "Bold 07" }, + { shape: "web_editor/Bold/09", label: "Bold 08" }, + { shape: "web_editor/Bold/10_001", label: "Bold 09" }, + { shape: "web_editor/Bold/02_001", label: "Bold 10" }, +]; +const blobShapes = [ + { shape: "web_editor/Blobs/01_001", label: "Blobs 01" }, + { shape: "web_editor/Blobs/02", label: "Blobs 02" }, + { shape: "web_editor/Blobs/03", label: "Blobs 03" }, + { shape: "web_editor/Blobs/04", label: "Blobs 04" }, + { shape: "web_editor/Blobs/05", label: "Blobs 05" }, + { shape: "web_editor/Blobs/06", label: "Blobs 06" }, + { shape: "web_editor/Blobs/07", label: "Blobs 07" }, + { shape: "web_editor/Blobs/08", label: "Blobs 08" }, + { shape: "web_editor/Blobs/09", label: "Blobs 09" }, + { shape: "web_editor/Blobs/10_001", label: "Blobs 10" }, + { shape: "web_editor/Blobs/11", label: "Blobs 11" }, + { shape: "web_editor/Blobs/12", label: "Blobs 12" }, +]; +const airyAndZigShapes = [ + { shape: "web_editor/Airy/01", label: "Airy 01" }, + { shape: "web_editor/Airy/06", label: "Airy 02" }, + { shape: "web_editor/Airy/02", label: "Airy 03" }, + { shape: "web_editor/Airy/07", label: "Airy 04" }, + { shape: "web_editor/Airy/08", label: "Airy 05" }, + { shape: "web_editor/Airy/10", label: "Airy 06" }, + { shape: "web_editor/Airy/09", label: "Airy 07" }, + { shape: "web_editor/Airy/11", label: "Airy 08" }, + { shape: "web_editor/Airy/03_001", label: "Airy 09", animated: true }, + { shape: "web_editor/Airy/04_001", label: "Airy 10", animated: true }, + { shape: "web_editor/Airy/05_001", label: "Airy 11", animated: true }, + { shape: "web_editor/Airy/12_001", label: "Airy 12", animated: true }, + { shape: "web_editor/Airy/13_001", label: "Airy 13", animated: true }, + { shape: "web_editor/Airy/14", label: "Airy 14" }, + { shape: "web_editor/Zigs/01_001", label: "Zigs 01", animated: true }, + { shape: "web_editor/Zigs/02_001", label: "Zigs 02", animated: true }, + { shape: "web_editor/Zigs/03", label: "Zigs 03" }, + { shape: "web_editor/Zigs/04", label: "Zigs 04" }, +]; +const wavyShapes = [ + { shape: "web_editor/Wavy/03", label: "Wavy 01" }, + { shape: "web_editor/Wavy/10", label: "Wavy 02" }, + { shape: "web_editor/Wavy/24", label: "Wavy 03", animated: true }, + { shape: "web_editor/Wavy/26", label: "Wavy 04", animated: true }, + { shape: "web_editor/Wavy/27", label: "Wavy 05", animated: true }, + { shape: "web_editor/Wavy/04", label: "Wavy 06" }, + { shape: "web_editor/Wavy/06_001", label: "Wavy 07" }, + { shape: "web_editor/Wavy/07", label: "Wavy 08" }, + { shape: "web_editor/Wavy/08", label: "Wavy 09" }, + { shape: "web_editor/Wavy/09", label: "Wavy 10" }, + { shape: "web_editor/Wavy/11", label: "Wavy 11" }, + { shape: "web_editor/Wavy/28", label: "Wavy 12", animated: true }, + { shape: "web_editor/Wavy/16", label: "Wavy 13" }, + { shape: "web_editor/Wavy/17", label: "Wavy 14" }, + { shape: "web_editor/Wavy/18", label: "Wavy 15" }, + { shape: "web_editor/Wavy/19", label: "Wavy 16" }, + { shape: "web_editor/Wavy/22", label: "Wavy 17" }, + { shape: "web_editor/Wavy/23", label: "Wavy 18" }, +]; +const blockAndRainyShapes = [ + { shape: "web_editor/Blocks/02_001", label: "Blocks 01" }, + { shape: "web_editor/Rainy/01_001", label: "Rainy 01", animated: true }, + { shape: "web_editor/Blocks/01_001", label: "Blocks 02" }, + { shape: "web_editor/Rainy/02_001", label: "Rainy 02", animated: true }, + { shape: "web_editor/Rainy/06", label: "Rainy 03" }, + { shape: "web_editor/Blocks/04", label: "Blocks 04" }, + { shape: "web_editor/Rainy/07", label: "Rainy 04" }, + { shape: "web_editor/Rainy/10", label: "Rainy 05", animated: true }, + { shape: "web_editor/Rainy/08_001", label: "Rainy 06", animated: true }, + { shape: "web_editor/Rainy/09_001", label: "Rainy 07" }, +]; +const floatingShapes = [ + { shape: "web_editor/Floats/01", label: "Float 01", animated: true }, + { shape: "web_editor/Floats/02", label: "Float 02", animated: true }, + { shape: "web_editor/Floats/03", label: "Float 03", animated: true }, + { shape: "web_editor/Floats/04", label: "Float 04", animated: true }, + { shape: "web_editor/Floats/05", label: "Float 05", animated: true }, + { shape: "web_editor/Floats/06", label: "Float 06", animated: true }, + { shape: "web_editor/Floats/07", label: "Float 07", animated: true }, + { shape: "web_editor/Floats/08", label: "Float 08", animated: true }, + { shape: "web_editor/Floats/09", label: "Float 09", animated: true }, + { shape: "web_editor/Floats/10", label: "Float 10", animated: true }, + { shape: "web_editor/Floats/11", label: "Float 11", animated: true }, + { shape: "web_editor/Floats/12", label: "Float 12", animated: true }, + { shape: "web_editor/Floats/13", label: "Float 13", animated: true }, + { shape: "web_editor/Floats/14", label: "Float 14", animated: true }, +]; + +export class BackgroundShapeComponent extends Component { + static template = "html_builder.BackgroundShapeComponent"; + static components = { + ...defaultBuilderComponents, + }; + static props = {}; + + setup() { + this.basicEl = useRef("basic"); + this.linearEl = useRef("linear"); + this.creativeEl = useRef("creative"); + this.shapeBackgroundImagePerClass = {}; + for (const styleSheet of this.env.iframeEl.contentWindow.document.styleSheets) { + if (styleSheet.href && new URL(styleSheet.href).host !== location.host) { + // In some browsers, if a stylesheet is loaded from a different domain + // accessing cssRules results in a SecurityError. + continue; + } + for (const rule of [...styleSheet.cssRules]) { + if (rule.selectorText && rule.selectorText.startsWith(".o_we_shape.")) { + this.shapeBackgroundImagePerClass[rule.selectorText] = + rule.style.backgroundImage; + } + } + } + this.connectionShapes = connectionShapes; + this.originShapes = originShapes; + this.boldShapes = boldShapes; + this.blobShapes = blobShapes; + this.airyAndZigShapes = airyAndZigShapes; + this.wavyShapes = wavyShapes; + this.blockAndRainyShapes = blockAndRainyShapes; + this.floatingShapes = floatingShapes; + } + getShapeUrl(shapeName) { + const shapeClassName = `o_${shapeName.replace(/\//g, "_")}`; + // Match current palette. + return this.shapeBackgroundImagePerClass[`.o_we_shape.${shapeClassName}`]; + } + scrollToShapes(sectionName) { + const sectionElementMap = { + basic: this.basicEl.el, + linear: this.linearEl.el, + creative: this.creativeEl.el, + }; + sectionElementMap[sectionName].scrollIntoView({ behavior: "smooth", block: "start" }); + } + hideComponent() { + this.env.hideComponent(); + } +} diff --git a/addons/html_builder/static/src/components/background_shape_component.xml b/addons/html_builder/static/src/components/background_shape_component.xml new file mode 100644 index 0000000000000..11b40e3062b91 --- /dev/null +++ b/addons/html_builder/static/src/components/background_shape_component.xml @@ -0,0 +1,81 @@ + + + + +
+
+
+
+ + + +
+
+
+
+ Connections + + + + Origins + + + + Bold + + + + Blobs + + + +
+
+ Airy & Zigs + + + +
+
+ Wavy + + + + Block & Rainy + + + + Floating Shape + + + +
+
+
+ + + +
+ + +
+ +
+
+ Animated +
+ +
+ +
+
+ + diff --git a/addons/html_builder/static/src/components/global_sidebar_component.js b/addons/html_builder/static/src/components/global_sidebar_component.js new file mode 100644 index 0000000000000..1537c2cd1acd2 --- /dev/null +++ b/addons/html_builder/static/src/components/global_sidebar_component.js @@ -0,0 +1,5 @@ +import { BackgroundShapeComponent } from "./background_shape_component"; + +export const globalSidebarComponent = { + BackgroundShapeComponent, +}; diff --git a/addons/html_builder/static/src/components/option_container.js b/addons/html_builder/static/src/components/option_container.js index 88fc262803e23..8449d2b8e0c77 100644 --- a/addons/html_builder/static/src/components/option_container.js +++ b/addons/html_builder/static/src/components/option_container.js @@ -1,4 +1,4 @@ -import { Component, useSubEnv, markup } from "@odoo/owl"; +import { Component, useState, useSubEnv, markup } from "@odoo/owl"; import { useService } from "@web/core/utils/hooks"; import { _t } from "@web/core/l10n/translation"; import { defaultBuilderComponents } from "../builder_components/default_builder_components"; @@ -6,10 +6,15 @@ import { globalBuilderOptions } from "../builder_components/global_builder_optio import { useVisibilityObserver, useApplyVisibility } from "../builder_components/utils"; import { DependencyManager } from "../plugins/dependency_manager"; import { getSnippetName } from "@html_builder/utils/utils"; +import { globalSidebarComponent } from "@html_builder/components/global_sidebar_component"; export class OptionsContainer extends Component { static template = "html_builder.OptionsContainer"; - static components = { ...defaultBuilderComponents, ...globalBuilderOptions }; + static components = { + ...defaultBuilderComponents, + ...globalBuilderOptions, + ...globalSidebarComponent, + }; static props = { snippetModel: { type: Object }, options: { type: Array }, @@ -21,11 +26,17 @@ export class OptionsContainer extends Component { setup() { this.notification = useService("notification"); + this.state = useState({ + componentToShow: undefined, + }); + useSubEnv({ dependencyManager: new DependencyManager(), getEditingElement: () => this.props.editingElement, getEditingElements: () => [this.props.editingElement], weContext: {}, + showComponent: this.showComponent.bind(this), + hideComponent: this.hideComponent.bind(this), }); useVisibilityObserver("content", useApplyVisibility("root")); } @@ -47,6 +58,14 @@ export class OptionsContainer extends Component { this.env.editor.shared["builder-options"].updateContainers(this.props.editingElement); } + showComponent(componentName) { + this.state.componentToShow = componentName; + } + + hideComponent() { + this.state.componentToShow = undefined; + } + toggleOverlayPreview(el, show) { if (show) { this.env.editor.shared.overlayButtons.hideOverlayButtons(); diff --git a/addons/html_builder/static/src/components/option_container.xml b/addons/html_builder/static/src/components/option_container.xml index 3d5924d9f49f6..47f0d60cb7b0a 100644 --- a/addons/html_builder/static/src/components/option_container.xml +++ b/addons/html_builder/static/src/components/option_container.xml @@ -33,7 +33,8 @@
- + + diff --git a/addons/html_builder/static/src/img/options/bg_shape.svg b/addons/html_builder/static/src/img/options/bg_shape.svg new file mode 100644 index 0000000000000..838ddc5320334 --- /dev/null +++ b/addons/html_builder/static/src/img/options/bg_shape.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/addons/html_builder/static/src/options/background_component.js b/addons/html_builder/static/src/options/background_component.js new file mode 100644 index 0000000000000..cc8411fe862cd --- /dev/null +++ b/addons/html_builder/static/src/options/background_component.js @@ -0,0 +1,32 @@ +import { Component } from "@odoo/owl"; +import { defaultBuilderComponents } from "../builder_components/default_builder_components"; +import { getBgImageURLFromEl } from "@html_builder/utils/utils_css"; +import { BackgroundShapeComponent } from "@html_builder/components/background_shape_component"; + +export class BackgroundComponent extends Component { + static template = "html_builder.BackgroundComponent"; + static components = { ...defaultBuilderComponents }; + static props = { + withColors: { type: Boolean }, + withImages: { type: Boolean }, + withColorCombinations: { type: Boolean }, + withGradient: { type: Boolean }, + withShapes: { type: Boolean, optional: true }, + }; + static defaultProps = { + withShapes: false, + }; + showWebShapeColorpicker() { + // TODO: double check the getBgImageURLFromEl(editingEl) + const editingEl = this.env.getEditingElement(); + const src = new URL(getBgImageURLFromEl(editingEl), window.location.origin); + return ( + src.origin === window.location.origin && + (src.pathname.startsWith("/html_editor/shape/") || + src.pathname.startsWith("/web_editor/shape/")) + ); + } + onClick() { + this.env.showComponent(BackgroundShapeComponent); + } +} diff --git a/addons/html_builder/static/src/options/background_component.xml b/addons/html_builder/static/src/options/background_component.xml new file mode 100644 index 0000000000000..f4140d8c9726c --- /dev/null +++ b/addons/html_builder/static/src/options/background_component.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + Add + + + + + + + + + + Cover + Repeat pattern + + + + + + + diff --git a/addons/html_builder/static/src/options/background_option.js b/addons/html_builder/static/src/options/background_option.js new file mode 100644 index 0000000000000..7fc7762b07eed --- /dev/null +++ b/addons/html_builder/static/src/options/background_option.js @@ -0,0 +1,396 @@ +import { Plugin } from "@html_editor/plugin"; +import { registry } from "@web/core/registry"; +import { pick } from "@web/core/utils/objects"; +import { removeOnImageChangeAttrs } from "@html_editor/utils/image_processing"; +import { + backgroundImageCssToParts, + backgroundImagePartsToCss, + getBgImageURLFromEl, + getBgImageURLFromURL, +} from "@html_builder/utils/utils_css"; +import { coreBuilderActions } from "../core_builder_action_plugin"; +// import { isMobileView } from "@html_builder/builder/utils/utils"; + +// const connectionShapes = [ +// { shape: "web_editor/Connections/01", label: "Connections 01" }, +// { shape: "web_editor/Connections/02", label: "Connections 02" }, +// { shape: "web_editor/Connections/03", label: "Connections 03" }, +// { shape: "web_editor/Connections/04", label: "Connections 04" }, +// { shape: "web_editor/Connections/05", label: "Connections 05" }, +// { shape: "web_editor/Connections/06", label: "Connections 06" }, +// ]; + +const replaceImage = { + load: function () { + return new Promise((resolve) => { + this.dependencies.media.openMediaDialog({ + onlyImages: true, + noDocuments: true, + noVideos: true, + save: (imageEl) => { + resolve(imageEl.getAttribute("src")); + }, + }); + }); + }, + apply: ({ editingElement, loadResult: imageSrc }) => { + // What was in background method of BackgroundImage option + setBackground(editingElement, imageSrc); + for (const attr of removeOnImageChangeAttrs) { + delete editingElement.dataset[attr]; + } + // TODO: handle the _onBackgroundChanged of BackgroundOptimize (previoulsy a trigger check if inherit or something) + }, +}; + +class BackgroundOptionPlugin extends Plugin { + static id = "BackgroundOption"; + static dependencies = ["media"]; + resources = { + builder_actions: this.getActions(), + }; + getActions() { + return { + applyShape: { + apply: ({ editingElement, param }) => { + const shapeData = this.getShapeData(editingElement); + const applyShapeParams = { + shape: param.shape, + colors: this.getImplicitColors(editingElement, shapeData.colors, param.url), + flip: [], + animated: param.animated, + shapeAnimationSpeed: shapeData.shapeAnimationSpeed, + }; + this.applyShape(editingElement, () => applyShapeParams); + }, + isApplied: ({ editingElement, param }) => { + const currentShapeApplied = this.getShapeData(editingElement).shape; + return currentShapeApplied === param.shape; + }, + }, + replaceImage: { + load: replaceImage.load.bind(this), + apply: replaceImage.apply.bind(this), + }, + toggleBgImage: { + load: replaceImage.load.bind(this), + apply: replaceImage.apply.bind(this), + clean: ({ editingElement }) => { + editingElement.querySelector(".o_we_bg_filter")?.remove(); + // TODO: use setWidgetValue instead of calling background directly when possible + replaceImage.apply.bind(this)({ + editingElement: editingElement, + loadResult: "", + }); + }, + isApplied: ({ editingElement }) => !!editingElement.style["background-image"], + }, + // toggleBgShape todo + toggleBgShape: { + apply: ({ editingElement }) => { + //TODO + }, + }, + toggleShape: { + apply: ({ editingElement }) => { + // // TODO: continue + // const previousSibling = editingElement.previousElementSibling; + // const possibleShapes = connectionShapes.map( + // (connectionShape) => connectionShape.key + // ); + // let shapeToSelect; + // if (previousSibling) { + // const previousShape = this.getShapeData(previousSibling).shape; + // shapeToSelect = possibleShapes.find( + // (shape, i) => possibleShapes[i - 1] === previousShape + // ); + // } + // // If there is no previous sibling, if the previous sibling had the + // // last shape selected or if the previous shape could not be found + // // in the possible shapes, default to the first shape. ([0] being no + // // shapes selected.) + // if (!shapeToSelect) { + // shapeToSelect = possibleShapes[1]; + // } + // // Only show on mobile by default if toggled from mobile view + // const showOnMobile = isMobileView(editingElement); + // this.trigger_up("snippet_edition_request", {exec: () => { + // // options for shape will only be available after _toggleShape() returned + // this._requestUserValueWidgets('bg_shape_opt')[0].enable(); + // }}); + // this.createShapeContainer(shapeToSelect); + // return this.applyShape(editingElement, () => ({ + // shape: shapeToSelect, + // colors: this._getImplicitColors(shapeToSelect), + // showOnMobile, + // })); + }, + clean: ({ editingElement }) => { + this.applyShape(editingElement, () => ({ shape: "" })); + }, + }, + backgroundType: { + apply: ({ editingElement }) => { + // TODO + }, + }, + backgroundPositionOverlay: { + apply: ({ editingElement }) => { + // TODO + }, + }, + }; + } + /** + * Creates and inserts a container for the shape with the right classes. + * + * @param {HTMLElement} editingElement + * @param {string} shape the shape name for which to create a container + */ + createShapeContainer(editingElement, shape) { + const shapeContainer = this.insertShapeContainer( + editingElement, + document.createElement("div") + ); + editingElement.style.setProperty("position", "relative"); + shapeContainer.className = `o_we_shape o_${shape.replace(/\//g, "_")}`; + return shapeContainer; + } + + /** + * Inserts or removes the given container at the right position in the + * document. + * + * @param {HTMLElement} editingElement + * @param {HTMLElement} [newContainer] container to insert, null to remove + */ + insertShapeContainer(editingElement, newContainer) { + const shapeContainerEl = editingElement.querySelector(":scope > .o_we_shape"); + if (shapeContainerEl) { + shapeContainerEl.remove(); + } + if (newContainer) { + const preShapeLayerElement = this.getLastPreShapeLayerElement(editingElement); + if (preShapeLayerElement) { + preShapeLayerElement.insertAdjacentElement("afterend", newContainer); + } else { + editingElement.prepend(newContainer); + } + } + return newContainer; + } + + getLastPreShapeLayerElement(editingElement) { + return editingElement.querySelector(":scope > .o_we_bg_filter"); + } + + /** + * Handles everything related to saving state before preview and restoring + * it after a preview or locking in the changes when not in preview. + * + * @param {HTMLElement} editingElement + * @param {function} computeShapeData function to compute the new shape data. + */ + applyShape(editingElement, computeShapeData) { + const curShapeData = editingElement.dataset.oeShapeData || {}; + const newShapeData = computeShapeData(); + // TODO: check if really work as curShapeData seems to be of form {"shape": blabla} + const { shape: curShape } = curShapeData; + const changedShape = newShapeData.shape !== curShape; + this.markShape(editingElement, newShapeData); + if (changedShape) { + // Need to rerender for correct number of colorpickers + // TODO check how to do + // this.rerender = true; + } + + // Updates/removes the shape container as needed and gives it the + // correct background shape + const json = editingElement.dataset.oeShapeData; + const { + shape, + colors, + flip = [], + animated = "false", + showOnMobile, + shapeAnimationSpeed, + } = json ? JSON.parse(json) : {}; + let shapeContainerEl = editingElement.querySelector(":scope > .o_we_shape"); + if (!shape) { + return this.insertShapeContainer(editingElement, null); + } + // When changing shape we want to reset the shape container (for transparency color) + if (changedShape) { + shapeContainerEl = this.createShapeContainer(editingElement, shape); + } + // Compat: remove old flip classes as flipping is now done inside the svg + shapeContainerEl.classList.remove("o_we_flip_x", "o_we_flip_y"); + + shapeContainerEl.classList.toggle("o_we_animated", animated === "true"); + if (colors || flip.length || parseFloat(shapeAnimationSpeed) !== 0) { + // Custom colors/flip/speed, overwrite shape that is set by the class + shapeContainerEl.style.setProperty( + "background-image", + `url("${this.getShapeSrc(editingElement)}")` + ); + shapeContainerEl.style.backgroundPosition = ""; + if (flip.length) { + let [xPos, yPos] = getComputedStyle(shapeContainerEl) + .backgroundPosition.split(" ") + .map((p) => parseFloat(p)); + // -X + 2*Y is a symmetry of X around Y, this is a symmetry around 50% + xPos = flip.includes("x") ? -xPos + 100 : xPos; + yPos = flip.includes("y") ? -yPos + 100 : yPos; + shapeContainerEl.style.backgroundPosition = `${xPos}% ${yPos}%`; + } + } else { + // Remove custom bg image and let the shape class set the bg shape + shapeContainerEl.style.setProperty("background-image", ""); + shapeContainerEl.style.setProperty("background-position", ""); + } + shapeContainerEl.classList.toggle("o_shape_show_mobile", !!showOnMobile); + } + /** + * Overwrites shape properties with the specified data. + * + * @param {HTMLElement} editingElement + * @param {Object} newData an object with the new data + */ + markShape(editingElement, newData) { + const defaultColors = this.getDefaultColors(editingElement); + const shapeData = Object.assign(this.getShapeData(editingElement), newData); + const areColorsDefault = Object.entries(shapeData.colors).every( + ([colorName, colorValue]) => + defaultColors[colorName] && + colorValue.toLowerCase() === defaultColors[colorName].toLowerCase() + ); + if (areColorsDefault) { + delete shapeData.colors; + } + if (!shapeData.shape) { + delete editingElement.dataset.oeShapeData; + } else { + editingElement.dataset.oeShapeData = JSON.stringify(shapeData); + } + } + /** + * Returns the src of the shape corresponding to the current parameters. + * + * @param {HTMLElement} editingElement + */ + getShapeSrc(editingElement) { + const { shape, colors, flip, shapeAnimationSpeed } = this.getShapeData(editingElement); + if (!shape) { + return ""; + } + const searchParams = Object.entries(colors).map(([colorName, colorValue]) => { + const encodedCol = encodeURIComponent(colorValue); + return `${colorName}=${encodedCol}`; + }); + if (flip.length) { + searchParams.push(`flip=${encodeURIComponent(flip.sort().join(""))}`); + } + if (Number(shapeAnimationSpeed)) { + searchParams.push(`shapeAnimationSpeed=${encodeURIComponent(shapeAnimationSpeed)}`); + } + return `/web_editor/shape/${encodeURIComponent(shape)}.svg?${searchParams.join("&")}`; + } + /** + * Returns the implicit colors for the currently selected shape. + * + * The implicit colors are use upon shape selection. They are computed as: + * - the default colors + * - patched with each set of colors of previous siblings shape + * - patched with the colors of the previously selected shape + * - filtered to only keep the colors involved in the current shape + * + * @param {HTMLElement} editingElement + * @param {String} shape identifier of the selected shape + * @param {Object} previousColors colors of the shape before its replacement + */ + getImplicitColors(editingElement, previousColors, selectedBackgroundUrl) { + const defaultColors = this.getShapeDefaultColors(selectedBackgroundUrl); + let colors = previousColors || {}; + let sibling = editingElement.previousElementSibling; + while (sibling) { + colors = Object.assign(this.getShapeData(sibling).colors || {}, colors); + sibling = sibling.previousElementSibling; + } + const defaultKeys = Object.keys(defaultColors); + colors = Object.assign(defaultColors, colors); + return pick(colors, ...defaultKeys); + } + /** + * Returns the default colors for the a shape in the selector. + * + * @param {String} selectedBackgroundUrl + */ + getShapeDefaultColors(selectedBackgroundUrl) { + const shapeSrc = selectedBackgroundUrl && getBgImageURLFromURL(selectedBackgroundUrl); + const url = new URL(shapeSrc, window.location.origin); + return Object.fromEntries(url.searchParams.entries()); + } + /** + * Returns the default colors for the currently selected shape. + * + * @param {HTMLElement} editingElement the element on which to read the shape + * data. + */ + getDefaultColors(editingElement) { + const shapeContainer = editingElement.querySelector(":scope > .o_we_shape").cloneNode(true); + shapeContainer.classList.add("d-none"); + // Needs to be in document for bg-image class to take effect + editingElement.ownerDocument.body.appendChild(shapeContainer); + shapeContainer.style.setProperty("background-image", ""); + const shapeSrc = shapeContainer && getBgImageURLFromEl(shapeContainer); + shapeContainer.remove(); + if (!shapeSrc) { + return {}; + } + const url = new URL(shapeSrc, window.location.origin); + return Object.fromEntries(url.searchParams.entries()); + } + + /** + * Retrieves current shape data from the target's dataset. + * + * @param {HTMLElement} editingElement the target on which to read the shape + * data. + */ + getShapeData(editingElement) { + const defaultData = { + shape: "", + colors: this.getDefaultColors(editingElement), + flip: [], + showOnMobile: false, + shapeAnimationSpeed: "0", + }; + const json = editingElement.dataset.oeShapeData; + return json ? Object.assign(defaultData, JSON.parse(json.replace(/'/g, '"'))) : defaultData; + } +} +registry.category("website-plugins").add(BackgroundOptionPlugin.id, BackgroundOptionPlugin); + +function setBackground(editingElement, backgroundURL) { + const parts = backgroundImageCssToParts(editingElement.style["background-image"]); + if (backgroundURL) { + parts.url = `url('${backgroundURL}')`; + editingElement.classList.add("oe_img_bg", "o_bg_img_center"); + } else { + delete parts.url; + editingElement.classList.remove("oe_img_bg", "o_bg_img_center", "o_modified_image_to_save"); + } + const combined = backgroundImagePartsToCss(parts); + // TODO: check this comment + // We use selectStyle so that if when a background image is removed the + // remaining image matches the o_cc's gradient background, it can be + // removed too. + // -> styleAction + + coreBuilderActions.styleAction.apply({ + editingElement: editingElement, + param: "background-image", + value: combined, + }); + // Check if really needed this.options.wysiwyg.odooEditor.editable.focus(); +} diff --git a/addons/html_builder/static/src/options/image_gallery_option.js b/addons/html_builder/static/src/options/image_gallery_option.js index 370172757cb75..1e12f27ddc3c6 100644 --- a/addons/html_builder/static/src/options/image_gallery_option.js +++ b/addons/html_builder/static/src/options/image_gallery_option.js @@ -29,6 +29,7 @@ class ImageGalleryOption extends Plugin { }), apply: ({ editingElement, loadResult: images }) => { addImages(images, editingElement); + // TODO: check if really needed this.dependencies.history.addStep(); }, }, diff --git a/addons/html_builder/static/src/options/process_steps_option.js b/addons/html_builder/static/src/options/process_steps_option.js index 7916ebbd08755..8e103149af0ed 100644 --- a/addons/html_builder/static/src/options/process_steps_option.js +++ b/addons/html_builder/static/src/options/process_steps_option.js @@ -1,4 +1,5 @@ import { defaultBuilderComponents } from "../builder_components/default_builder_components"; +import { BackgroundComponent } from "./background_component"; import { coreBuilderActions } from "@html_builder/core_builder_action_plugin"; import { applyFunDependOnSelectorAndExclude } from "@html_builder/options/utils"; import { Plugin } from "@html_editor/plugin"; @@ -7,12 +8,25 @@ import { registry } from "@web/core/registry"; class ProcessStepsOptionPlugin extends Plugin { static id = "ProcessStepsOption"; + static dependencies = ["media"]; selector = ".s_process_steps"; resources = { - builder_options: { - OptionComponent: ProcessStepsOption, - selector: this.selector, - }, + builder_options: [ + { + OptionComponent: ProcessStepsOption, + selector: this.selector, + }, + { + selector: ".s_process_step .s_process_step_number", + OptionComponent: BackgroundComponent, + props: { + withColors: true, + withImages: false, + withColorCombinations: false, + withGradient: true, + }, + }, + ], builder_actions: this.getActions(), content_updated_handlers: (rootEl) => applyFunDependOnSelectorAndExclude(reloadConnectors, rootEl, this.selector), diff --git a/addons/html_builder/static/src/options/section_background_option.js b/addons/html_builder/static/src/options/section_background_option.js index 602b2350933c5..50f496f467ae4 100644 --- a/addons/html_builder/static/src/options/section_background_option.js +++ b/addons/html_builder/static/src/options/section_background_option.js @@ -1,3 +1,4 @@ +import { BackgroundComponent } from "./background_component"; import { registry } from "@web/core/registry"; import { Plugin } from "@html_editor/plugin"; @@ -8,9 +9,21 @@ class SectionBackgroundOptionPlugin extends Plugin { static id = "SectionBackgroundOption"; resources = { builder_options: [ + // { + // template: "html_builder.SectionBackgroundOption", + // selector: "section", + // }, { - template: "html_builder.SectionBackgroundOption", selector: "section", + OptionComponent: BackgroundComponent, + props: { + withColors: true, + withImages: true, + // todo: handle with_videos + withShapes: true, + withGradient: true, + withColorCombinations: true, + }, }, ], }; diff --git a/addons/html_builder/static/src/utils/utils_css.js b/addons/html_builder/static/src/utils/utils_css.js index 25151a0f350c5..b2df41427da11 100644 --- a/addons/html_builder/static/src/utils/utils_css.js +++ b/addons/html_builder/static/src/utils/utils_css.js @@ -371,10 +371,19 @@ export function normalizeColor(color) { * @param {string} string a css value in the form 'url("...")' * @returns {string|false} the src of the image or false if not parsable */ -export function getBgImageURL(el) { +export function getBgImageURLFromEl(el) { const parts = backgroundImageCssToParts(window.getComputedStyle(el).backgroundImage); const string = parts.url || ""; - const match = string.match(/^url\((['"])(.*?)\1\)$/); + return getBgImageURLFromURL(string); +} +/** + * Parse an element's background-image's url. + * + * @param {string} string a css value in the form 'url("...")' + * @returns {string|false} the src of the image or false if not parsable + */ +export function getBgImageURLFromURL(url) { + const match = url.match(/^url\((['"])(.*?)\1\)$/); if (!match) { return ""; } diff --git a/addons/html_editor/static/src/utils/image_processing.js b/addons/html_editor/static/src/utils/image_processing.js index 40f04ab202397..f4fc7e8d497db 100644 --- a/addons/html_editor/static/src/utils/image_processing.js +++ b/addons/html_editor/static/src/utils/image_processing.js @@ -6,6 +6,20 @@ import { getAffineApproximation, getProjective } from "./perspective_utils"; // initializing the cropper to reuse the previous crop. export const cropperDataFields = ["x", "y", "width", "height", "rotate", "scaleX", "scaleY"]; export const isGif = (mimetype) => mimetype === "image/gif"; +const modifierFields = [ + 'filter', + 'quality', + 'mimetype', + 'glFilter', + 'originalId', + 'originalSrc', + 'resizeWidth', + 'aspectRatio', + "bgSrc", + "mimetypeBeforeConversion", +]; + +export const removeOnImageChangeAttrs = [...cropperDataFields, ...modifierFields]; // webgl color filters const _applyAll = (result, filter, filters) => { diff --git a/addons/web_editor/views/snippets.xml b/addons/web_editor/views/snippets.xml index 6066afe4e7d1a..99fd5681e3be2 100644 --- a/addons/web_editor/views/snippets.xml +++ b/addons/web_editor/views/snippets.xml @@ -107,6 +107,7 @@ t-att-data-css-compatible="css_compatible and 'true' or None"/> +