editingElements[0];
}
const weContext = {};
- const contextKeys = [
- "preview",
- "action",
- "actionParam",
- "classAction",
- "attributeAction",
- "dataAttributeAction",
- "styleAction",
- ];
- for (const key of contextKeys) {
+ for (const key of basicContainerBuilderComponentPropsNames) {
if (key in comp.props) {
weContext[key] = comp.props[key];
}
@@ -93,6 +84,43 @@ export function useDependencies(dependencies) {
return isDependenciesVisible;
}
+export function useIsActiveItem() {
+ const env = useEnv();
+ const listenedKeys = new Set();
+
+ function isActive(itemId) {
+ const isActiveFn = env.dependencyManager.get(itemId)?.isActive;
+ if (!isActiveFn) {
+ return false;
+ }
+ return isActiveFn();
+ }
+
+ const getState = () => {
+ const newState = {};
+ for (const itemId of listenedKeys) {
+ newState[itemId] = isActive(itemId);
+ }
+ return newState;
+ };
+ const state = useDomState(getState);
+ const listener = () => {
+ const newState = getState();
+ Object.assign(state, newState);
+ };
+ env.dependencyManager.addEventListener("dependency-updated", listener);
+ onWillDestroy(() => {
+ env.dependencyManager.removeEventListener("dependency-updated", listener);
+ });
+ return function isActiveItem(itemId) {
+ listenedKeys.add(itemId);
+ if (state[itemId] === undefined) {
+ return isActive(itemId);
+ }
+ return state[itemId];
+ };
+}
+
export function useSelectableComponent(id, { onItemChange } = {}) {
useBuilderComponent();
const selectableItems = [];
@@ -202,7 +230,9 @@ 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 inheritedActionIds =
+ comp.props.inheritedActions || comp.env.weContext.inheritedActions || [];
const hasPreview =
comp.props.preview === true ||
(comp.props.preview === undefined && comp.env.weContext.preview !== false);
@@ -254,8 +284,8 @@ export function useClickableBuilderComponent() {
function callApply(applySpecs) {
comp.env.selectableContext?.cleanSelectedItem(applySpecs);
- const cleans = comp.props.inheritedActions
- ?.map((actionId) => comp.env.dependencyManager.get(actionId).cleanSelectedItem)
+ const cleans = inheritedActionIds
+ .map((actionId) => comp.env.dependencyManager.get(actionId).cleanSelectedItem)
.filter(Boolean);
for (const clean of new Set(cleans)) {
clean(applySpecs);
@@ -434,7 +464,7 @@ export function useVisibilityObserver(contentName, callback) {
export const basicContainerBuilderComponentProps = {
applyTo: { type: String, optional: true },
preview: { type: Boolean, optional: true },
- dependencies: { type: [String, Array], optional: true },
+ inheritedActions: { type: Array, element: String, optional: true },
// preview: { type: Boolean, optional: true },
// reloadPage: { type: Boolean, optional: true },
@@ -447,6 +477,7 @@ export const basicContainerBuilderComponentProps = {
dataAttributeAction: { type: String, optional: true },
styleAction: { type: String, optional: true },
};
+const basicContainerBuilderComponentPropsNames = Object.keys(basicContainerBuilderComponentProps);
const validateIsNull = { validate: (value) => value === null };
export const clickableBuilderComponentProps = {
...basicContainerBuilderComponentProps,
@@ -467,6 +498,9 @@ export const clickableBuilderComponentProps = {
};
export function getAllActionsAndOperations(comp) {
+ const inheritedActionIds =
+ comp.props.inheritedActions || comp.env.weContext.inheritedActions || [];
+
function getActionsSpecs(actions, userValueInput) {
const getAction = comp.env.editor.shared.builderActions.getAction;
const specs = [];
@@ -522,15 +556,16 @@ export function getAllActionsAndOperations(comp) {
if (actionId) {
actions.push({ actionId, actionParam, actionValue });
}
- const inheritedActions = comp.props.inheritedActions
- ?.map(
- (actionId) =>
- comp.env.dependencyManager
- // The dependency might not be loaded yet.
- .get(actionId)
- ?.getActions?.() || []
- )
- .flat();
+ const inheritedActions =
+ inheritedActionIds
+ .map(
+ (actionId) =>
+ comp.env.dependencyManager
+ // The dependency might not be loaded yet.
+ .get(actionId)
+ ?.getActions?.() || []
+ )
+ .flat() || [];
return actions.concat(inheritedActions || []);
}
function callOperation(fn, params = {}) {
diff --git a/addons/html_builder/static/src/components/option_container.js b/addons/html_builder/static/src/components/option_container.js
index 88fc262803e23..6a7870a07b941 100644
--- a/addons/html_builder/static/src/components/option_container.js
+++ b/addons/html_builder/static/src/components/option_container.js
@@ -3,7 +3,11 @@ import { useService } from "@web/core/utils/hooks";
import { _t } from "@web/core/l10n/translation";
import { defaultBuilderComponents } from "../builder_components/default_builder_components";
import { globalBuilderOptions } from "../builder_components/global_builder_options";
-import { useVisibilityObserver, useApplyVisibility } from "../builder_components/utils";
+import {
+ useVisibilityObserver,
+ useApplyVisibility,
+ useIsActiveItem,
+} from "../builder_components/utils";
import { DependencyManager } from "../plugins/dependency_manager";
import { getSnippetName } from "@html_builder/utils/utils";
@@ -27,6 +31,7 @@ export class OptionsContainer extends Component {
getEditingElements: () => [this.props.editingElement],
weContext: {},
});
+ this.isActiveItem = useIsActiveItem();
useVisibilityObserver("content", useApplyVisibility("root"));
}
diff --git a/addons/html_builder/static/src/options/accordion_option.xml b/addons/html_builder/static/src/options/accordion_option.xml
index e64e452b12582..0f3da06c64b31 100644
--- a/addons/html_builder/static/src/options/accordion_option.xml
+++ b/addons/html_builder/static/src/options/accordion_option.xml
@@ -19,7 +19,7 @@
Highlight Active
-
+
diff --git a/addons/html_builder/static/src/options/animate_option.js b/addons/html_builder/static/src/options/animate_option.js
index 4977ab127019a..fff2aabf8a032 100644
--- a/addons/html_builder/static/src/options/animate_option.js
+++ b/addons/html_builder/static/src/options/animate_option.js
@@ -1,9 +1,10 @@
import { Plugin } from "@html_editor/plugin";
-import { Component } from "@odoo/owl";
+import { Component, useEnv } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { defaultBuilderComponents } from "../builder_components/default_builder_components";
import { withSequence } from "@html_editor/utils/resource";
-
+import { useDomState, useIsActiveItem } from "../builder_components/utils";
+import { getScrollingElement } from "@web/core/utils/scrolling";
class AnimateOptionPlugin extends Plugin {
static id = "AnimateOption";
resources = {
@@ -13,27 +14,293 @@ class AnimateOptionPlugin extends Plugin {
selector: ".o_animable, section .row > div, img, .fa, .btn, .o_animated_text",
exclude:
"[data-oe-xpath], .o_not-animable, .s_col_no_resize.row > div, .s_col_no_resize",
+ props: {
+ getDirectionsItems: this.getDirectionsItems.bind(this),
+ getEffectsItems: this.getEffectsItems.bind(this),
+ },
// todo: to implement
// textSelector: ".o_animated_text",
}),
],
builder_actions: this.getActions(),
+ normalize_handlers: this.normalize.bind(this),
+ clean_for_save_handlers: this.cleanForSave.bind(this),
};
+
+ setup() {
+ this.scrollingElement = getScrollingElement(this.document);
+ }
+
+ getEffectsItems(isActiveItem) {
+ const isOnAppearance = () => isActiveItem("animation_on_appearance_opt");
+ return [
+ { className: "o_anim_fade_in", label: "Fade" },
+ { className: "o_anim_slide_in", label: "Slide", directionClass: "o_anim_from_right" },
+ { className: "o_anim_bounce_in", label: "Bounce" },
+ { className: "o_anim_rotate_in", label: "Rotate" },
+ { className: "o_anim_zoom_out", label: "Zoom Out" },
+ { className: "o_anim_zoom_in", label: "Zoom In" },
+ { className: "o_anim_flash", label: "Flash", check: isOnAppearance },
+ { className: "o_anim_pulse", label: "Pulse", check: isOnAppearance },
+ { className: "o_anim_shake", label: "Shake", check: isOnAppearance },
+ { className: "o_anim_tada", label: "Tada", check: isOnAppearance },
+ { className: "o_anim_flip_in_x", label: "Flip-In-X", check: isOnAppearance },
+ { className: "o_anim_flip_in_y", label: "Flip-In-Y", check: isOnAppearance },
+ ];
+ }
+ getDirectionsItems() {
+ const isNotSlideIn = (editingElement) =>
+ !editingElement.classList.contains("o_anim_slide_in");
+ const isRotate = (editingElement) => editingElement.classList.contains("o_anim_rotate_in");
+ const isNotRotate = (editingElement) => !isRotate(editingElement);
+
+ return [
+ { className: "", label: "In place", check: isNotSlideIn },
+
+ { className: "o_anim_from_right", label: "From right", check: isNotRotate },
+ { className: "o_anim_from_left", label: "From left", check: isNotRotate },
+ { className: "o_anim_from_bottom", label: "From bottom", check: isNotRotate },
+ { className: "o_anim_from_top", label: "From top", check: isNotRotate },
+
+ { className: "o_anim_from_top_right", label: "From top right", check: isRotate },
+ { className: "o_anim_from_top_left", label: "From top left", check: isRotate },
+ { className: "o_anim_from_bottom_right", label: "From bottom right", check: isRotate },
+ { className: "o_anim_from_bottom_left", label: "From bottom left", check: isRotate },
+ ];
+ }
+
getActions() {
+ const effectWithFadein = ["onAppearance", "onScroll"];
return {
setAnimationMode: {
+ // todo: to remove after having the commit of louis
isApplied: () => true,
+ clean: ({ editingElement, value: effectName, nextAction }) => {
+ this.scrollingElement.classList.remove("o_wanim_overflow_xy_hidden");
+ editingElement.classList.remove(
+ "o_animating",
+ "o_animate_both_scroll",
+ "o_visible",
+ "o_animated",
+ "o_animate_out"
+ );
+ editingElement.style.animationDelay = "";
+ editingElement.style.animationPlayState = "";
+ editingElement.style.animationName = "";
+ editingElement.style.visibility = "";
+
+ if (effectName === "onScroll") {
+ delete editingElement.dataset.scrollZoneStart;
+ delete editingElement.dataset.scrollZoneEnd;
+ }
+ if (effectName === "onHover") {
+ // todo: to implement
+ // this.trigger_up("option_update", {
+ // optionName: "ImageTools",
+ // name: "disable_hover_effect",
+ // });
+ }
+
+ const isNextEffectFadein = effectWithFadein.includes(nextAction.value);
+ if (!isNextEffectFadein) {
+ this.removeEffectAndDirectionClasses(editingElement.classList);
+ editingElement.style.setProperty("--wanim-intensity", "");
+ editingElement.style.animationDuration = "";
+ this.setImagesLazyLoading(editingElement, true);
+ }
+ },
+ apply: ({ editingElement, value: effectName }) => {
+ if (effectWithFadein.includes(effectName)) {
+ editingElement.classList.add("o_anim_fade_in");
+ this.setImagesLazyLoading(editingElement, false);
+ }
+ if (effectName === "onScroll") {
+ editingElement.dataset.scrollZoneStart = 0;
+ editingElement.dataset.scrollZoneEnd = 100;
+ }
+ if (effectName === "onHover") {
+ // todo: to implement
+ // Pause the history until the hover effect is applied in
+ // "setImgShapeHoverEffect". This prevents saving the intermediate
+ // steps done (in a tricky way) up to that point.
+ // this.options.wysiwyg.odooEditor.historyPauseSteps();
+ // this.trigger_up("option_update", {
+ // optionName: "ImageTools",
+ // name: "enable_hover_effect",
+ // });
+ }
+ },
+ },
+ setAnimateIntensity: {
+ getValue: ({ editingElement }) => {
+ const intensity = parseInt(
+ this.document.defaultView
+ .getComputedStyle(editingElement)
+ .getPropertyValue("--wanim-intensity")
+ );
+ return intensity;
+ },
+ apply: ({ editingElement, value }) => {},
+ },
+ forceAnimation: {
+ // todo: to remove after having the commit of louis
+ isActive: () => true,
apply: () => {
console.warn("todo");
},
},
+ setAnimationEffect: {
+ isApplied({ editingElement, value: className }) {
+ return editingElement.classList.contains(className);
+ },
+ clean: ({ editingElement }) => {
+ const classNames = this.getEffectsItems()
+ .map(({ className }) => className)
+ .concat(this.getDirectionsItems().map(({ className }) => className));
+ for (const className of classNames) {
+ if (editingElement.classList.contains(className)) {
+ editingElement.classList.remove(className);
+ }
+ }
+ },
+ apply: ({ editingElement, param: directionClassName, value: effectClassName }) => {
+ if (directionClassName) {
+ editingElement.classList.add(directionClassName);
+ }
+ editingElement.classList.add(effectClassName);
+ },
+ },
};
}
+
+ removeEffectAndDirectionClasses(targetClassList) {
+ const classes = this.getEffectsItems()
+ .map(({ className }) => className)
+ .concat(
+ this.getDirectionsItems()
+ .map(({ className }) => className)
+ .filter(Boolean)
+ );
+
+ const classesToRemove = intersect(classes, [...targetClassList]);
+ for (const className of classesToRemove) {
+ targetClassList.remove(className);
+ }
+ }
+
+ /**
+ * Removes or adds the lazy loading on images because animated images can
+ * appear before or after their parents and cause bugs in the animations.
+ * To put "lazy" back on the "loading" attribute, we simply remove the
+ * attribute as it is automatically added on page load.
+ *
+ * @private
+ * @param {Boolean} isLazy
+ */
+ setImagesLazyLoading(editingElement, isLazy) {
+ const imgEls = editingElement.matches("img")
+ ? [editingElement]
+ : editingElement.querySelectorAll("img");
+ for (const imgEl of imgEls) {
+ if (isLazy) {
+ // Let the automatic system add the loading attribute
+ imgEl.removeAttribute("loading");
+ } else {
+ imgEl.loading = "eager";
+ }
+ }
+ }
+
+ normalize(root) {
+ const previewEls = [...root.querySelectorAll(".o_animate_preview")];
+ if (root.classList.contains("o_animate_preview")) {
+ previewEls.push(root);
+ }
+ for (const el of previewEls) {
+ if (el.classList.contains("o_animate")) {
+ el.classList.remove("o_animate_preview");
+ }
+ }
+
+ const animateEls = [...root.querySelectorAll(".o_animate")];
+ if (root.classList.contains("o_animate")) {
+ animateEls.push(root);
+ }
+ for (const el of animateEls) {
+ if (!el.classList.contains("o_animate_preview")) {
+ el.classList.add("o_animate_preview");
+ }
+ }
+ }
+ cleanForSave({ root }) {
+ for (const el of root.querySelectorAll(".o_animate_preview")) {
+ el.classList.remove("o_animate_preview");
+ }
+ }
}
registry.category("website-plugins").add(AnimateOptionPlugin.id, AnimateOptionPlugin);
class AnimateOption extends Component {
static template = "html_builder.AnimateOption";
static components = { ...defaultBuilderComponents };
- static props = {};
+ static props = {
+ getDirectionsItems: Function,
+ getEffectsItems: Function,
+ };
+
+ setup() {
+ this.isActiveItem = useIsActiveItem();
+
+ this.state = useDomState((editingElement) => {
+ const hasAnimateClass = editingElement.classList.contains("o_animate");
+ return {
+ hasAnimateClass: hasAnimateClass,
+ canHover: editingElement.tagName === "IMG",
+ isLimitedAnimation: this.limitedAnimations.some((className) =>
+ editingElement.classList.contains(className)
+ ),
+ showIntensity: this.shouldShowIntensity(editingElement, hasAnimateClass),
+ effectItems: this.props.getEffectsItems(this.isActiveItem),
+ directionItems: this.props
+ .getDirectionsItems(editingElement)
+ .filter((i) => !i.check || i.check(editingElement)),
+ isInDropdown: editingElement.closest(".dropdown"),
+ };
+ });
+ }
+ get limitedAnimations() {
+ // Animations for which the "On Scroll" and "Direction" options are not
+ // available.
+ return [
+ "o_anim_flash",
+ "o_anim_pulse",
+ "o_anim_shake",
+ "o_anim_tada",
+ "o_anim_flip_in_x",
+ "o_anim_flip_in_y",
+ ];
+ }
+
+ shouldShowIntensity(editingElement, hasAnimateClass) {
+ if (!hasAnimateClass) {
+ return false;
+ }
+ if (!editingElement.classList.contains("o_anim_fade_in")) {
+ return true;
+ }
+
+ const possibleDirections = this.props
+ .getDirectionsItems()
+ .map((i) => i.className)
+ .filter(Boolean);
+ const hasDirection = possibleDirections.some((direction) =>
+ editingElement.classList.contains(direction)
+ );
+
+ return hasDirection;
+ }
+}
+
+function intersect(a, b) {
+ return a.filter((value) => b.includes(value));
}
diff --git a/addons/html_builder/static/src/options/animate_option.xml b/addons/html_builder/static/src/options/animate_option.xml
index bff5b8ca41a16..854f37d33b6fe 100644
--- a/addons/html_builder/static/src/options/animate_option.xml
+++ b/addons/html_builder/static/src/options/animate_option.xml
@@ -3,7 +3,7 @@
-
+
None
@@ -17,9 +17,56 @@
id="'animation_on_scroll_opt'">On Scroll
On Hover
+ id="'animation_on_hover_opt'"
+ t-if="state.canHover">On Hover
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ First Time Only
+ Every Time
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/html_builder/static/src/options/block_alignment_option.xml b/addons/html_builder/static/src/options/block_alignment_option.xml
index 9e9558b11c751..5b1d83a48f8a3 100644
--- a/addons/html_builder/static/src/options/block_alignment_option.xml
+++ b/addons/html_builder/static/src/options/block_alignment_option.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/addons/html_builder/static/src/options/blockquote_option.xml b/addons/html_builder/static/src/options/blockquote_option.xml
index 07de97889caf8..6b8f0929f6227 100644
--- a/addons/html_builder/static/src/options/blockquote_option.xml
+++ b/addons/html_builder/static/src/options/blockquote_option.xml
@@ -25,7 +25,7 @@
-
+
diff --git a/addons/html_builder/static/src/options/image_tool_option.js b/addons/html_builder/static/src/options/image_tool_option.js
index 36c13cbabac6e..064b7b8f97aa5 100644
--- a/addons/html_builder/static/src/options/image_tool_option.js
+++ b/addons/html_builder/static/src/options/image_tool_option.js
@@ -11,6 +11,7 @@ import { defaultBuilderComponents } from "../builder_components/default_builder_
import { AddElementOption } from "./add_element_option";
import { SpacingOption } from "./spacing_option";
import { Plugin } from "@html_editor/plugin";
+import { useIsActiveItem } from "../builder_components/utils";
class ImageToolOptionPlugin extends Plugin {
static id = "ImageToolOption";
@@ -129,6 +130,9 @@ class ImageToolOption extends Component {
static template = "html_builder.ImageToolOption";
static components = { ...defaultBuilderComponents, SpacingOption, AddElementOption };
static props = {};
+ setup() {
+ this.isActiveItem = useIsActiveItem();
+ }
}
/**
diff --git a/addons/html_builder/static/src/options/image_tool_option.xml b/addons/html_builder/static/src/options/image_tool_option.xml
index e15b9fb284242..6c1bcddf8be42 100644
--- a/addons/html_builder/static/src/options/image_tool_option.xml
+++ b/addons/html_builder/static/src/options/image_tool_option.xml
@@ -17,9 +17,9 @@
- Reset
+ Reset
- Reset
+ Reset
diff --git a/addons/html_builder/static/src/options/media_list_option.xml b/addons/html_builder/static/src/options/media_list_option.xml
index 10373fcbddfeb..0ff4d4eb35921 100644
--- a/addons/html_builder/static/src/options/media_list_option.xml
+++ b/addons/html_builder/static/src/options/media_list_option.xml
@@ -36,12 +36,12 @@
-
+
-
+
@@ -51,12 +51,12 @@
-
+
-
+
@@ -67,6 +67,3 @@
-
-
-
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..4d530470ff85b 100644
--- a/addons/html_builder/static/src/options/process_steps_option.js
+++ b/addons/html_builder/static/src/options/process_steps_option.js
@@ -4,6 +4,7 @@ import { applyFunDependOnSelectorAndExclude } from "@html_builder/options/utils"
import { Plugin } from "@html_editor/plugin";
import { Component } from "@odoo/owl";
import { registry } from "@web/core/registry";
+import { useIsActiveItem } from "../builder_components/utils";
class ProcessStepsOptionPlugin extends Plugin {
static id = "ProcessStepsOption";
@@ -73,6 +74,7 @@ export class ProcessStepsOption extends Component {
static props = {};
setup() {
+ this.isActiveItem = useIsActiveItem();
this.connectorOptionParams = connectorOptionParams;
}
diff --git a/addons/html_builder/static/src/options/process_steps_option.xml b/addons/html_builder/static/src/options/process_steps_option.xml
index c81b638565cfa..1c3d1977c8bb4 100644
--- a/addons/html_builder/static/src/options/process_steps_option.xml
+++ b/addons/html_builder/static/src/options/process_steps_option.xml
@@ -10,7 +10,7 @@
diff --git a/addons/html_builder/static/src/options/progress_bar_option.xml b/addons/html_builder/static/src/options/progress_bar_option.xml
index 48c34a5b451a1..524477780842c 100644
--- a/addons/html_builder/static/src/options/progress_bar_option.xml
+++ b/addons/html_builder/static/src/options/progress_bar_option.xml
@@ -20,7 +20,7 @@
-
+
diff --git a/addons/html_builder/static/src/options/separator_option.xml b/addons/html_builder/static/src/options/separator_option.xml
index 710716f5f85b7..c2f464a835e6f 100644
--- a/addons/html_builder/static/src/options/separator_option.xml
+++ b/addons/html_builder/static/src/options/separator_option.xml
@@ -11,7 +11,7 @@
100%
-
+
diff --git a/addons/html_builder/static/src/options/shadow_option.js b/addons/html_builder/static/src/options/shadow_option.js
index 83ed23fcaf685..afb893f1c9b8c 100644
--- a/addons/html_builder/static/src/options/shadow_option.js
+++ b/addons/html_builder/static/src/options/shadow_option.js
@@ -2,6 +2,7 @@ import { Plugin } from "@html_editor/plugin";
import { Component } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { defaultBuilderComponents } from "../builder_components/default_builder_components";
+import { useIsActiveItem } from "@html_builder/builder_components/utils";
const shadowClass = "shadow";
@@ -9,6 +10,9 @@ export class ShadowOption extends Component {
static template = "html_builder.ShadowOption";
static components = { ...defaultBuilderComponents };
static props = {};
+ setup() {
+ this.isActiveItem = useIsActiveItem();
+ }
}
class ShadowOptionPlugin extends Plugin {
diff --git a/addons/html_builder/static/src/options/shadow_option.xml b/addons/html_builder/static/src/options/shadow_option.xml
index 86ae15b3f65d5..0d8b1fcf556b4 100644
--- a/addons/html_builder/static/src/options/shadow_option.xml
+++ b/addons/html_builder/static/src/options/shadow_option.xml
@@ -9,7 +9,7 @@
-
+
diff --git a/addons/html_builder/static/tests/custom_tab/builder_components/builder_button.test.js b/addons/html_builder/static/tests/custom_tab/builder_components/builder_button.test.js
index bd4519612144e..5813c5f3b9e20 100644
--- a/addons/html_builder/static/tests/custom_tab/builder_components/builder_button.test.js
+++ b/addons/html_builder/static/tests/custom_tab/builder_components/builder_button.test.js
@@ -105,12 +105,38 @@ test("prevent preview of a specific action", async () => {
await setupWebsiteBuilder(`b
`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
- const c = contains("[data-action-id='customAction']");
- await c.hover();
+ await contains("[data-action-id='customAction']").hover();
expect.verifySteps([]);
- await c.click();
+ await contains("[data-action-id='customAction']").click();
expect.verifySteps(["customAction"]);
});
+test("should toggle when not in a BuilderButtonGroup", async () => {
+ addOption({
+ selector: ".test-options-target",
+ template: xml``,
+ });
+ await setupWebsiteBuilder(`b
`);
+ await contains(":iframe .test-options-target").click();
+ await contains("[data-class-action='c1']").click();
+ expect(":iframe .test-options-target").toHaveClass("test-options-target c1");
+ await contains("[data-class-action='c1']").click();
+ expect(":iframe .test-options-target").not.toHaveClass("test-options-target c1");
+});
+test("should not toggle when in a BuilderButtonGroup", async () => {
+ addOption({
+ selector: ".test-options-target",
+ template: xml`
+
+
+ `,
+ });
+ await setupWebsiteBuilder(`b
`);
+ await contains(":iframe .test-options-target").click();
+ await contains("[data-class-action='c1']").click();
+ expect(":iframe .test-options-target").toHaveClass("test-options-target c1");
+ await contains("[data-class-action='c1']").click();
+ expect(":iframe .test-options-target").toHaveClass("test-options-target c1");
+});
test("clean another action", async () => {
addOption({
selector: ".test-options-target",
@@ -371,6 +397,34 @@ describe("inherited actions", () => {
"customAction3 apply myParam3 myValue3",
]);
});
+ test("inherit actions for another button (from the context)", async () => {
+ addActionOption({
+ customAction1: makeAction(1).action,
+ customAction2: makeAction(2).action,
+ customAction3: makeAction(3, { isApplied: falsy }).action,
+ });
+ addOption({
+ selector: ".test-options-target",
+ template: xml`
+
+ MyAction1
+ MyAction2
+
+
+ MyAction2
+
+ `,
+ });
+ await setupWebsiteBuilder(`a
`);
+ await contains(":iframe .test-options-target").click();
+ await contains("[data-action-id='customAction3']").hover();
+ expect.verifySteps([
+ "customAction1 clean myParam1 myValue1",
+
+ "customAction3 apply myParam3 myValue3",
+ "customAction1 apply myParam1 myValue1",
+ ]);
+ });
});
describe("Operation", () => {
function makeAsyncActionItem(actionName) {
diff --git a/addons/html_builder/static/tests/custom_tab/builder_components/builder_select_item.test.js b/addons/html_builder/static/tests/custom_tab/builder_components/builder_select_item.test.js
index 47b76438cd7b9..b1dc4f6148ec7 100644
--- a/addons/html_builder/static/tests/custom_tab/builder_components/builder_select_item.test.js
+++ b/addons/html_builder/static/tests/custom_tab/builder_components/builder_select_item.test.js
@@ -56,10 +56,10 @@ test("set the label of the select from the active select item and be updated on
expect(".options-container").toBeDisplayed();
expect(".we-bg-options-container .dropdown").toHaveText("A");
await contains(".we-bg-options-container .dropdown").click();
- await contains(".o-overlay-item [data-attribute-action-value-id='b']").click();
+ await contains(".o-overlay-item [data-attribute-action-value='b']").click();
expect(".we-bg-options-container .dropdown").toHaveText("B");
await animationFrame();
- expect(".o-overlay-item [data-attribute-action-value-id='b']").not.toBeDisplayed();
+ expect(".o-overlay-item [data-attribute-action-value='b']").not.toBeDisplayed();
await contains(".o-snippets-top-actions .fa-undo").click();
expect(".we-bg-options-container .dropdown").toHaveText("A");
await contains(".o-snippets-top-actions .fa-repeat").click();
diff --git a/addons/html_builder/static/tests/custom_tab/misc.test.js b/addons/html_builder/static/tests/custom_tab/misc.test.js
index af8c70fcba034..c6719347adf72 100644
--- a/addons/html_builder/static/tests/custom_tab/misc.test.js
+++ b/addons/html_builder/static/tests/custom_tab/misc.test.js
@@ -427,15 +427,15 @@ test("useDomState callback shouldn't be called when the editingElement is remove
expect.verifySteps(["useDomState 1"]);
});
-describe("dependencies", () => {
+describe("isActiveItem", () => {
test("a button should not be visible if its dependency isn't (with undo)", async () => {
addOption({
selector: ".test-options-target",
template: xml`
b1
b2
- b3
- b4
+ b3
+ b4
`,
});
await setupWebsiteBuilder(`b
`);
@@ -495,8 +495,8 @@ describe("dependencies", () => {
x
y
- b1
- b2
+ b1
+ b2
`,
});
await setupWebsiteBuilder(`a
`);
@@ -523,7 +523,7 @@ describe("dependencies", () => {
selector: ".test-options-target",
template: xml`
b1
- b3
+ b3
`,
});
await setupWebsiteBuilder(`b
`);
@@ -544,8 +544,8 @@ describe("dependencies", () => {
addOption({
selector: ".test-options-target",
template: xml`
- b1
- b2
+ b1
+ b2
b3
diff --git a/addons/html_builder/static/tests/helpers.js b/addons/html_builder/static/tests/helpers.js
index 582b4ecb7aabc..749d6d4bb2690 100644
--- a/addons/html_builder/static/tests/helpers.js
+++ b/addons/html_builder/static/tests/helpers.js
@@ -49,7 +49,13 @@ export function defineWebsiteModels() {
export async function setupWebsiteBuilder(
websiteContent,
- { snippets, openEditor = true, loadIframeBundles = false, hasToCreateWebsite = true } = {}
+ {
+ snippets,
+ openEditor = true,
+ loadIframeBundles = false,
+ hasToCreateWebsite = true,
+ styleContent,
+ } = {}
) {
if (hasToCreateWebsite) {
const pyEnv = await startServer();
@@ -60,6 +66,12 @@ export async function setupWebsiteBuilder(
let resolveIframeLoaded = () => {};
const iframeLoaded = new Promise((resolve) => {
resolveIframeLoaded = (el) => {
+ const iframe = el;
+ if (styleContent) {
+ const style = iframe.contentDocument.createElement("style");
+ style.innerHTML = styleContent;
+ iframe.contentDocument.body.appendChild(style);
+ }
resolve(el);
};
});
diff --git a/addons/html_builder/static/tests/options/animate_option.test.js b/addons/html_builder/static/tests/options/animate_option.test.js
new file mode 100644
index 0000000000000..00681bfcbb2d0
--- /dev/null
+++ b/addons/html_builder/static/tests/options/animate_option.test.js
@@ -0,0 +1,180 @@
+import { describe, expect, test } from "@odoo/hoot";
+import { contains } from "@web/../tests/web_test_helpers";
+import { defineWebsiteModels, setupWebsiteBuilder } from "../helpers";
+
+defineWebsiteModels();
+
+const base64Img =
+ "data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUA\n AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO\n 9TXL0Y4OHwAAAABJRU5ErkJggg==";
+
+const styleContent = `
+.o_animate {
+ animation-duration: 1s;
+ --wanim-intensity: 50;
+}
+`;
+
+test("visibility of animation animation=none", async () => {
+ await setupWebsiteBuilder(`
+
+

+
+ `);
+ await contains(":iframe .test-options-target img").click();
+
+ expect(".options-container [data-label='Effect']").not.toBeVisible();
+ expect(".options-container [data-label='Direction']").not.toBeVisible();
+ expect(".options-container [data-label='Trigger']").not.toBeVisible();
+ expect(".options-container [data-label='Intensity']").not.toBeVisible();
+ expect(".options-container [data-label='Start After']").not.toBeVisible();
+ expect(".options-container [data-label='Duration']").not.toBeVisible();
+});
+describe("onAppearance", () => {
+ test("visibility of animation animation=onAppearance", async () => {
+ await setupWebsiteBuilder(
+ `
+
+

+
+ `,
+ { styleContent }
+ );
+ await contains(":iframe .test-options-target img").click();
+
+ await contains(".options-container [data-label='Animation'] .dropdown-toggle").click();
+ await contains(".o-dropdown--menu [data-action-value='onAppearance']").click();
+ expect(".options-container [data-label='Animation'] .o-dropdown").toHaveText(
+ "On Appearance"
+ );
+
+ expect(".options-container [data-label='Effect'] .o-dropdown").toHaveText("Fade");
+ expect(".options-container [data-label='Direction'] .o-dropdown").toHaveText("In place");
+ expect(".options-container [data-label='Trigger'] .o-dropdown").toHaveText(
+ "First Time Only"
+ );
+ expect(".options-container [data-label='Intensity']").not.toBeVisible();
+ expect(".options-container [data-label='Start After'] input").toHaveValue("0");
+ expect(".options-container [data-label='Duration'] input").toHaveValue("1");
+ });
+ test("visibility of animation animation=onAppearance effect=slide", async () => {
+ await setupWebsiteBuilder(
+ `
+
+

+
+ `,
+ { styleContent }
+ );
+ await contains(":iframe .test-options-target img").click();
+
+ await contains(".options-container [data-label='Animation'] .dropdown-toggle").click();
+ await contains(".o-dropdown--menu [data-action-value='onAppearance']").click();
+
+ await contains(".options-container [data-label='Effect'] .dropdown-toggle").click();
+ await contains(".o-dropdown--menu [data-action-value='o_anim_slide_in']").click();
+ expect(".options-container [data-label='Effect'] .o-dropdown").toHaveText("Slide");
+
+ expect(".options-container [data-label='Direction'] .o-dropdown").toHaveText("From right");
+ expect(".options-container [data-label='Trigger'] .o-dropdown").toHaveText(
+ "First Time Only"
+ );
+ expect(".options-container [data-label='Intensity'] input").toHaveValue(50);
+ expect(".options-container [data-label='Start After'] input").toHaveValue("0");
+ expect(".options-container [data-label='Duration'] input").toHaveValue("1");
+ });
+ test("visibility of animation animation=onAppearance effect=bounce", async () => {
+ await setupWebsiteBuilder(
+ `
+
+

+
+ `,
+ { styleContent }
+ );
+ await contains(":iframe .test-options-target img").click();
+
+ await contains(".options-container [data-label='Animation'] .dropdown-toggle").click();
+ await contains(".o-dropdown--menu [data-action-value='onAppearance']").click();
+
+ await contains(".options-container [data-label='Effect'] .dropdown-toggle").click();
+ await contains(".o-dropdown--menu [data-action-value='o_anim_bounce_in']").click();
+ expect(".options-container [data-label='Effect'] .o-dropdown").toHaveText("Bounce");
+
+ expect(".options-container [data-label='Direction'] .o-dropdown").toHaveText("In place");
+ expect(".options-container [data-label='Trigger'] .o-dropdown").toHaveText(
+ "First Time Only"
+ );
+ expect(".options-container [data-label='Intensity'] input").toHaveValue(50);
+ expect(".options-container [data-label='Start After'] input").toHaveValue("0");
+ expect(".options-container [data-label='Duration'] input").toHaveValue("1");
+ });
+ test("visibility of animation animation=onAppearance effect=flash", async () => {
+ await setupWebsiteBuilder(
+ `
+
+

+
+ `,
+ { styleContent }
+ );
+ await contains(":iframe .test-options-target img").click();
+
+ await contains(".options-container [data-label='Animation'] .dropdown-toggle").click();
+ await contains(".o-dropdown--menu [data-action-value='onAppearance']").click();
+
+ await contains(".options-container [data-label='Effect'] .dropdown-toggle").click();
+ await contains(".o-dropdown--menu [data-action-value='o_anim_flash']").click();
+ expect(".options-container [data-label='Effect'] .o-dropdown").toHaveText("Flash");
+
+ expect(".options-container [data-label='Direction']").not.toBeVisible();
+ expect(".options-container [data-label='Trigger'] .o-dropdown").toHaveText(
+ "First Time Only"
+ );
+ expect(".options-container [data-label='Intensity'] input").toHaveValue(50);
+ expect(".options-container [data-label='Start After'] input").toHaveValue("0");
+ expect(".options-container [data-label='Duration'] input").toHaveValue("1");
+ });
+});
+test("visibility of animation animation=onScroll", async () => {
+ await setupWebsiteBuilder(`
+
+

+
+ `);
+ await contains(":iframe .test-options-target img").click();
+
+ await contains(".options-container [data-label='Animation'] .dropdown-toggle").click();
+ await contains(".o-dropdown--menu [data-action-value='onScroll']").click();
+ expect(".options-container [data-label='Animation'] .o-dropdown").toHaveText("On Scroll");
+
+ expect(".options-container [data-label='Effect'] .o-dropdown").toHaveText("Fade");
+ expect(".options-container [data-label='Direction'] .o-dropdown").toHaveText("In place");
+
+ expect(".options-container [data-label='Trigger']").not.toBeVisible();
+ expect(".options-container [data-label='Intensity']").not.toBeVisible();
+ expect(".options-container [data-label='Start After']").not.toBeVisible();
+ expect(".options-container [data-label='Duration']").not.toBeVisible();
+
+ // todo: check the scrollzone row
+});
+test("visibility of animation animation=onHover", async () => {
+ await setupWebsiteBuilder(`
+
+

+
+ `);
+ await contains(":iframe .test-options-target img").click();
+
+ await contains(".options-container [data-label='Animation'] .dropdown-toggle").click();
+ await contains(".o-dropdown--menu [data-action-value='onHover']").click();
+ expect(".options-container [data-label='Animation'] .o-dropdown").toHaveText("On Hover");
+
+ expect(".options-container [data-label='Effect']").not.toBeVisible();
+ expect(".options-container [data-label='Direction']").not.toBeVisible();
+ expect(".options-container [data-label='Trigger']").not.toBeVisible();
+ expect(".options-container [data-label='Intensity']").not.toBeVisible();
+ expect(".options-container [data-label='Start After']").not.toBeVisible();
+ expect(".options-container [data-label='Duration']").not.toBeVisible();
+
+ // todo: check all the hover options
+});