[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:
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user