feature: allow embed <replay-web-page> to also support updating favicons from replay (#287)
It may be useful to be able to have <replay-web-page> embed also take over the favicons for the page, propagating them from the replay (eg. if in a single page app where replay takes up most of the view). This PR adds a `updateFavicons` attribute, used as `<replay-web-page ... updateFavicons>` to have icons propagated from the replay to the top-level frame. (This may be especially useful with `deepLink` option, which allows for linking directly to archived pages) Details: - enabled via 'updateFavicons' attribute on <replay-web-page> - pass icons via postMessage() update, adding type to embed message to allow different types of message - currently supporting 'urlchange' and 'favicons' message types - add shared updateFaviconLinks() in misc.ts, used by both embed and main app --------- Co-authored-by: emma <hi@emma.cafe>
This commit is contained in:
+4
-15
@@ -14,6 +14,7 @@ import {
|
|||||||
IS_APP,
|
IS_APP,
|
||||||
VERSION,
|
VERSION,
|
||||||
clickOnSpacebarPress,
|
clickOnSpacebarPress,
|
||||||
|
updateFaviconLinks,
|
||||||
} from "./misc";
|
} from "./misc";
|
||||||
|
|
||||||
import fasHelp from "@fortawesome/fontawesome-free/svgs/solid/question-circle.svg";
|
import fasHelp from "@fortawesome/fontawesome-free/svgs/solid/question-circle.svg";
|
||||||
@@ -24,6 +25,7 @@ import "./item";
|
|||||||
import "./item-index";
|
import "./item-index";
|
||||||
import "./chooser";
|
import "./chooser";
|
||||||
import { type LoadInfo } from "./item";
|
import { type LoadInfo } from "./item";
|
||||||
|
import type { FavIconEventDetail } from "./types";
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
@customElement("replay-app-main")
|
@customElement("replay-app-main")
|
||||||
@@ -480,21 +482,8 @@ export class ReplayWebApp extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'event' implicitly has an 'any' type.
|
onFavIcons(event: CustomEvent<FavIconEventDetail>) {
|
||||||
onFavIcons(event) {
|
updateFaviconLinks(event.detail);
|
||||||
const head = document.querySelector("head")!;
|
|
||||||
const oldLinks = document.querySelectorAll("link[rel*='icon']");
|
|
||||||
|
|
||||||
for (const link of oldLinks) {
|
|
||||||
head.removeChild(link);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const icon of event.detail.icons) {
|
|
||||||
const link = document.createElement("link");
|
|
||||||
link.rel = icon.rel;
|
|
||||||
link.href = icon.href;
|
|
||||||
head.appendChild(link);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'event' implicitly has an 'any' type.
|
// @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'event' implicitly has an 'any' type.
|
||||||
|
|||||||
+62
-20
@@ -7,9 +7,20 @@ import {
|
|||||||
} from "lit";
|
} from "lit";
|
||||||
import { ifDefined } from "lit/directives/if-defined.js";
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
import { wrapCss, rwpLogo } from "./misc";
|
import { wrapCss, rwpLogo, updateFaviconLinks } from "./misc";
|
||||||
import { SWManager } from "./swmanager";
|
import { SWManager } from "./swmanager";
|
||||||
import { property } from "lit/decorators.js";
|
import { property } from "lit/decorators.js";
|
||||||
|
import type { FavIconEventDetail } from "./types";
|
||||||
|
import type { EmbedReplayData } from "./item";
|
||||||
|
|
||||||
|
type IframeMessage = MessageEvent<
|
||||||
|
| ({
|
||||||
|
type: "urlchange";
|
||||||
|
} & EmbedReplayData)
|
||||||
|
| ({
|
||||||
|
type: "favicons";
|
||||||
|
} & FavIconEventDetail)
|
||||||
|
>;
|
||||||
|
|
||||||
const scriptSrc =
|
const scriptSrc =
|
||||||
document.currentScript && (document.currentScript as HTMLScriptElement).src;
|
document.currentScript && (document.currentScript as HTMLScriptElement).src;
|
||||||
@@ -46,6 +57,7 @@ class Embed extends LitElement {
|
|||||||
@property({ type: String }) hashString: string | undefined;
|
@property({ type: String }) hashString: string | undefined;
|
||||||
|
|
||||||
@property({ type: Boolean }) deepLink = false;
|
@property({ type: Boolean }) deepLink = false;
|
||||||
|
@property({ type: Boolean }) updateFavicons = false;
|
||||||
@property({ type: Boolean }) sandbox = false;
|
@property({ type: Boolean }) sandbox = false;
|
||||||
@property({ type: Boolean }) noSandbox: boolean | null = null;
|
@property({ type: Boolean }) noSandbox: boolean | null = null;
|
||||||
@property({ type: Boolean }) noWebWorker = false;
|
@property({ type: Boolean }) noWebWorker = false;
|
||||||
@@ -113,30 +125,53 @@ class Embed extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'event' implicitly has an 'any' type.
|
handleMessage(event: IframeMessage) {
|
||||||
handleMessage(event) {
|
|
||||||
const iframe = this.renderRoot.querySelector("iframe");
|
const iframe = this.renderRoot.querySelector("iframe");
|
||||||
|
|
||||||
if (iframe && event.source === iframe.contentWindow) {
|
if (iframe && event.source === iframe.contentWindow) {
|
||||||
if (!event.data.view) {
|
switch (event.data.type) {
|
||||||
return;
|
case "urlchange":
|
||||||
|
if (this.deepLink) {
|
||||||
|
this.handleUrlChangeMessage(event.data);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "favicons":
|
||||||
|
if (this.updateFavicons) {
|
||||||
|
updateFaviconLinks(event.data);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.data.title) {
|
handleUrlChangeMessage(data: EmbedReplayData) {
|
||||||
this.title = event.data.title;
|
const { url, ts, view, query, title } = data;
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
this.title = title;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.deepLink) {
|
const params: Record<string, string> = {};
|
||||||
return;
|
|
||||||
|
if (url) {
|
||||||
|
params.url = url;
|
||||||
|
}
|
||||||
|
if (ts) {
|
||||||
|
params.ts = ts;
|
||||||
|
}
|
||||||
|
if (query) {
|
||||||
|
params.query = query;
|
||||||
|
}
|
||||||
|
if (view && !url) {
|
||||||
|
params.view = view;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Fix this the next time the file is edited.
|
const currHash = new URLSearchParams(params);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
||||||
const currHash = new URLSearchParams(event.data);
|
const fullUrl = new URL(window.location.href);
|
||||||
const url = new URL(window.location.href);
|
fullUrl.hash = "#" + currHash.toString();
|
||||||
url.hash = "#" + currHash.toString();
|
window.history.replaceState({}, "", fullUrl);
|
||||||
window.history.replaceState({}, "", url);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated() {
|
firstUpdated() {
|
||||||
@@ -149,7 +184,9 @@ class Embed extends LitElement {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.doRegister();
|
this.doRegister();
|
||||||
|
|
||||||
window.addEventListener("message", (event) => this.handleMessage(event));
|
window.addEventListener("message", (event: IframeMessage) =>
|
||||||
|
this.handleMessage(event),
|
||||||
|
);
|
||||||
|
|
||||||
if (this.deepLink) {
|
if (this.deepLink) {
|
||||||
this.updateFromHash();
|
this.updateFromHash();
|
||||||
@@ -295,12 +332,17 @@ class Embed extends LitElement {
|
|||||||
params as unknown as Record<string, string>,
|
params as unknown as Record<string, string>,
|
||||||
).toString();
|
).toString();
|
||||||
|
|
||||||
this.hashString = new URLSearchParams({
|
const hashParams: Record<string, string> = {
|
||||||
url: this.url,
|
url: this.url,
|
||||||
ts: this.ts,
|
ts: this.ts,
|
||||||
query: this.query,
|
query: this.query,
|
||||||
view: this.view,
|
};
|
||||||
}).toString();
|
|
||||||
|
if (!this.url) {
|
||||||
|
hashParams.view = this.view;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hashString = new URLSearchParams(hashParams).toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+27
-16
@@ -57,7 +57,7 @@ import fasCaretDown from "@fortawesome/fontawesome-free/svgs/solid/caret-down.sv
|
|||||||
import { RWPEmbedReceipt } from "./embed-receipt";
|
import { RWPEmbedReceipt } from "./embed-receipt";
|
||||||
import Split from "split.js";
|
import Split from "split.js";
|
||||||
|
|
||||||
import type { ItemType, URLResource } from "./types";
|
import type { FavIconEventDetail, ItemType, URLResource } from "./types";
|
||||||
import type { Replay } from "./replay";
|
import type { Replay } from "./replay";
|
||||||
import { ifDefined } from "lit/directives/if-defined.js";
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
@@ -85,6 +85,21 @@ export type LoadInfo = {
|
|||||||
importCollId?: string;
|
importCollId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type EmbedReplayData = {
|
||||||
|
view?: "story" | "pages" | "resources";
|
||||||
|
url?: string;
|
||||||
|
ts?: string;
|
||||||
|
title?: string;
|
||||||
|
query?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TabData = EmbedReplayData & {
|
||||||
|
multiTs?: string[];
|
||||||
|
currList?: number;
|
||||||
|
urlSearchType?: string;
|
||||||
|
currMime?: string;
|
||||||
|
};
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
class Item extends LitElement {
|
class Item extends LitElement {
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
@@ -112,16 +127,7 @@ class Item extends LitElement {
|
|||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
|
||||||
@property({ type: Object, attribute: false })
|
@property({ type: Object, attribute: false })
|
||||||
tabData: {
|
tabData: TabData = {};
|
||||||
view?: "story" | "pages" | "resources";
|
|
||||||
url?: string;
|
|
||||||
ts?: string;
|
|
||||||
multiTs?: string[];
|
|
||||||
currList?: number;
|
|
||||||
query?: string;
|
|
||||||
urlSearchType?: string;
|
|
||||||
currMime?: string;
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
url = "";
|
url = "";
|
||||||
@@ -328,7 +334,11 @@ class Item extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.embed && window.parent !== window) {
|
if (this.embed && window.parent !== window) {
|
||||||
window.parent.postMessage(this.tabData, "*");
|
const { url, ts, view, query, title }: EmbedReplayData = this.tabData;
|
||||||
|
window.parent.postMessage(
|
||||||
|
{ type: "urlchange", url, ts, view, query, title },
|
||||||
|
"*",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._locUpdateNeeded = false;
|
this._locUpdateNeeded = false;
|
||||||
@@ -1698,11 +1708,12 @@ class Item extends LitElement {
|
|||||||
this.showSidebar = false;
|
this.showSidebar = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'event' implicitly has an 'any' type.
|
async onFavIcons(event: CustomEvent<FavIconEventDetail>) {
|
||||||
async onFavIcons(event) {
|
if (this.embed && window.parent !== window) {
|
||||||
|
window.parent.postMessage({ type: "favicons", ...event.detail }, "*");
|
||||||
|
}
|
||||||
|
|
||||||
for (const icon of event.detail.icons) {
|
for (const icon of event.detail.icons) {
|
||||||
// TODO: Fix this the next time the file is edited.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
||||||
const resp = await fetch(icon.href);
|
const resp = await fetch(icon.href);
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
const ct = resp.headers.get("Content-Type");
|
const ct = resp.headers.get("Content-Type");
|
||||||
|
|||||||
+20
@@ -7,6 +7,7 @@ import { styleMap } from "lit/directives/style-map.js";
|
|||||||
import allCssRaw from "../assets/main.scss";
|
import allCssRaw from "../assets/main.scss";
|
||||||
|
|
||||||
import rwpLogo from "../assets/logo.svg";
|
import rwpLogo from "../assets/logo.svg";
|
||||||
|
import type { FavIconEventDetail } from "./types";
|
||||||
|
|
||||||
const apiPrefix = "./w/api";
|
const apiPrefix = "./w/api";
|
||||||
const replayPrefix = "./w";
|
const replayPrefix = "./w";
|
||||||
@@ -37,6 +38,25 @@ function clickOnSpacebarPress(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update favicon links from an array of {rel, href} objects
|
||||||
|
// remove all existing icon links
|
||||||
|
// used by both embed and main app
|
||||||
|
export function updateFaviconLinks(data: FavIconEventDetail) {
|
||||||
|
const head = document.querySelector("head")!;
|
||||||
|
const oldLinks = document.querySelectorAll("link[rel*='icon']");
|
||||||
|
|
||||||
|
for (const link of oldLinks) {
|
||||||
|
head.removeChild(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const icon of data.icons) {
|
||||||
|
const link = document.createElement("link");
|
||||||
|
link.rel = icon.rel;
|
||||||
|
link.href = icon.href;
|
||||||
|
head.appendChild(link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
class FaIcon extends LitElement {
|
class FaIcon extends LitElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
@@ -50,3 +50,10 @@ export type ItemType = {
|
|||||||
totalSize?: unknown;
|
totalSize?: unknown;
|
||||||
size?: number | string;
|
size?: number | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FavIconEventDetail = {
|
||||||
|
icons: {
|
||||||
|
rel: string;
|
||||||
|
href: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user