/** * HTML manipulation utilities */ /** * Strips fake background wrapper elements from HTML content. * These elements are used by the inline toolbar for visual selection highlighting * and should not be persisted in saved data. * @param html - HTML content that may contain fake background elements * @returns HTML content with fake background wrappers removed but their content preserved */ export const stripFakeBackgroundElements = (html: string): string => { if (!html || !html.includes('data-blok-fake-background')) { return html; } const tempDiv = document.createElement('div'); tempDiv.innerHTML = html; const fakeBackgrounds = tempDiv.querySelectorAll('[data-blok-fake-background="true"]'); fakeBackgrounds.forEach((element) => { const parent = element.parentNode; if (!parent) { return; } while (element.firstChild) { parent.insertBefore(element.firstChild, element); } parent.removeChild(element); }); return tempDiv.innerHTML; }; /** * An intersection rather than `interface extends Element`: TypeScript 6's * lib.dom.d.ts declares `moveBefore` as a required member of ParentNode, and an * interface may not redeclare an inherited member as optional. Intersections * skip that declaration-compatibility check, so this compiles on both the 5.9 * libs (no `moveBefore` at all) and the 6.0 libs (required `moveBefore`). */ type StatefulMoveParent = Element & { moveBefore?(node: Node, reference: Node | null): void; }; /** * Repositions an already-attached node within `parent`, inserting it before * `reference` (or at the end when `reference` is null), while preserving the * node's live state — an iframe keeps its loaded document, media keeps playing, * form fields keep focus and value. * * A plain insertBefore/appendChild detaches and re-attaches the node, which * resets exactly that state (re-parented iframes reload). The browser's * state-preserving move (`moveBefore`, Chrome 133+) avoids the detach; we fall * back to insertBefore where it is unavailable or throws (e.g. cross-document * moves, or a node that is not connected). */ const statefulMove = (parent: StatefulMoveParent, node: Node, reference: Node | null): void => { if (reference === node) { return; } if (typeof parent.moveBefore === 'function' && node.isConnected && parent.isConnected) { try { parent.moveBefore(node, reference); return; } catch { // moveBefore enforces stricter invariants than insertBefore (same // document, connected node); fall back to a plain, state-resetting insert. } } parent.insertBefore(node, reference); }; /** * Moves `node` to sit immediately before `reference`, preserving live state. * @see statefulMove */ export const moveElementBefore = (node: Node, reference: Element): void => { const parent = reference.parentNode; if (parent instanceof Element) { statefulMove(parent, node, reference); } }; /** * Moves `node` to sit immediately after `reference`, preserving live state. * @see statefulMove */ export const moveElementAfter = (node: Node, reference: Element): void => { const parent = reference.parentNode; if (parent instanceof Element) { statefulMove(parent, node, reference.nextSibling); } }; /** * Moves `node` to the end of `parent`'s children, preserving live state. * @see statefulMove */ export const moveElementToEnd = (parent: Element, node: Node): void => { statefulMove(parent, node, null); };