/** * Handles DOM nodes to keep alive inside a Vue component */ class DomKeepAlive { /** * Holds a mapping of all generated IDs to their according child node fragments */ protected fragments = new Map() /** * Registers the component in Vue * @param Vue The Vue constructor to use */ public constructor (Vue: any) { if (!('dom-keep-alive' in Vue.options.components)) { Vue.component('dom-keep-alive', { render: createElement => createElement('div') }) } } /** * Generates a random ID * @return The generated ID */ protected generateId (): string { // @ts-ignore return `dom-keep-alive--${([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16))}` } /** * Takes the child nodes of an element and transfers them to a DocumentFragment * @param target The element to take child nodes from * @return The DocumentFragment containing the target's child nodes */ protected childNodesToFragment (target: Element) { const frag = document.createDocumentFragment() for (const node of Array.from(target.childNodes)) { frag.appendChild(node) } return frag } /** * Checks if the given element contains an element with the given ID (or has the ID itself) * @param target The target for looking up the ID * @param id The ID to check for * @return The element with the present ID, null otherwise */ protected containsId (target: Element, id: string) { return target.id === id ? target : target.querySelector('#' + id) } /** * Waits (via MutationObserver) for a certain element ID to be present/gone from the subtree of a target. Resolves immediately if the ID is present/gone from the beginning. * @param target The element that will be observed for the given ID to be present/gone * @param id The element ID to look out for * @param present If to look for an added (true) or removed (false) ID * @return Resolves when the ID is present/gone */ protected waitForId (target: Element, id: string, present: boolean) { return new Promise((resolve, reject) => { const el = this.containsId(target, id) if (present ? el : !el) { resolve(present ? el : null) } else { const observer = new MutationObserver(mutations => { let foundElement = null beforeMutations: { for (const mutation of mutations) { for (const node of mutation[`${present ? 'added' : 'removed'}Nodes`]) { if (node instanceof Element) { const el = this.containsId(node, id) if (el) { foundElement = el break beforeMutations } } } } } if (foundElement) { observer.disconnect() resolve(foundElement) } }) observer.observe(target, { childList: true, subtree: true }) } }) } /** * Waits for a certain element ID to be present from the subtree of a target. Resolves immediately if the ID is present from the beginning. * @param target The element that will be observed for the given ID to be present * @param id The element ID to look out for * @return Resolves when the ID is present */ protected waitForIdPresent (target: Element, id: string) { return this.waitForId(target, id, true) as Promise } /** * Waits for a certain element ID to be gone from the subtree of a target. Resolves immediately if the ID is not present from the beginning. * @param target The element that will be observed for the given ID to be gone * @param id The element ID to look out for * @return Resolves when the ID is gone */ protected waitForIdGone (target: Element, id: string) { return this.waitForId(target, id, false) } /** * Prepare a render target element with all its contained keep-alive nodes * @param renderTarget The render root node * @return An initializer function that can be passed the Vue root node and handles contained keep-alive nodes */ public prepareRenderTarget (renderTarget: Element) { const ids = [] for (const element of Array.from(renderTarget.querySelectorAll('dom-keep-alive'))) { ids.push(this.prepareKeepAlive(element)) } return (vueRoot: Element) => this.initKeepAliveLifecycles(vueRoot, ids) } /** * Prepare a single keep-alive element * @param keepAliveElement The keep-alive node to prepare * @return The ID generated for the keep-alive node */ public prepareKeepAlive (keepAliveElement: Element) { const id = this.generateId() keepAliveElement.setAttribute('id', id) this.fragments.set(id, this.childNodesToFragment(keepAliveElement)) return id } /** * Initialize the lifecycle of a keep-alive node inside a render target * @param renderTarget The render target to check for the keep-alive node * @param keepAliveId The ID the surveilled keep-alive node will have */ public async initKeepAliveLifecycle (renderTarget, keepAliveId) { const appearedEl = await this.waitForIdPresent(renderTarget, keepAliveId) if (this.fragments.has(keepAliveId)) { const frag = this.fragments.get(keepAliveId) appearedEl.appendChild(frag) } const removedEl = await this.waitForIdGone(renderTarget, keepAliveId) this.fragments.set(keepAliveId, this.childNodesToFragment(removedEl)) this.initKeepAliveLifecycle(renderTarget, keepAliveId) } /** * Initialize the lifecycle of multiple keep-alive nodes inside a render target * @param renderTarget The render target to check for keep-alive nodes * @param keepAliveIds The IDs the surveilled keep-alive nodes */ public initKeepAliveLifecycles (target: Element, keepAliveIds: string) { for (const id of keepAliveIds) { this.initKeepAliveLifecycle(target, id) } } }