|
/** |
|
* 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<string, DocumentFragment>() |
|
|
|
/** |
|
* Registers the <dom-keep-alive /> 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<Element|null>((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<Element> |
|
} |
|
|
|
/** |
|
* 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) |
|
} |
|
} |
|
} |
You're absolutely right — sorry, I haven't used this code in quite some time. 😁
An alternative would be to use native DOM events instead of Vue events in this place:
Then you should be able (haven't tested, so no guarantees) to do this in your template:
And then implement an
onInsert
method which handles the event: