UNPKG

@hydecorp/push-state

Version:

Turn static web sites into dynamic web apps

115 lines (93 loc) 3.93 kB
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); } }