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(` -
-
-
-
-

TEST

-
-
-
-
- `); - - 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(`