[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) <bvr@odoo.com>
This commit is contained in:
Guillaume (gdi)
2023-10-02 14:12:46 +00:00
parent b5b94857e7
commit 2cede25222
9 changed files with 286 additions and 5 deletions
+2
View File
@@ -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',
@@ -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();
}
@@ -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 {
@@ -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;
@@ -0,0 +1,5 @@
.s_instagram_page {
div.container-fluid {
padding: 0;
}
}
@@ -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,
};
@@ -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,
};
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="s_instagram_page" name="Instagram Page">
<section class="s_instagram_page" data-instagram-page="odoo.official">
<div class="o_container_small o_instagram_container o_not_editable">
<!-- The iframe will be added here by the public widget. -->
</div>
</section>
</template>
<template id="s_instagram_page_options" inherit_id="website.snippet_options">
<xpath expr="." position="inside">
<div data-js="InstagramPage" data-selector=".s_instagram_page">
<we-alert class="mt-2">
Your instagram page must be public to be integrated into an Odoo website.
</we-alert>
<we-input string="Instagram Page"
class="o_we_large"
placeholder="odoo.official"
data-set-instagram-page=""
data-no-preview="true"/>
</div>
</xpath>
</template>
<record id="website.s_instagram_page_000_js" model="ir.asset">
<field name="name">Instagram Page 000 JS</field>
<field name="bundle">web.assets_frontend</field>
<field name="path">website/static/src/snippets/s_instagram_page/000.js</field>
</record>
<record id="website.s_instagram_page_000_scss" model="ir.asset">
<field name="name">Instagram Page 000 SCSS</field>
<field name="bundle">web.assets_frontend</field>
<field name="path">website/static/src/snippets/s_instagram_page/000.scss</field>
</record>
</odoo>
+4 -1
View File
@@ -124,6 +124,9 @@
<t id="mass_mailing_newsletter_popup_hook"/>
<t t-snippet="website.s_popup" string="Popup" t-thumbnail="/website/static/src/img/snippets_thumbs/s_popup.svg"/>
<t t-snippet="website.s_facebook_page" string="Facebook" t-thumbnail="/website/static/src/img/snippets_thumbs/s_facebook_page.svg"/>
<t t-snippet="website.s_instagram_page" string="Instagram Page" t-thumbnail="/website/static/src/img/snippets_thumbs/s_instagram_page.svg">
<keywords>social media, ig, feed</keywords>
</t>
<t t-snippet="website.s_countdown" string="Countdown" t-thumbnail="/website/static/src/img/snippets_thumbs/s_countdown.svg">
<keywords>celebration, launch</keywords>
</t>
@@ -1230,7 +1233,7 @@
</div>
<!-- Scroll to next section button (only for full height) -->
<div data-js="ScrollButton" data-selector="section" data-exclude="[data-snippet] :not(.oe_structure) > [data-snippet]">
<div data-js="ScrollButton" data-selector="section" data-exclude="[data-snippet] :not(.oe_structure) > [data-snippet], .s_instagram_page">
<!-- Min height of section -->
<we-button-group string="Height" data-show-scroll-button="">
<we-button data-name="minheight_auto_opt" data-select-class="" title="Fit content">Auto</we-button>