136 lines
3.8 KiB
TypeScript
136 lines
3.8 KiB
TypeScript
import { isExternal, fragmentFromString } from "./common";
|
|
|
|
import { ScriptManager } from "./script";
|
|
import { rewriteURLs } from "./rewrite-urls";
|
|
|
|
import { ResponseContext, ResponseContextOk } from "./fetch";
|
|
import { HyPushState } from ".";
|
|
|
|
const CANONICAL_SEL = "link[rel=canonical]";
|
|
const META_DESC_SEL = "meta[name=description]";
|
|
|
|
export interface ReplaceContext extends ResponseContext {
|
|
title: string;
|
|
document: Document;
|
|
replaceEls: (Element | null)[];
|
|
scripts: Array<[HTMLScriptElement, HTMLScriptElement]>;
|
|
}
|
|
|
|
export class UpdateManager {
|
|
private parent!: HyPushState;
|
|
private scriptManager!: ScriptManager;
|
|
|
|
constructor(parent: HyPushState) {
|
|
this.parent = parent;
|
|
this.scriptManager = new ScriptManager(parent);
|
|
}
|
|
|
|
get el() {
|
|
return this.parent;
|
|
}
|
|
get replaceSelector() {
|
|
return this.parent.replaceSelector;
|
|
}
|
|
get scriptSelector() {
|
|
return this.parent.scriptSelector;
|
|
}
|
|
|
|
// Extracts the elements to be replaced
|
|
private getReplaceElements(doc: Document): (Element | null)[] {
|
|
if (this.replaceSelector) {
|
|
return this.replaceSelector
|
|
.split(",")
|
|
.map((sel) => doc.querySelector(sel));
|
|
} else if (this.el.id) {
|
|
return [doc.getElementById(this.el.id)];
|
|
} else {
|
|
const index = Array.from(
|
|
document.getElementsByTagName(this.el.tagName),
|
|
).indexOf(this.el);
|
|
return [doc.getElementsByTagName(this.el.tagName)[index]];
|
|
}
|
|
}
|
|
|
|
// Takes the response string and turns it into document fragments
|
|
// that can be inserted into the DOM.
|
|
responseToContent(context: ResponseContextOk): ReplaceContext {
|
|
const { responseText } = context;
|
|
|
|
const doc = new DOMParser().parseFromString(responseText, "text/html");
|
|
const { title = "" } = doc;
|
|
const replaceEls = this.getReplaceElements(doc);
|
|
|
|
if (replaceEls.every((el) => el == null)) {
|
|
throw new Error(
|
|
`Couldn't find any element in the document at '${location}'.`,
|
|
);
|
|
}
|
|
|
|
const scripts = this.scriptSelector
|
|
? this.scriptManager.removeScriptTags(replaceEls)
|
|
: [];
|
|
|
|
return { ...context, document: doc, title, replaceEls, scripts };
|
|
}
|
|
|
|
// Replaces the old elements with the new one, one-by-one.
|
|
private replaceContentWithSelector(
|
|
replaceSelector: string,
|
|
elements: (Element | null)[],
|
|
) {
|
|
replaceSelector
|
|
.split(",")
|
|
.map((sel) => document.querySelector(sel))
|
|
.forEach((oldElement, i) => {
|
|
const el = elements[i];
|
|
if (el) oldElement?.parentNode?.replaceChild(el, oldElement);
|
|
});
|
|
}
|
|
|
|
// When no `replaceIds` are set, replace the entire content of the component (slow).
|
|
private replaceContentWholesale([el]: (Element | null)[]) {
|
|
if (el) this.el.innerHTML = el.innerHTML;
|
|
}
|
|
|
|
private replaceContent(replaceEls: (Element | null)[]) {
|
|
if (this.replaceSelector) {
|
|
this.replaceContentWithSelector(this.replaceSelector, replaceEls);
|
|
} else {
|
|
this.replaceContentWholesale(replaceEls);
|
|
}
|
|
}
|
|
|
|
private replaceHead(doc: Document) {
|
|
const { head } = this.el.ownerDocument;
|
|
|
|
const canonicalEl = head.querySelector(
|
|
CANONICAL_SEL,
|
|
) as HTMLLinkElement | null;
|
|
const cEl = doc.head.querySelector(CANONICAL_SEL) as HTMLLinkElement | null;
|
|
if (canonicalEl && cEl) canonicalEl.href = cEl.href;
|
|
|
|
const metaDescEl = head.querySelector(
|
|
META_DESC_SEL,
|
|
) as HTMLMetaElement | null;
|
|
const mEl = doc.head.querySelector(META_DESC_SEL) as HTMLMetaElement | null;
|
|
if (metaDescEl && mEl) metaDescEl.content = mEl.content;
|
|
}
|
|
|
|
updateDOM(context: ReplaceContext) {
|
|
try {
|
|
const { replaceEls, document } = context;
|
|
if (isExternal(this.parent)) rewriteURLs(replaceEls, this.parent.href);
|
|
this.replaceHead(document);
|
|
this.replaceContent(replaceEls);
|
|
} catch (error) {
|
|
throw { ...context, error };
|
|
}
|
|
}
|
|
|
|
reinsertScriptTags(context: {
|
|
scripts: Array<[HTMLScriptElement, HTMLScriptElement]>;
|
|
}) {
|
|
return this.scriptManager.reinsertScriptTags(context);
|
|
}
|
|
}
|