diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js index 6ae66ec6690..98d00337e0a 100644 --- a/addons/website/static/src/js/editor/snippets.options.js +++ b/addons/website/static/src/js/editor/snippets.options.js @@ -2671,27 +2671,18 @@ options.registry.HideFooter = VisibilityPageOptionUpdate.extend({ options.registry.anchor = options.Class.extend({ isTopOption: true, - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - /** * @override */ - start: function () { + start() { // Generate anchor and copy it to clipboard on click, show the tooltip on success - this.$button = this.$el.find('we-button'); - const clipboard = new ClipboardJS(this.$button[0], {text: () => this._getAnchorLink()}); - clipboard.on('success', () => { - const message = sprintf(Markup(_t("Anchor copied to clipboard
Link: %s")), this._getAnchorLink()); - this.displayNotification({ - type: 'success', - message: message, - buttons: [{text: _t("Edit"), click: () => this.openAnchorDialog(), primary: true}], - }); - }); + const buttonEl = this.el.querySelector("we-button"); + this.isModal = this.$target[0].classList.contains("modal"); + if (buttonEl && !this.isModal) { + this._buildClipboard(buttonEl); + } - return this._super.apply(this, arguments); + return this._super(...arguments); }, /** * @override @@ -2701,13 +2692,45 @@ options.registry.anchor = options.Class.extend({ this.$target.filter(':not(.carousel)').removeAttr('id'); }, + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + notify(name, data) { + this._super(...arguments); + if (name === "modalAnchor") { + this._buildClipboard(data.buttonEl); + } + }, + //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- + /** - * @see this.selectClass for parameters + * @private + * @param {Element} buttonEl */ - openAnchorDialog: function (previewMode, widgetValue, params) { + _buildClipboard(buttonEl) { + const clipboard = new ClipboardJS(buttonEl, {text: () => this._getAnchorLink()}); + clipboard.on("success", () => { + const message = sprintf(Markup(_t("Anchor copied to clipboard
Link: %s")), this._getAnchorLink()); + this.displayNotification({ + type: "success", + message: message, + buttons: [{text: _t("Edit"), click: () => this._openAnchorDialog(buttonEl), primary: true}], + }); + }); + }, + + /** + * @private + * @param {Element} buttonEl + */ + _openAnchorDialog(buttonEl) { var self = this; var buttons = [{ text: _t("Save & copy"), @@ -2728,7 +2751,7 @@ options.registry.anchor = options.Class.extend({ if (!alreadyExists) { self._setAnchorName(anchorName); this.close(); - self.$button[0].click(); + buttonEl.click(); } }, }, { @@ -2760,10 +2783,10 @@ options.registry.anchor = options.Class.extend({ */ _setAnchorName: function (value) { if (value) { - this.$target.attr({ - 'id': value, - 'data-anchor': true, - }); + this.$target[0].id = value; + if (!this.isModal) { + this.$target[0].dataset.anchor = true; + } } else { this.$target.removeAttr('id data-anchor'); } @@ -2786,7 +2809,8 @@ options.registry.anchor = options.Class.extend({ } this._setAnchorName(anchorName + n); } - return `${this.ownerDocument.location.pathname}#${this.$target[0].id}`; + const pathName = this.isModal ? "" : this.ownerDocument.location.pathname; + return `${pathName}#${this.$target[0].id}`; }, /** * Creates a safe id/anchor from text. diff --git a/addons/website/static/src/js/tours/tour_utils.js b/addons/website/static/src/js/tours/tour_utils.js index 2db178ce20b..f7b4b195bdb 100644 --- a/addons/website/static/src/js/tours/tour_utils.js +++ b/addons/website/static/src/js/tours/tour_utils.js @@ -108,6 +108,7 @@ function changeOption(optionName, weName = '', optionTooltipLabel = '', position trigger: `${option_block} ${weName}, ${option_block} [title='${weName}']`, content: Markup(sprintf(_t("Click on this option to change the %s of the block."), optionTooltipLabel)), position: position, + in_modal: false, run: "click", }; } diff --git a/addons/website/static/src/js/utils.js b/addons/website/static/src/js/utils.js index 6b4dfd84f62..3712656df4c 100644 --- a/addons/website/static/src/js/utils.js +++ b/addons/website/static/src/js/utils.js @@ -22,7 +22,7 @@ function loadAnchors(url, body) { resolve(); } }).then(function (response) { - const anchors = $(response).find('[id][data-anchor=true]').toArray().map((el) => { + const anchors = $(response).find('[id][data-anchor=true], .modal[id][data-display="onClick"]').toArray().map((el) => { return '#' + el.id; }); // Always suggest the top and the bottom of the page as internal link diff --git a/addons/website/static/src/snippets/s_popup/000.js b/addons/website/static/src/snippets/s_popup/000.js index e1f12192315..63b902ebadc 100644 --- a/addons/website/static/src/snippets/s_popup/000.js +++ b/addons/website/static/src/snippets/s_popup/000.js @@ -81,9 +81,21 @@ const PopupWidget = publicWidget.Widget.extend({ * @override */ start: function () { - this._popupAlreadyShown = !!getCookie(this.$el.attr('id')); - if (!this._popupAlreadyShown) { - this._bindPopup(); + this.modalShownOnClickEl = this.el.querySelector(".modal[data-display='onClick']"); + if (this.modalShownOnClickEl) { + // We add a "hashchange" listener in case a button to open a popup + // is clicked. + this.__onHashChange = this._onHashChange.bind(this); + window.addEventListener('hashchange', this.__onHashChange); + // Check if a hash exists and if the modal needs to be opened when + // the page loads (e.g. The user has clicked a button on the + // "Contact us" page to open a popup on the homepage). + this._showPopupOnClick(); + } else { + this._popupAlreadyShown = !!getCookie(this.$el.attr('id')); + if (!this._popupAlreadyShown) { + this._bindPopup(); + } } return this._super(...arguments); }, @@ -95,6 +107,9 @@ const PopupWidget = publicWidget.Widget.extend({ $(document).off('mouseleave.open_popup'); this.$el.find('.modal').modal('hide'); clearTimeout(this.timeout); + if (this.modalShownOnClickEl) { + window.removeEventListener('hashchange', this.__onHashChange); + } }, //-------------------------------------------------------------------------- @@ -119,7 +134,7 @@ const PopupWidget = publicWidget.Widget.extend({ if (display === 'afterDelay') { this.timeout = setTimeout(() => this._showPopup(), delay); - } else { + } else if (display === "mouseExit") { $(document).on('mouseleave.open_popup', () => this._showPopup()); } }, @@ -144,6 +159,21 @@ const PopupWidget = publicWidget.Widget.extend({ } this.$el.find('.modal').modal('show'); }, + /** + * @private + */ + _showPopupOnClick() { + const hash = window.location.hash; + // If a hash exists in the URL and it corresponds to the ID of the modal, + // then we open the modal. + if (hash && hash.substring(1) === this.modalShownOnClickEl.id) { + // We remove the hash from the URL because otherwise the popup + // cannot open again after being closed. + const urlWithoutHash = window.location.href.replace(hash, ''); + window.history.replaceState(null, null, urlWithoutHash); + this._showPopup(); + } + }, //-------------------------------------------------------------------------- // Handlers @@ -161,7 +191,7 @@ const PopupWidget = publicWidget.Widget.extend({ _onHideModal: function () { const nbDays = this.$el.find('.modal').data('consentsDuration'); setCookie(this.el.id, this.cookieValue, nbDays * 24 * 60 * 60, 'required'); - this._popupAlreadyShown = true; + this._popupAlreadyShown = true && !this.modalShownOnClickEl; this.$el.find('.media_iframe_video iframe').each((i, iframe) => { iframe.src = ''; @@ -176,6 +206,12 @@ const PopupWidget = publicWidget.Widget.extend({ iframe.src = media.dataset.oeExpression || media.dataset.src; // TODO still oeExpression to remove someday }); }, + /** + * @private + */ + _onHashChange() { + this._showPopupOnClick(); + }, }); publicWidget.registry.popup = PopupWidget; diff --git a/addons/website/static/src/snippets/s_popup/options.js b/addons/website/static/src/snippets/s_popup/options.js index c611f8841cc..a1ab208afa1 100644 --- a/addons/website/static/src/snippets/s_popup/options.js +++ b/addons/website/static/src/snippets/s_popup/options.js @@ -31,6 +31,15 @@ options.registry.SnippetPopup = options.Class.extend({ // sound). Stop the video, as the user can't do it (no button on video // in edit mode). this._removeIframeSrc(); + if (!this.$target[0].parentElement.matches("#website_cookies_bar")) { + this.trigger_up("option_update", { + optionName: "anchor", + name: "modalAnchor", + data: { + buttonEl: this._requestUserValueWidgets("onclick_opt")[0].el, + }, + }); + } return this._super(...arguments); }, /** diff --git a/addons/website/static/src/snippets/s_website_form/000.js b/addons/website/static/src/snippets/s_website_form/000.js index 7703952e016..ed5fa635aad 100644 --- a/addons/website/static/src/snippets/s_website_form/000.js +++ b/addons/website/static/src/snippets/s_website_form/000.js @@ -401,10 +401,18 @@ if (successPage.charAt(0) === "#") { const successAnchorEl = document.getElementById(successPage.substring(1)); if (successAnchorEl) { - await dom.scrollTo(successAnchorEl, { - duration: 500, - extraOffset: 0, - }); + // Check if the target of the link is a modal. + if (successAnchorEl.classList.contains("modal")) { + // Trigger a "hashChange" event to + // notify the popup widget to show the + // popup. + window.location.href = successPage; + } else { + await dom.scrollTo(successAnchorEl, { + duration: 500, + extraOffset: 0, + }); + } } break; } diff --git a/addons/website/static/tests/tours/snippet_popup_display_on_click.js b/addons/website/static/tests/tours/snippet_popup_display_on_click.js new file mode 100644 index 00000000000..81daa6c7a76 --- /dev/null +++ b/addons/website/static/tests/tours/snippet_popup_display_on_click.js @@ -0,0 +1,85 @@ +/** @odoo-module */ + +import wTourUtils from "website.tour_utils"; + +wTourUtils.registerWebsitePreviewTour("snippet_popup_display_on_click", { + test: true, + url: "/", + edition: true, +}, [ + wTourUtils.dragNDrop({id: "s_text_image", name: "Image - Text"}), + wTourUtils.dragNDrop({id: "s_popup", name: "Popup"}), + { + content: "Click inside the popup to access its options menu.", + in_modal: false, + trigger: "iframe .s_popup .s_banner", + }, + wTourUtils.changeOption("SnippetPopup", 'we-select[data-attribute-name="display"] we-toggler'), + { + content: "Click on the display 'On Click' option", + trigger: "#oe_snippets we-button[data-name='onclick_opt']", + in_modal: false, + run() { + // The clipboard cannot be accessed from a script. + // https://w3c.github.io/editing/docs/execCommand/#dfn-the-copy-command + // The execCommand is patched for that step so that ClipboardJS still + // sends the "success" event. + const oldExecCommand = document.execCommand; + document.execCommand = () => true; + this.$anchor[0].click(); + document.execCommand = oldExecCommand; + } + }, + { + content: "Check the copied anchor from the notification toast", + trigger: ".o_notification_manager .o_notification_content", + run() { + const notificationContent = this.$anchor[0].innerText; + const anchor = notificationContent.substring(notificationContent.indexOf("#")); + + if (anchor !== "#Win-%2420") { + console.error("The popup anchor is not '#Win-%2420' as expected."); + } + }, + }, + wTourUtils.clickOnElement("button to close the popup", "iframe .s_popup_close"), + wTourUtils.clickOnElement("text image snippet button", "iframe .s_text_image .btn-primary"), + { + content: "Paste the popup anchor in the URL input", + trigger: "#o_link_dialog_url_input", + run: "text #Win-%2420" + }, + ...wTourUtils.clickOnSave(), + wTourUtils.clickOnElement("text image snippet button", "iframe .s_text_image .btn-primary"), + { + content: "Verify that the popup opens after clicked the button.", + in_modal: false, + trigger: "iframe .s_popup .modal[id='Win-%2420'].show", + }, + wTourUtils.clickOnElement("button to close the popup", "iframe .s_popup_close"), + { + content: "Go to the 'contactus' page.", + trigger: "iframe a[href='/contactus']", + }, + { + content: "wait for the page to be loaded", + trigger: ".o_website_preview[data-view-xmlid='website.contactus']", + run: () => null, // it"s a check + }, + ...wTourUtils.clickOnEditAndWaitEditMode(), + wTourUtils.dragNDrop({id: "s_text_image", name: "Image - Text"}), + wTourUtils.clickOnElement("text image snippet button", "iframe .s_text_image .btn-primary"), + { + content: "Add a link to the homepage popup in the URL input", + trigger: "#o_link_dialog_url_input", + run: "text /#Win-%2420" + }, + ...wTourUtils.clickOnSave(), + wTourUtils.clickOnElement("text image snippet button", "iframe .s_text_image .btn-primary"), + { + content: "Verify that the popup opens when the homepage page loads.", + in_modal: false, + extra_trigger: ".o_website_preview[data-view-xmlid='website.homepage']", + trigger: "iframe .s_popup .modal[id='Win-%2420'].show", + }, +]); diff --git a/addons/website/tests/test_snippets.py b/addons/website/tests/test_snippets.py index 7a1504f95f4..93e3cd47b92 100644 --- a/addons/website/tests/test_snippets.py +++ b/addons/website/tests/test_snippets.py @@ -74,3 +74,6 @@ class TestSnippets(HttpCase): def test_10_parallax(self): self.start_tour(self.env['website'].get_client_action_url('/'), 'test_parallax', login='admin') + + def test_11_snippet_popup_display_on_click(self): + self.start_tour(self.env['website'].get_client_action_url('/'), 'snippet_popup_display_on_click', login='admin') diff --git a/addons/website/views/snippets/s_popup.xml b/addons/website/views/snippets/s_popup.xml index 73215922fdf..f710d37f917 100644 --- a/addons/website/views/snippets/s_popup.xml +++ b/addons/website/views/snippets/s_popup.xml @@ -53,10 +53,11 @@ Delay On Exit + On Click (via link) days - + This page All pages diff --git a/addons/website/views/snippets/snippets.xml b/addons/website/views/snippets/snippets.xml index d660b4cb705..78af05a43af 100644 --- a/addons/website/views/snippets/snippets.xml +++ b/addons/website/views/snippets/snippets.xml @@ -1120,6 +1120,11 @@ data-no-preview="true"/> +
+
+