diff --git a/addons/html_builder/static/src/builder.js b/addons/html_builder/static/src/builder.js
index a522e1c3849fa..e87ce67480be1 100644
--- a/addons/html_builder/static/src/builder.js
+++ b/addons/html_builder/static/src/builder.js
@@ -142,7 +142,6 @@ export class Builder extends Component {
key: this.env.localOverlayContainerKey,
ref: this.props.overlayRef,
},
- replaceSnippet: async (snippet) => await this.snippetModel.replaceSnippet(snippet),
saveSnippet: (snippetEl, cleanForSaveHandlers) =>
this.snippetModel.saveSnippet(snippetEl, cleanForSaveHandlers),
getShared: () => this.editor.shared,
diff --git a/addons/html_builder/static/src/core/clone_plugin.js b/addons/html_builder/static/src/core/clone_plugin.js
index df9e8632eb1af..9329d24b0cf45 100644
--- a/addons/html_builder/static/src/core/clone_plugin.js
+++ b/addons/html_builder/static/src/core/clone_plugin.js
@@ -70,25 +70,43 @@ export class ClonePlugin extends Plugin {
title: _t("Duplicate"),
disabledReason,
handler: () => {
- this.cloneElement(this.overlayTarget, { scrollToClone: true });
+ this.cloneElement(this.overlayTarget, { activateClone: false });
this.dependencies.history.addStep();
},
});
return buttons;
}
- cloneElement(el, { position = "afterend", scrollToClone = false } = {}) {
+ /**
+ * Duplicates the given element and returns the created clone.
+ *
+ * @param {HTMLElement} el the element to clone
+ * @param {Object}
+ * - `position`: specifies where to position the clone (first parameter of
+ * the `insertAdjacentElement` function)
+ * - `scrollToClone`: true if the we should scroll to the clone (if not in
+ * the viewport), false otherwise
+ * - `activateClone`: true if the option containers of the clone should be
+ * the active ones, false otherwise
+ * @returns {HTMLElement}
+ */
+ cloneElement(el, { position = "afterend", scrollToClone = false, activateClone = true } = {}) {
this.dispatchTo("on_will_clone_handlers", { originalEl: el });
- // TODO cleanUI resource for each option
const cloneEl = el.cloneNode(true);
- this.cleanElement(cloneEl);
+ this.cleanElement(cloneEl); // TODO check that
el.insertAdjacentElement(position, cloneEl);
- this.dependencies["builder-options"].updateContainers(cloneEl);
- this.dispatchTo("on_cloned_handlers", { cloneEl: cloneEl, originalEl: el });
+
+ // Update the containers if required.
+ if (activateClone) {
+ this.dependencies["builder-options"].updateContainers(cloneEl);
+ }
+
+ // Scroll to the clone if required and if it is not visible.
if (scrollToClone && !isElementInViewport(cloneEl)) {
cloneEl.scrollIntoView({ behavior: "smooth", block: "center" });
}
- // TODO snippet_cloned ?
+
+ this.dispatchTo("on_cloned_handlers", { cloneEl: cloneEl, originalEl: el });
return cloneEl;
}
diff --git a/addons/html_builder/static/src/core/core_plugins.js b/addons/html_builder/static/src/core/core_plugins.js
index 77f9b03d0e258..949f3c1288b4b 100644
--- a/addons/html_builder/static/src/core/core_plugins.js
+++ b/addons/html_builder/static/src/core/core_plugins.js
@@ -18,7 +18,6 @@ import { MovePlugin } from "./move_plugin";
import { OperationPlugin } from "./operation_plugin";
import { OverlayButtonsPlugin } from "./overlay_buttons/overlay_buttons_plugin";
import { RemovePlugin } from "./remove_plugin";
-import { ReplacePlugin } from "./replace_plugin";
import { SavePlugin } from "./save_plugin";
import { SaveSnippetPlugin } from "./save_snippet_plugin";
import { SetupEditorPlugin } from "./setup_editor_plugin";
@@ -35,7 +34,6 @@ export const CORE_PLUGINS = [
MovePlugin,
GridLayoutPlugin,
DragAndDropPlugin,
- ReplacePlugin,
RemovePlugin,
ClonePlugin,
SaveSnippetPlugin,
diff --git a/addons/html_builder/static/src/core/drag_and_drop_plugin.js b/addons/html_builder/static/src/core/drag_and_drop_plugin.js
index f11a029e60bbe..726925bc97ac6 100644
--- a/addons/html_builder/static/src/core/drag_and_drop_plugin.js
+++ b/addons/html_builder/static/src/core/drag_and_drop_plugin.js
@@ -137,9 +137,11 @@ export class DragAndDropPlugin extends Plugin {
},
{ withLoadingEffect: false }
);
- this.restoreDragSavePoint = this.dependencies.history.makeSavePoint();
+ const restoreDragSavePoint = this.dependencies.history.makeSavePoint();
this.cancelDragAndDrop = () => {
- this.restoreDragSavePoint();
+ // Undo the changes needed to ease the drag and drop.
+ this.dragState.restoreCallbacks?.forEach((restore) => restore());
+ restoreDragSavePoint();
dragAndDropResolve();
this.dependencies["builder-options"].updateContainers(this.overlayTarget);
};
@@ -326,6 +328,7 @@ export class DragAndDropPlugin extends Plugin {
// Undo the changes needed to ease the drag and drop.
this.dragState.restoreCallbacks.forEach((restore) => restore());
+ this.dragState.restoreCallbacks = null;
// Add a history step only if the element was not dropped where
// it was before, otherwise cancel everything.
diff --git a/addons/html_builder/static/src/core/remove_plugin.js b/addons/html_builder/static/src/core/remove_plugin.js
index ab76cbe734453..4cf20c63665d3 100644
--- a/addons/html_builder/static/src/core/remove_plugin.js
+++ b/addons/html_builder/static/src/core/remove_plugin.js
@@ -44,7 +44,7 @@ export class RemovePlugin extends Plugin {
static id = "remove";
static dependencies = ["history", "builder-options"];
resources = {
- get_overlay_buttons: withSequence(4, {
+ get_overlay_buttons: withSequence(3, {
getButtons: this.getActiveOverlayButtons.bind(this),
}),
};
diff --git a/addons/html_builder/static/src/core/replace_plugin.js b/addons/html_builder/static/src/core/replace_plugin.js
deleted file mode 100644
index 0dfac91a682f9..0000000000000
--- a/addons/html_builder/static/src/core/replace_plugin.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import { Plugin } from "@html_editor/plugin";
-import { withSequence } from "@html_editor/utils/resource";
-import { _t } from "@web/core/l10n/translation";
-
-// Snippets are replaceable only if they are not within another snippet (e.g. a
-// "s_countdown" is not replaceable when it is dropped as inner content).
-function isReplaceable(el) {
- // TODO has snippet group ?
- return (
- el.matches("[data-snippet]:not([data-snippet] *), .oe_structure > *") &&
- !el.matches(".oe_structure_solo *")
- );
-}
-
-export class ReplacePlugin extends Plugin {
- static id = "replace";
- static dependencies = ["history", "builder-options"];
- resources = {
- get_overlay_buttons: withSequence(3, {
- getButtons: this.getActiveOverlayButtons.bind(this),
- }),
- };
-
- setup() {
- this.overlayTarget = null;
- }
-
- getActiveOverlayButtons(target) {
- if (!isReplaceable(target)) {
- this.overlayTarget = null;
- return [];
- }
-
- const buttons = [];
- this.overlayTarget = target;
- buttons.push({
- class: "o_snippet_replace bg-warning fa fa-exchange",
- title: _t("Replace"),
- handler: this.replaceSnippet.bind(this),
- });
- return buttons;
- }
-
- async replaceSnippet() {
- const newSnippet = await this.config.replaceSnippet(this.overlayTarget);
- if (newSnippet) {
- this.overlayTarget = null;
- newSnippet.querySelectorAll(".s_dialog_preview").forEach((el) => el.remove());
- // TODO find a way to wait for the images to load before updating or
- // to trigger a refresh once the images are loaded afterwards.
- // If not possible, call updateContainers with nothing.
- this.dependencies.history.addStep();
- this.dependencies["builder-options"].updateContainers(newSnippet);
- // TODO post snippet drop (onBuild,...)
- }
- }
-}
diff --git a/addons/html_builder/static/src/sidebar/block_tab.js b/addons/html_builder/static/src/sidebar/block_tab.js
index c0f1fd7cbee33..bea43b6d0df90 100644
--- a/addons/html_builder/static/src/sidebar/block_tab.js
+++ b/addons/html_builder/static/src/sidebar/block_tab.js
@@ -54,6 +54,7 @@ export class BlockTab extends Component {
onSnippetGroupClick(snippet) {
this.shared.operation.next(
async () => {
+ this.cancelDragAndDrop = this.shared.history.makeSavePoint();
let snippetEl;
const baseSectionEl = snippet.content.cloneNode(true);
this.state.ongoingInsertion = true;
@@ -97,9 +98,11 @@ export class BlockTab extends Component {
});
if (snippetEl) {
+ await scrollTo(snippetEl, { extraOffset: 50 });
await this.processDroppedSnippet(snippetEl);
}
this.state.ongoingInsertion = false;
+ delete this.cancelDragAndDrop;
},
{ withLoadingEffect: false }
);
@@ -145,6 +148,7 @@ export class BlockTab extends Component {
});
if (selectedSnippetEl) {
+ await scrollTo(selectedSnippetEl, { extraOffset: 50 });
await this.processDroppedSnippet(selectedSnippetEl);
} else {
this.cancelDragAndDrop();
@@ -221,7 +225,12 @@ export class BlockTab extends Component {
},
{ withLoadingEffect: false }
);
- this.cancelDragAndDrop = this.shared.history.makeSavePoint();
+ const restoreDragSavePoint = this.shared.history.makeSavePoint();
+ this.cancelDragAndDrop = () => {
+ // Undo the changes needed to ease the drag and drop.
+ this.dragState.restoreCallbacks?.forEach((restore) => restore());
+ restoreDragSavePoint();
+ };
this.hideSnippetToolTip?.();
this.document.body.classList.add("oe_dropzone_active");
@@ -230,6 +239,14 @@ export class BlockTab extends Component {
this.dragState = {};
dropzoneEls = [];
+ // Make some changes on the page to ease the drag and drop.
+ const restoreCallbacks = [];
+ for (const prepareDrag of this.env.editor.getResource("on_prepare_drag_handlers")) {
+ const restore = prepareDrag();
+ restoreCallbacks.push(restore);
+ }
+ this.dragState.restoreCallbacks = restoreCallbacks;
+
const category = element.closest(".o_snippets_container").id;
const id = element.dataset.id;
snippet = this.snippetModel.getSnippet(category, id);
@@ -336,8 +353,6 @@ export class BlockTab extends Component {
if (closestDropzoneEl) {
currentDropzoneEl = closestDropzoneEl;
}
- } else {
- this.cancelDragAndDrop();
}
}
@@ -345,8 +360,13 @@ export class BlockTab extends Component {
currentDropzoneEl.after(snippetEl);
this.shared.dropzone.removeDropzones();
+ // Undo the changes needed to ease the drag and drop.
+ this.dragState.restoreCallbacks.forEach((restore) => restore());
+ this.dragState.restoreCallbacks = null;
+
if (!isSnippetGroup) {
await this.processDroppedSnippet(snippetEl);
+ delete this.cancelDragAndDrop;
} else {
this.shared.operation.next(
async () => {
@@ -355,13 +375,13 @@ export class BlockTab extends Component {
{ withLoadingEffect: false }
);
}
+ } else {
+ this.cancelDragAndDrop();
+ delete this.cancelDragAndDrop;
}
this.state.ongoingInsertion = false;
delete this.cancelSnippetPreview;
- if (!isSnippetGroup) {
- delete this.cancelDragAndDrop;
- }
dragAndDropResolve();
},
};
@@ -375,13 +395,12 @@ export class BlockTab extends Component {
*/
async processDroppedSnippet(snippetEl) {
this.updateDroppedSnippet(snippetEl);
- await scrollTo(snippetEl, { extraOffset: 50 });
// Build the snippet.
for (const onSnippetDropped of this.env.editor.getResource("on_snippet_dropped_handlers")) {
const cancel = await onSnippetDropped({ snippetEl, dragState: this.dragState });
// Cancel everything if the resource asked to.
if (cancel) {
- this.cancelDragAndDrop?.();
+ this.cancelDragAndDrop();
return;
}
}
diff --git a/addons/html_builder/static/src/sidebar/option_container.js b/addons/html_builder/static/src/sidebar/option_container.js
index 7e8fa1aa9dd9d..0ca4c6d0a7040 100644
--- a/addons/html_builder/static/src/sidebar/option_container.js
+++ b/addons/html_builder/static/src/sidebar/option_container.js
@@ -124,7 +124,7 @@ export class OptionsContainer extends BaseOptionComponent {
cloneElement() {
this.callOperation(() => {
this.env.editor.shared.clone.cloneElement(this.props.editingElement, {
- scrollToClone: true,
+ activateClone: false,
});
});
}
diff --git a/addons/html_builder/static/src/snippets/snippet_service.js b/addons/html_builder/static/src/snippets/snippet_service.js
index 3471c55ff2fae..df2ca568fa254 100644
--- a/addons/html_builder/static/src/snippets/snippet_service.js
+++ b/addons/html_builder/static/src/snippets/snippet_service.js
@@ -314,30 +314,6 @@ export class SnippetModel extends Reactive {
return originalSnippet.thumbnailSrc;
}
- /**
- * Opens the snippet dialog in order to replace the given snippet by the
- * selected one.
- *
- * @param {HTMLElement} snippetEl the snippet to replace
- * @returns {HTMLElement}
- */
- async replaceSnippet(snippetEl) {
- // Find the original snippet to open the dialog on the same group.
- const originalSnippet = this.getOriginalSnippet(snippetEl.dataset.snippet);
- let newSnippetEl;
- await new Promise((resolve) => {
- this.openSnippetDialog(originalSnippet, {
- onSelect: (selectedSnippet) => {
- newSnippetEl = selectedSnippet.content.cloneNode(true);
- snippetEl.replaceWith(newSnippetEl);
- return newSnippetEl;
- },
- onClose: () => resolve(),
- });
- });
- return newSnippetEl;
- }
-
/**
* Removes the previews from the given snippet.
*
diff --git a/addons/website/static/tests/builder/edit_interaction.test.js b/addons/website/static/tests/builder/edit_interaction.test.js
index c60c8b9fc6ac2..95bc9496846f2 100644
--- a/addons/website/static/tests/builder/edit_interaction.test.js
+++ b/addons/website/static/tests/builder/edit_interaction.test.js
@@ -10,7 +10,7 @@ import {
setupWebsiteBuilderWithSnippet,
waitForEndOfOperation,
} from "./website_helpers";
-import { click, waitFor } from "@odoo/hoot-dom";
+import { waitFor } from "@odoo/hoot-dom";
import { xml } from "@odoo/owl";
defineWebsiteModels();
@@ -35,26 +35,6 @@ test("dropping a new snippet starts its interaction", async () => {
expect.verifySteps(["refresh"]);
});
-test("replacing a snippet starts the interaction of the new snippet", async () => {
- const { openBuilderSidebar } = await setupWebsiteBuilderWithSnippet("s_text_block", {
- openEditor: false,
- });
- patchWithCleanup(EditInteractionPlugin.prototype, {
- setup() {
- super.setup();
- this.websiteEditService.update = () => expect.step("update");
- this.websiteEditService.refresh = () => expect.step("refresh");
- },
- });
- await openBuilderSidebar();
- await waitFor(":iframe [data-snippet='s_text_block']");
- expect.verifySteps(["update"]);
- await click(`:iframe [data-snippet="s_text_block"]`);
- await contains(".btn.o_snippet_replace").click();
- await confirmAddSnippet("s_title");
- expect.verifySteps(["refresh"]);
-});
-
test("ensure order of operations when hovering an option", async () => {
addActionOption({
customAction: {
diff --git a/addons/website/static/tests/builder/overlay_buttons.test.js b/addons/website/static/tests/builder/overlay_buttons.test.js
index 37f711762266d..bbfb9a18e0472 100644
--- a/addons/website/static/tests/builder/overlay_buttons.test.js
+++ b/addons/website/static/tests/builder/overlay_buttons.test.js
@@ -177,39 +177,6 @@ test("Use the 'remove' overlay buttons: removing the last element will remove th
expect(".oe_overlay.oe_active").toHaveRect(":iframe .second-section");
});
-test("Use the 'replace' overlay buttons", async () => {
- await setupWebsiteBuilder(`
-
- `);
-
- await contains(":iframe .col-lg-5").click();
- expect(".overlay .o_snippet_replace").toHaveCount(0);
-
- await contains(":iframe section").click();
- expect(".overlay .o_snippet_replace").toHaveCount(1);
-
- await contains(".overlay .o_snippet_replace").click();
- // Check that the snippet dialog is open on the right category.
- expect(".o_add_snippet_dialog").toHaveCount(1);
- expect(".o_add_snippet_dialog button:contains('Content')").toHaveClass("active");
-
- await contains(
- ".o_add_snippet_dialog .o_add_snippet_iframe:iframe section.s_shape_image"
- ).click();
- // Check that the snippet was replaced by the chosen one.
- expect(":iframe section.s_text_image").toHaveCount(0);
- expect(":iframe section.s_shape_image").toHaveCount(1);
- // TODO add checks of the overlay + options if the behavior is kept.
-});
-
test("Use the 'clone' overlay buttons", async () => {
await setupWebsiteBuilder(`