wui-api
Version:
WotsUI API Classes and Utils
221 lines (220 loc) • 7.96 kB
JavaScript
import { TypewriterElementStatus, } from "./types.js";
export default class Typewriter {
constructor(settings) {
this._elementsData = new Map();
this._settings = Typewriter.parseSettings(settings);
}
initElement(el, settings) {
if (!el || !(el instanceof Element)) {
throw new Error("Unable to parse, not an Element");
}
const textNodesData = this.parseElementData(el);
const charsCount = this.parseElementCharsCount(textNodesData);
const status = TypewriterElementStatus.Initial;
const lastNodeIndex = textNodesData.length - 1;
const lastCharIndex = textNodesData[lastNodeIndex].length - 1;
const elSettings = settings
? Typewriter.parseSettings(settings)
: undefined;
const data = {
settings: elSettings,
textNodesData,
charsCount,
status,
lastNodeIndex,
lastCharIndex,
};
this._elementsData.set(el, data);
}
clearText(el) {
if (!el || !(el instanceof Element))
return;
const data = this._elementsData.get(el);
if (!data)
return;
const { textNodesData } = data;
textNodesData.forEach((tnd) => {
tnd.node.textContent = "";
});
data.status = TypewriterElementStatus.Clear;
data.lastNodeIndex = 0;
data.lastCharIndex = 0;
}
async deleteText(el) {
if (!el || !(el instanceof Element))
return;
const data = this._elementsData.get(el);
if (!data || data.status === TypewriterElementStatus.InProgress)
return;
data.status = TypewriterElementStatus.InProgress;
const settings = this.getSettings(el);
const { lastNodeIndex, textNodesData } = data;
let i = lastNodeIndex;
for (; i >= 0; i--) {
const { node, text } = textNodesData[i];
if (!text || text === "" || !node.textContent)
continue;
while (node.textContent && node.textContent.length > 0) {
// stop
if (data.status !== TypewriterElementStatus.InProgress) {
data.lastNodeIndex = i;
data.lastCharIndex = node.textContent.length - 1;
return;
}
node.textContent = node.textContent.slice(0, -1);
const { deleteModifier, timePerChar } = settings;
// default timePerChar
const timeToWait = this.getTimeToWait(settings);
await new Promise((resolve) => {
setTimeout(() => {
resolve(null);
}, timeToWait);
});
}
}
}
restoreText(el) {
if (!el || !(el instanceof Element))
return;
const data = this._elementsData.get(el);
if (!data)
return;
const { textNodesData } = data;
textNodesData.forEach((tnd) => {
tnd.node.textContent = tnd.text;
});
data.status = TypewriterElementStatus.Initial;
data.lastNodeIndex = textNodesData.length - 1;
data.lastCharIndex = textNodesData[data.lastNodeIndex].length - 1;
}
async writeText(el) {
if (!el || !(el instanceof Element))
return;
const data = this._elementsData.get(el);
if (!data || data.status === TypewriterElementStatus.InProgress)
return;
data.status = TypewriterElementStatus.InProgress;
const settings = this.getSettings(el);
const { lastCharIndex, lastNodeIndex, textNodesData } = data;
for (let i = 0; i < textNodesData.length; i++) {
const { node, text } = textNodesData[i];
if (!text || text === "" || i < lastNodeIndex)
continue;
let j = i === lastNodeIndex ? lastCharIndex : 0;
for (; j < text.length; j++) {
// stop
if (data.status !== TypewriterElementStatus.InProgress) {
data.lastNodeIndex = i;
data.lastCharIndex = j;
return;
}
const char = text[j];
node.textContent += char;
const timeToWait = this.getTimeToWait(settings, char);
await new Promise((resolve) => {
setTimeout(() => {
resolve(null);
}, timeToWait);
});
}
}
data.status = TypewriterElementStatus.Initial;
data.lastNodeIndex = textNodesData.length - 1;
data.lastCharIndex = textNodesData[data.lastNodeIndex].text.length - 1;
}
stopText(el) {
if (!el || !(el instanceof Element))
return;
const data = this._elementsData.get(el);
if (!data || data.status !== TypewriterElementStatus.InProgress)
return;
data.status = TypewriterElementStatus.Partial;
}
parseElementData(src) {
let textNodesData = [];
src.childNodes.forEach((node) => {
if (!node.textContent)
return;
if (node.nodeType === 3) {
textNodesData.push({
node,
text: node.textContent,
length: node.textContent.length,
});
}
else {
textNodesData = textNodesData.concat(this.parseElementData(node));
}
});
return textNodesData;
}
parseElementCharsCount(textNodesData) {
let charsCount = 0;
textNodesData.forEach((tn) => {
charsCount += tn.length;
});
return charsCount;
}
getTimeToWait(settings, char) {
const timePerChar = settings.timePerChar || 25;
// delete
if (typeof char !== "string") {
const deleteModifier = settings.deleteModifier || 1;
return timePerChar * deleteModifier;
}
if (char.match(/\W/g)) {
if (char.match(/[\@\{\}\[\]\(\)]/)) {
return timePerChar * 2.5;
}
if (char.match(/[\,\>\<\%\$\€]/)) {
return timePerChar * 5;
}
if (char.match(/[:;]/)) {
return timePerChar * 10;
}
if (char.match(/[\.\?\!]/)) {
return timePerChar * 15;
}
}
return timePerChar;
}
/* accessors */
getSettings(el) {
const data = this._elementsData.get(el);
if (!data || !data.settings)
return this._settings;
return { ...this._settings, ...data.settings };
}
setSettings(settings, el) {
const newSettings = Typewriter.parseSettings(settings);
if (!el) {
this._settings = { ...this._settings, ...newSettings };
return this._settings;
}
const data = this._elementsData.get(el);
if (!data) {
return;
}
data.settings = { ...data.settings, ...newSettings };
return data.settings;
}
/* static methods */
static parseSettings(obj, onlyDefinedProps = true) {
const settings = {};
if (typeof obj !== "object")
return settings;
if (!onlyDefinedProps || typeof obj.ignorePunctuation === "boolean") {
settings.ignorePunctuation = !!obj.ignorePunctuation;
}
if (typeof obj.timePerChar === "number") {
settings.timePerChar = obj.timePerChar;
}
if (typeof obj.deleteModifier === "number") {
settings.deleteModifier = obj.deleteModifier;
}
else {
settings.deleteModifier = settings.timePerChar;
}
return settings;
}
}