From 2cede25222514dbcc2fd4fb1360b1a9cd34d518d Mon Sep 17 00:00:00 2001 From: "Guillaume (gdi)" Date: Fri, 1 Sep 2023 09:38:18 +0200 Subject: [PATCH] [IMP] website: add Instagram page snippet This commit adds a new snippet to display an Instagram page on the website using an iframe that embeds the Instagram page. task-2603045 closes odoo/odoo#133905 Signed-off-by: Benjamin Vray (bvr) --- addons/website/__manifest__.py | 2 + .../static/src/js/editor/snippets.options.js | 8 +- .../static/src/scss/website.edit_mode.scss | 14 ++- .../src/snippets/s_instagram_page/000.js | 110 ++++++++++++++++++ .../src/snippets/s_instagram_page/000.scss | 5 + .../src/snippets/s_instagram_page/options.js | 103 ++++++++++++++++ .../src/snippets/s_social_media/options.js | 4 + .../views/snippets/s_instagram_page.xml | 40 +++++++ addons/website/views/snippets/snippets.xml | 5 +- 9 files changed, 286 insertions(+), 5 deletions(-) create mode 100644 addons/website/static/src/snippets/s_instagram_page/000.js create mode 100644 addons/website/static/src/snippets/s_instagram_page/000.scss create mode 100644 addons/website/static/src/snippets/s_instagram_page/options.js create mode 100644 addons/website/views/snippets/s_instagram_page.xml diff --git a/addons/website/__manifest__.py b/addons/website/__manifest__.py index c5cf214e4e2..49fe6145843 100644 --- a/addons/website/__manifest__.py +++ b/addons/website/__manifest__.py @@ -38,6 +38,7 @@ 'views/snippets/s_text_cover.xml', 'views/snippets/s_text_image.xml', 'views/snippets/s_image_text.xml', + 'views/snippets/s_instagram_page.xml', 'views/snippets/s_banner.xml', 'views/snippets/s_text_block.xml', 'views/snippets/s_features.xml', @@ -219,6 +220,7 @@ 'website/static/src/snippets/s_facebook_page/options.js', 'website/static/src/snippets/s_image_gallery/options.js', 'website/static/src/snippets/s_image_gallery/000.xml', + 'website/static/src/snippets/s_instagram_page/options.js', 'website/static/src/snippets/s_countdown/options.js', 'website/static/src/snippets/s_countdown/options.xml', 'website/static/src/snippets/s_masonry_block/options.js', diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js index 3a398417b45..ee66601e1fc 100644 --- a/addons/website/static/src/js/editor/snippets.options.js +++ b/addons/website/static/src/js/editor/snippets.options.js @@ -924,7 +924,13 @@ options.Class.include({ _select: async function (previewMode, widget) { await this._super(...arguments); - if (this.options.isWebsite && !widget.$el.closest('[data-no-widget-refresh="true"]').length) { + // Some blocks flicker when we start their public widgets, so we skip + // the refresh for them to avoid the flickering. + const targetNoRefreshSelector = ".s_instagram_page"; + // TODO: we should review the way public widgets are restarted when + // converting to OWL and a new API. + if (this.options.isWebsite && !widget.$el.closest('[data-no-widget-refresh="true"]').length + && !this.$target[0].matches(targetNoRefreshSelector)) { // TODO the flag should be retrieved through widget params somehow await this._refreshPublicWidgets(); } diff --git a/addons/website/static/src/scss/website.edit_mode.scss b/addons/website/static/src/scss/website.edit_mode.scss index ae614e90a5f..fe72f699d41 100644 --- a/addons/website/static/src/scss/website.edit_mode.scss +++ b/addons/website/static/src/scss/website.edit_mode.scss @@ -20,6 +20,11 @@ $-editor-messages-margin-x: 2%; display: block; } } +%o-disable-iframes { + iframe { + pointer-events: none; + } +} .o_we_snippet_area_animation { animation-delay: 999ms; // Disable it but allow to inherit the animation @@ -203,14 +208,17 @@ table.editorbar-panel { // Facebook Page .editor_enable .o_facebook_page:not(.o_facebook_preview) { - iframe { - pointer-events: none; - } + @extend %o-disable-iframes; .o_facebook_alert .o_add_facebook_page { cursor: pointer; } } +// Prevent Instagram block to open Instagram in edit mode. +.editor_enable .s_instagram_page { + @extend %o-disable-iframes; +} + // s_countdown preview classes body.editor_enable { .s_countdown { diff --git a/addons/website/static/src/snippets/s_instagram_page/000.js b/addons/website/static/src/snippets/s_instagram_page/000.js new file mode 100644 index 00000000000..d3355b03190 --- /dev/null +++ b/addons/website/static/src/snippets/s_instagram_page/000.js @@ -0,0 +1,110 @@ +/** @odoo-module **/ + +import publicWidget from "@web/legacy/js/public/public_widget"; + +// Note that Instagram can automatically detect the language of the user and +// translate the embed. + +const InstagramPage = publicWidget.Widget.extend({ + selector: ".s_instagram_page", + disabledInEditableMode: false, + + /** + * @override + */ + start() { + const iframeEl = document.createElement("iframe"); + this.el.querySelector(".o_instagram_container").appendChild(iframeEl); + iframeEl.setAttribute("scrolling", "no"); + iframeEl.classList.add("w-100"); + // We can already estimate the height of the iframe. + iframeEl.height = this._estimateIframeHeight(); + // We have to setup the message listener before setting the src, because + // the iframe can send a message before this JS is fully loaded. + this.__onMessage = this._onMessage.bind(this); + window.addEventListener("message", this.__onMessage); + // We set the src now, we are ready to receive the message. + iframeEl.src = `https://www.instagram.com/${this.el.dataset.instagramPage}/embed`; + + return this._super(...arguments); + }, + /** + * @override + */ + destroy() { + const iframeEl = this.el.querySelector(".o_instagram_container iframe"); + if (iframeEl) { + iframeEl.remove(); + window.removeEventListener("message", this.__onMessage); + } + this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Gives an estimation of the height of the Instagram iframe. + * + * @private + * @returns {number} + */ + _estimateIframeHeight() { + // In the meantime Instagram doesn't send us a message with the height, + // we use a formula to estimate the height of the iframe (the formula + // has been found with a linear regression). + const iframeEl = this.el.querySelector(".o_instagram_container iframe"); + const iframeWidth = parseInt(getComputedStyle(iframeEl).width); + // The profile picture is smaller when width < 432px. + return 0.659 * iframeWidth + (iframeWidth < 432 ? 156 : 203); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when a message is sent. Instagram sends us a message with the + * height of the iframe. + * + * @private + * @param {Event} ev + */ + _onMessage(ev) { + const iframeEl = this.el.querySelector(".o_instagram_container iframe"); + if (!iframeEl) { + // TODO: fix this case. We should never end up here. It happens when + // - Drop Instagram snippet + // - Undo + // - Drop a new Instagram snippet + // => The listener of the first one is still active because the + // public widget has not been destroyed. + window.removeEventListener("message", this.__onMessage); + return; + } + if (ev.origin !== "https://www.instagram.com" || iframeEl.contentWindow !== ev.source) { + // It's not a message from Instagram or it's a message from another + // Instagram iframe. + return; + } + const evDataJSON = JSON.parse(ev.data); + if (evDataJSON.type !== "MEASURE") { + // It's not a measure message. + return; + } + const height = parseInt(evDataJSON.details.height); + // Here we get the exact height of the iframe. + // Instagram can return a height of 0 before the real height. + if (height) { + // Prevent history step in edit mode. + this.options.wysiwyg?.odooEditor.observerUnactive(); + iframeEl.height = height; + this.options.wysiwyg?.odooEditor.observerActive(); + } + }, +}); + +publicWidget.registry.InstagramPage = InstagramPage; + +export default InstagramPage; diff --git a/addons/website/static/src/snippets/s_instagram_page/000.scss b/addons/website/static/src/snippets/s_instagram_page/000.scss new file mode 100644 index 00000000000..f0e9c2f1161 --- /dev/null +++ b/addons/website/static/src/snippets/s_instagram_page/000.scss @@ -0,0 +1,5 @@ +.s_instagram_page { + div.container-fluid { + padding: 0; + } +} diff --git a/addons/website/static/src/snippets/s_instagram_page/options.js b/addons/website/static/src/snippets/s_instagram_page/options.js new file mode 100644 index 00000000000..63c852c0159 --- /dev/null +++ b/addons/website/static/src/snippets/s_instagram_page/options.js @@ -0,0 +1,103 @@ +/** @odoo-module **/ + +import {_t} from "@web/core/l10n/translation"; +import options from "@web_editor/js/editor/snippets.options"; +import SocialMediaOption from "@website/snippets/s_social_media/options"; + +options.registry.InstagramPage = options.Class.extend({ + /** + * @override + */ + init() { + this._super(...arguments); + this.orm = this.bindService("orm"); + this.instagramUrlStr = "instagram.com/"; + }, + /** + * @override + */ + async onBuilt() { + // First we check if the user has changed his instagram during the + // current edition (via the social media options). + const dbSocialValuesCache = SocialMediaOption.getDbSocialValuesCache(); + let socialInstagram = dbSocialValuesCache && dbSocialValuesCache["social_instagram"]; + // If not, we check the value in the DB. + if (!socialInstagram) { + let websiteId; + this.trigger_up("context_get", { + callback: function (ctx) { + websiteId = ctx["website_id"]; + }, + }); + const values = await this.orm.read("website", [websiteId], ["social_instagram"]); + socialInstagram = values[0]["social_instagram"]; + } + if (socialInstagram) { + const pageName = this._getInstagramPageNameFromUrl(socialInstagram); + if (pageName) { + this.$target[0].dataset.instagramPage = pageName; + } + } + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Registers the instagram page name. + * + * @see this.selectClass for parameters + */ + async setInstagramPage(previewMode, widgetValue, params) { + if (widgetValue.includes(this.instagramUrlStr)) { + widgetValue = this._getInstagramPageNameFromUrl(widgetValue); + } + if (!widgetValue) { + this.displayNotification({ + message: _t("The Instagram page name is not valid"), + type: "warning", + }); + } + this.$target[0].dataset.instagramPage = widgetValue || ""; + // As the public widget restart is disabled for instagram, we have to + // manually restart the widget. + await this.trigger_up("widgets_start_request", { + $target: this.$target, + editableMode: true, + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeWidgetState(widgetName, params) { + if (widgetName === "setInstagramPage") { + return this.$target[0].dataset.instagramPage; + } + return this._super(...arguments); + }, + /** + * Returns the instagram page name from the given url. + * + * @private + * @param {string} url + * @returns {string|undefined} + */ + _getInstagramPageNameFromUrl(url) { + const pageName = url.split(this.instagramUrlStr)[1]; + if (!pageName || pageName.includes("?") || pageName.includes("#") || + (pageName.includes("/") && pageName.split("/")[1].length > 0)) { + return; + } + return pageName.split("/")[0]; + }, +}); + +export default { + InstagramPage: options.registry.InstagramPage, +}; diff --git a/addons/website/static/src/snippets/s_social_media/options.js b/addons/website/static/src/snippets/s_social_media/options.js index 63e4b07f6b7..d0602ec69cf 100644 --- a/addons/website/static/src/snippets/s_social_media/options.js +++ b/addons/website/static/src/snippets/s_social_media/options.js @@ -11,6 +11,9 @@ const clearDbSocialValuesCache = () => { dbSocialValuesProm = undefined; dbSocialValues = undefined; }; +const getDbSocialValuesCache = () => { + return dbSocialValues; +}; options.registry.SocialMedia = options.Class.extend({ init() { @@ -365,4 +368,5 @@ options.registry.SocialMedia = options.Class.extend({ export default { SocialMedia: options.registry.SocialMedia, clearDbSocialValuesCache, + getDbSocialValuesCache, }; diff --git a/addons/website/views/snippets/s_instagram_page.xml b/addons/website/views/snippets/s_instagram_page.xml new file mode 100644 index 00000000000..03246bb3801 --- /dev/null +++ b/addons/website/views/snippets/s_instagram_page.xml @@ -0,0 +1,40 @@ + + + + + + + + + + Instagram Page 000 JS + web.assets_frontend + website/static/src/snippets/s_instagram_page/000.js + + + + Instagram Page 000 SCSS + web.assets_frontend + website/static/src/snippets/s_instagram_page/000.scss + + + diff --git a/addons/website/views/snippets/snippets.xml b/addons/website/views/snippets/snippets.xml index bbd521ded25..3400e369a1f 100644 --- a/addons/website/views/snippets/snippets.xml +++ b/addons/website/views/snippets/snippets.xml @@ -124,6 +124,9 @@ + + social media, ig, feed + celebration, launch @@ -1230,7 +1233,7 @@ -
+
Auto