UNPKG

@goodpie/html-typer

Version:

Types out HTML into a view, supporting vanilla JS

121 lines (120 loc) 5.17 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.animateHtmlRendering = void 0; const dompurify_1 = __importDefault(require("dompurify")); /** * Animates the rendering of HTML content by gradually adding text. * * @param container - The element to render content into. * @param html - The HTML string to animate. * @param speed - The interval in milliseconds between each character. * @returns A promise that resolves when the animation is complete. */ const animateHtmlRendering = (container_1, html_1, ...args_1) => __awaiter(void 0, [container_1, html_1, ...args_1], void 0, function* (container, html, speed = 50) { speed = Math.max(1, speed); // Sanitize HTML before processing const sanitizedHtml = dompurify_1.default.sanitize(html); container.innerHTML = ''; const chunks = splitHtmlString(sanitizedHtml); // animateTextContent now has access to container via closure. const animateTextContent = (element, text, speed) => { return new Promise(resolve => { let index = 0; const textInterval = setInterval(() => { if (index < text.length) { element.textContent += text[index++]; } else { clearInterval(textInterval); resolve(); } // Auto-scroll to the bottom of the container's parent requestAnimationFrame(() => { if (container.parentElement) { container.parentElement.scrollTop = container.parentElement.scrollHeight; } }); }, speed); }); }; const processChunk = (chunks, parent, speed) => __awaiter(void 0, void 0, void 0, function* () { for (const chunk of chunks) { if (chunk.tagName === "#text") { const textNode = document.createTextNode(""); parent.appendChild(textNode); yield animateTextContent(textNode, chunk.content || "", speed); } else { const newElement = document.createElement(chunk.tagName); if (chunk.classes && chunk.classes.length > 0) { newElement.classList.add(...chunk.classes); } parent.appendChild(newElement); if (chunk.content) { yield animateTextContent(newElement, chunk.content, speed); } if (chunk.children && chunk.children.length > 0) { yield processChunk(chunk.children, newElement, speed); } } } }); yield processChunk(chunks, container, speed); }); exports.animateHtmlRendering = animateHtmlRendering; /** * Splits an HTML string into a structured set of chunks for animation. * * @param input - The HTML string to parse. * @returns An array of chunk objects representing the DOM structure. */ const splitHtmlString = (input) => { const parser = new DOMParser(); const doc = parser.parseFromString(input, 'text/html'); const result = []; const traverse = (node, parent = null) => { if (node.nodeType === Node.ELEMENT_NODE) { const element = { tagName: node.tagName.toLowerCase(), classes: node.className ? node.className.split(/\s+/).filter(c => !!c) : [], children: [] }; if (parent) { parent.children.push(element); } else { result.push(element); } node.childNodes.forEach(child => traverse(child, element)); } else if (node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim()) { const textChunk = { tagName: "#text", content: node.textContent.trim() }; // Push text nodes at any level, including top-level (when parent is null) if (parent) { parent.children.push(textChunk); } else { result.push(textChunk); } } }; doc.body.childNodes.forEach(node => traverse(node)); return result; };