@goodpie/html-typer
Version:
Types out HTML into a view, supporting vanilla JS
121 lines (120 loc) • 5.17 kB
JavaScript
;
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;
};