UNPKG

svelte

Version:

Cybernetically enhanced web apps

228 lines (188 loc) 5.23 kB
/** @import { Effect, TemplateNode } from '#client' */ import { Batch, current_batch } from '../../reactivity/batch.js'; import { branch, destroy_effect, move_effect, pause_effect, resume_effect } from '../../reactivity/effects.js'; import { hydrate_node, hydrating } from '../hydration.js'; import { create_text, should_defer_append } from '../operations.js'; /** * @typedef {{ effect: Effect, fragment: DocumentFragment }} Branch */ /** * @template Key */ export class BranchManager { /** @type {TemplateNode} */ anchor; /** @type {Map<Batch, Key>} */ #batches = new Map(); /** * Map of keys to effects that are currently rendered in the DOM. * These effects are visible and actively part of the document tree. * Example: * ``` * {#if condition} * foo * {:else} * bar * {/if} * ``` * Can result in the entries `true->Effect` and `false->Effect` * @type {Map<Key, Effect>} */ #onscreen = new Map(); /** * Similar to #onscreen with respect to the keys, but contains branches that are not yet * in the DOM, because their insertion is deferred. * @type {Map<Key, Branch>} */ #offscreen = new Map(); /** * Keys of effects that are currently outroing * @type {Set<Key>} */ #outroing = new Set(); /** * Whether to pause (i.e. outro) on change, or destroy immediately. * This is necessary for `<svelte:element>` */ #transition = true; /** * @param {TemplateNode} anchor * @param {boolean} transition */ constructor(anchor, transition = true) { this.anchor = anchor; this.#transition = transition; } #commit = () => { var batch = /** @type {Batch} */ (current_batch); // if this batch was made obsolete, bail if (!this.#batches.has(batch)) return; var key = /** @type {Key} */ (this.#batches.get(batch)); var onscreen = this.#onscreen.get(key); if (onscreen) { // effect is already in the DOM — abort any current outro resume_effect(onscreen); this.#outroing.delete(key); } else { // effect is currently offscreen. put it in the DOM var offscreen = this.#offscreen.get(key); if (offscreen) { this.#onscreen.set(key, offscreen.effect); this.#offscreen.delete(key); // remove the anchor... /** @type {TemplateNode} */ (offscreen.fragment.lastChild).remove(); // ...and append the fragment this.anchor.before(offscreen.fragment); onscreen = offscreen.effect; } } for (const [b, k] of this.#batches) { this.#batches.delete(b); if (b === batch) { // keep values for newer batches break; } const offscreen = this.#offscreen.get(k); if (offscreen) { // for older batches, destroy offscreen effects // as they will never be committed destroy_effect(offscreen.effect); this.#offscreen.delete(k); } } // outro/destroy all onscreen effects... for (const [k, effect] of this.#onscreen) { // ...except the one that was just committed // or those that are already outroing (else the transition is aborted and the effect destroyed right away) if (k === key || this.#outroing.has(k)) continue; const on_destroy = () => { const keys = Array.from(this.#batches.values()); if (keys.includes(k)) { // keep the effect offscreen, as another batch will need it var fragment = document.createDocumentFragment(); move_effect(effect, fragment); fragment.append(create_text()); // TODO can we avoid this? this.#offscreen.set(k, { effect, fragment }); } else { destroy_effect(effect); } this.#outroing.delete(k); this.#onscreen.delete(k); }; if (this.#transition || !onscreen) { this.#outroing.add(k); pause_effect(effect, on_destroy, false); } else { on_destroy(); } } }; /** * @param {Batch} batch */ #discard = (batch) => { this.#batches.delete(batch); const keys = Array.from(this.#batches.values()); for (const [k, branch] of this.#offscreen) { if (!keys.includes(k)) { destroy_effect(branch.effect); this.#offscreen.delete(k); } } }; /** * * @param {any} key * @param {null | ((target: TemplateNode) => void)} fn */ ensure(key, fn) { var batch = /** @type {Batch} */ (current_batch); var defer = should_defer_append(); if (fn && !this.#onscreen.has(key) && !this.#offscreen.has(key)) { if (defer) { var fragment = document.createDocumentFragment(); var target = create_text(); fragment.append(target); this.#offscreen.set(key, { effect: branch(() => fn(target)), fragment }); } else { this.#onscreen.set( key, branch(() => fn(this.anchor)) ); } } this.#batches.set(batch, key); if (defer) { for (const [k, effect] of this.#onscreen) { if (k === key) { batch.skipped_effects.delete(effect); } else { batch.skipped_effects.add(effect); } } for (const [k, branch] of this.#offscreen) { if (k === key) { batch.skipped_effects.delete(branch.effect); } else { batch.skipped_effects.add(branch.effect); } } batch.oncommit(this.#commit); batch.ondiscard(this.#discard); } else { if (hydrating) { this.anchor = hydrate_node; } this.#commit(); } } }