From ca12faa75d7b46bdbcd7134403d80ccd8f5985be Mon Sep 17 00:00:00 2001 From: GnoxNahte Date: Sun, 14 Jul 2024 12:03:21 +0800 Subject: [PATCH] Cache the size for embeds that have dynamic sizes (Imgur, Reddit, TikTok, Twitter/X) Hopefully it should improve [Cumulative Layout Shift](https://web.dev/articles/cls) for embeds that were loaded previously. Also fixes https://github.com/GnoxNahte/obsidian-auto-embed/issues/4#issuecomment-2226989583 which was caused by TikTok not sending the resize message sometimes, especially if the time between each request is very short. --- src/embeds/embedBase.ts | 8 +++++++- src/embeds/imgur.ts | 16 +++++++++++++--- src/embeds/reddit.ts | 36 ++++++++++++++++++------------------ src/embeds/tiktok.ts | 14 ++++++++++++-- src/embeds/twitter.ts | 22 +++++++++++++++++----- src/utility.ts | 9 +++++++++ 6 files changed, 76 insertions(+), 29 deletions(-) diff --git a/src/embeds/embedBase.ts b/src/embeds/embedBase.ts index 3563a08..9771e9b 100644 --- a/src/embeds/embedBase.ts +++ b/src/embeds/embedBase.ts @@ -1,4 +1,5 @@ import AutoEmbedPlugin from "src/main"; +import { Dictionary, Size } from "src/utility"; export class BaseEmbedData { embedSource: EmbedBase; @@ -26,9 +27,14 @@ export abstract class EmbedBase { // If the website doesn't need / doesn't send the message, it'll be undefined. readonly embedOrigin?: string; readonly plugin: AutoEmbedPlugin; + // Cache the height of the embed. + // Correctly sizes the embeds that it has seen before as soon as the embed is loaded. Reducing Cumulative Layout Shift. + // Helps also when some websites don't send the resize message for some reason. + sizeCache: Dictionary; constructor(plugin: AutoEmbedPlugin) { this.plugin = plugin; + this.sizeCache = {}; } abstract createEmbed(link: string, embedData?: BaseEmbedData): HTMLElement; @@ -88,7 +94,7 @@ export abstract class EmbedBase { // To have a embed source respond to the resize event: // - Set EmbedBase.embedOrigin (e.g. embedOrigin = "https://platform.twitter.com") - // - Set body of resize method here (onResizeMessage) + // - Implement body of resize method here (onResizeMessage) onResizeMessage?(e: MessageEvent):void; onErrorCreatingEmbed(msg?: string): HTMLElement { diff --git a/src/embeds/imgur.ts b/src/embeds/imgur.ts index 16c30dc..22ba7f2 100644 --- a/src/embeds/imgur.ts +++ b/src/embeds/imgur.ts @@ -18,7 +18,15 @@ export class ImgurEmbed extends EmbedBase { iframe.src = `https://imgur.com/a/${imgurId}/embed?pub=true`; - iframe.classList.add(this.autoEmbedCssClass, "imgur-embed", "imgur-" + imgurId); + iframe.classList.add(this.autoEmbedCssClass, "imgur-embed"); + iframe.dataset.imgurId = imgurId; + if (this.sizeCache[imgurId] && this.sizeCache[imgurId].height) { + iframe.style.height = this.sizeCache[imgurId].height + "px"; + + // Optional + if (this.sizeCache[imgurId].width) + iframe.style.width = this.sizeCache[imgurId].width + "px"; + } iframe.setAttribute("scrolling", "no"); @@ -34,10 +42,10 @@ export class ImgurEmbed extends EmbedBase { return; const imgurId = regexMatch[1]; - // Why use class instead of id for getting the reference: + // Why use querySelectorAll instead of querySelector for getting the reference: // There might be multiple iframes, some in Reading mode and Live preview. // Some might even duplicates if they user has duplicates - const iframes = document.getElementsByClassName("imgur-" + imgurId); + const iframes = document.querySelectorAll(`.imgur-embed[data-imgur-id="${imgurId}"`); if (iframes.length === 0) return; @@ -48,6 +56,8 @@ export class ImgurEmbed extends EmbedBase { if (data.message === "resize_imgur") { iframe.height = data.height + "px"; iframe.width = data.width + "px"; + + this.sizeCache[imgurId] = { width: data.width, height: data.height }; } // ===== NOTE ===== // Imgur has different embed urls for "albums" and "posts" diff --git a/src/embeds/reddit.ts b/src/embeds/reddit.ts index 23ab71c..2f18055 100644 --- a/src/embeds/reddit.ts +++ b/src/embeds/reddit.ts @@ -11,19 +11,19 @@ export class RedditEmbed extends EmbedBase { if (regexMatch === null) return this.onErrorCreatingEmbed(); - const postId = url.match(/(?:\/comments\/)(\w+)/) as RegExpMatchArray; - if (!postId) + const postIdRegexResult = url.match(/(?:\/comments\/)(\w+)/) as RegExpMatchArray; + if (!postIdRegexResult) { return this.onErrorCreatingEmbed(); } + const postId = postIdRegexResult[1]; // Creating the iframe const iframe = createEl("iframe"); iframe.classList.add(this.autoEmbedCssClass, "reddit-embed", "reddit-" + postId[1]); - url = url.replace("www.reddit.com", "reddit.com"); // Remove "www" - url = url.replace("reddit.com", "embed.reddit.com"); // Add embed subdomain + url = url.replace("www.reddit.com", "embed.reddit.com"); // Remove "www" if (this.plugin.settings.darkMode) { @@ -32,20 +32,14 @@ export class RedditEmbed extends EmbedBase { } iframe.src = url; + iframe.setAttribute("scrolling", "no"); + iframe.setAttribute("allowfullscreen", ""); - // TODO: Dynamically set iframe height: - // Methods: - // - Listen to reddit's postmessage, reddit sends out a postmessage containing: - // { - // type: "resize.embed" - // data: [height value] - // } - // But it doesn't send the post id, so I'm not sure which iframe to set it to. - // - Get height from reddit api? Not sure how to do - // - Get content type from reddit api? Main heights are: - // - short text: 240px - // - long text (has a show more dropdown): 316px - // - picture / video: 739px + // console.log("Cache: " + JSON.stringify(this.sizeCache)) + iframe.dataset.redditPostId = postId; + if (this.sizeCache[postId] && this.sizeCache[postId].height) { + iframe.style.height = this.sizeCache[postId].height + "px"; + } // iframe.style.height="unset"; @@ -72,7 +66,13 @@ export class RedditEmbed extends EmbedBase { // Check where the message came from if (iframe.contentWindow == e.source) { - iframe.style.height = data.data + "px"; + const height = data.data; + iframe.style.height = height + "px"; + + const postId = iframe.dataset.redditPostId; + if (postId) + this.sizeCache[postId] = { width: 0, height: height}; + break; } // console.log("Height: " + iframe.style.height); diff --git a/src/embeds/tiktok.ts b/src/embeds/tiktok.ts index 8aa2033..c7ca711 100644 --- a/src/embeds/tiktok.ts +++ b/src/embeds/tiktok.ts @@ -3,7 +3,7 @@ import { EmbedBase } from "./embedBase"; export class TikTokEmbed extends EmbedBase { name = "TikTok"; embedOrigin = "https://www.tiktok.com" - regex = new RegExp(/https:\/\/www\.tiktok\.com\/\@([\w\.]+)\/video\/(\d+)/); + regex = new RegExp(/https:\/\/www\.tiktok\.com\/@([\w.]+)\/video\/(\d+)/); createEmbed(url: string): HTMLElement { const regexMatch = url.match(this.regex); @@ -19,8 +19,13 @@ export class TikTokEmbed extends EmbedBase { iframe.src = `https://www.tiktok.com/embed/v2/${tiktokId}/`; iframe.classList.add(this.autoEmbedCssClass, "tiktok-embed"); - // iframe.dataset.tiktokId = tiktokId; iframe.setAttribute("allowfullscreen", ""); + + // console.log("Cache: " + JSON.stringify(this.sizeCache)) + iframe.dataset.tiktokId = tiktokId; + if (this.sizeCache[tiktokId] && this.sizeCache[tiktokId].height) { + iframe.style.height = this.sizeCache[tiktokId].height + "px"; + } return iframe; } @@ -58,6 +63,11 @@ export class TikTokEmbed extends EmbedBase { if (iframe.contentWindow == e.source) { iframe.style.height = data.height + "px"; + + const tiktokId = iframe.dataset.tiktokId; + if (tiktokId) + this.sizeCache[tiktokId] = { width: 0, height: height}; + break; } diff --git a/src/embeds/twitter.ts b/src/embeds/twitter.ts index 73efb03..8b8973c 100644 --- a/src/embeds/twitter.ts +++ b/src/embeds/twitter.ts @@ -19,12 +19,17 @@ export class TwitterEmbed extends EmbedBase { url = `https://platform.twitter.com/embed/Tweet.html?dnt=true&theme=${this.plugin.settings.darkMode ? "dark" : "light"}&id=${postId}`; iframe.src = url; - iframe.classList.add(this.autoEmbedCssClass, "twitter-embed", "twitter-" + postId); + iframe.classList.add(this.autoEmbedCssClass, "twitter-embed"); iframe.sandbox.add("allow-forms", "allow-presentation", "allow-same-origin", "allow-scripts", "allow-modals", "allow-popups"); iframe.setAttribute("scrolling", "no"); // iframe.setAttribute("allowfullscreen", ""); + iframe.dataset.twitterPostId = postId; + if (this.sizeCache[postId] && this.sizeCache[postId].height) { + iframe.style.height = this.sizeCache[postId].height + "px"; + } + return iframe; } @@ -46,17 +51,20 @@ export class TwitterEmbed extends EmbedBase { } */ + // console.log("Twitter Message: " + JSON.stringify(e.data["twttr.embed"])) + // Only continue if the method is for resizing if (e.data["twttr.embed"]["method"] !== "twttr.private.resize") return; const params = e.data["twttr.embed"]["params"][0]; + const postId = params["data"]["tweet_id"]; + // console.log("Tweet-id: " + params["data"]["tweet_id"]); - // Why use class instead of id for getting the reference: + // Why use querySelectorAll instead of querySelector for getting the reference: // There might be multiple iframes, some in Reading mode and Live preview. // Some might even duplicates if they user has duplicates - const iframes = document.getElementsByClassName("twitter-" + params["data"]["tweet_id"]); - // console.log("Tweet-id: " + params["data"]["tweet_id"]); + const iframes = document.querySelectorAll(`.twitter-embed[data-twitter-post-id="${postId}"]`); if (iframes.length === 0) return; @@ -64,8 +72,12 @@ export class TwitterEmbed extends EmbedBase { for (let i = 0; i < iframes.length; ++i) { const iframe = iframes[i] as HTMLIFrameElement; - iframe.style.height = ((params["height"] as number) + 1) + "px"; + const height = (params["height"] as number) + 1; // iframe.style.width = ((params["width"] as number) + 1) + "px"; + iframe.style.height = height + "px"; + + if (postId) + this.sizeCache[postId] = { width: 0, height: height}; } } } \ No newline at end of file diff --git a/src/utility.ts b/src/utility.ts index 5c34470..bf0521c 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -11,4 +11,13 @@ export function isURL(str: string) : boolean { export function isLinkToImage(url: string) : boolean { return /\.(jpg|jpeg|png|webp|avif|gif)$/.test(url); +} + +export interface Dictionary { + [key: string]: T; +} + +export interface Size { + width: number; + height: number; } \ No newline at end of file