typeit
Version:
The most versatile animated typing utility on the planet.
996 lines (952 loc) • 27.1 kB
JavaScript
// TypeIt by Alex MacArthur - https://typeitjs.com
const isArray = (thing) => Array.isArray(thing);
const asArray = (value) => isArray(value) ? value : [value];
let Queue = function(initialItems) {
let add = function(steps) {
asArray(steps).forEach((step) => {
return _q.set(Symbol(step.char?.innerText), buildQueueItem({ ...step }));
});
return this;
};
let getTypeable = () => rawValues().filter((value) => value.typeable);
let set = function(index, item) {
let keys = [..._q.keys()];
_q.set(keys[index], buildQueueItem(item));
};
let buildQueueItem = (queueItem) => {
queueItem.shouldPauseCursor = function() {
return Boolean(this.typeable || this.cursorable || this.deletable);
};
return queueItem;
};
let reset = function() {
_q.forEach((item) => delete item.done);
};
let wipe = function() {
_q = /* @__PURE__ */ new Map();
add(initialItems);
};
let getQueue = () => _q;
let rawValues = () => Array.from(_q.values());
let destroy = (key) => _q.delete(key);
let getPendingQueueItems = () => {
const pending = [];
for (let [, value] of getQueue()) {
if (!value.done) {
pending.push(value);
}
}
return pending;
};
let getItems = (all = false) => all ? rawValues() : rawValues().filter((i) => !i.done);
let done = (key, shouldDestroy = false) => shouldDestroy ? _q.delete(key) : _q.get(key).done = true;
let _q = /* @__PURE__ */ new Map();
add(initialItems);
return {
add,
set,
wipe,
done,
reset,
destroy,
getItems,
getQueue,
getTypeable,
getPendingQueueItems
};
};
const DATA_ATTRIBUTE = "data-typeit-id";
const CURSOR_CLASS = "ti-cursor";
const END = "END";
const DEFAULT_STATUSES = {
started: false,
completed: false,
frozen: false,
destroyed: false
};
const DEFAULT_OPTIONS = {
breakLines: true,
cursor: {
autoPause: true,
autoPauseDelay: 500,
animation: {
frames: [0, 0, 1].map((n) => {
return { opacity: n };
}),
options: {
iterations: Infinity,
easing: "steps(2, start)",
fill: "forwards"
}
}
},
cursorChar: "|",
cursorSpeed: 1e3,
deleteSpeed: null,
html: true,
lifeLike: true,
loop: false,
loopDelay: 750,
nextStringDelay: 750,
speed: 100,
startDelay: 250,
startDelete: false,
strings: [],
waitUntilVisible: false,
beforeString: () => {
},
afterString: () => {
},
beforeStep: () => {
},
afterStep: () => {
},
afterComplete: () => {
}
};
const PLACEHOLDER_CSS = `[${DATA_ATTRIBUTE}]:before {content: '.'; display: inline-block; width: 0; visibility: hidden;}`;
const createElement = (el) => document.createElement(el);
const createTextNode = (content) => document.createTextNode(content);
const appendStyleBlock = (styles, id = "") => {
let styleBlock = createElement("style");
styleBlock.id = id;
styleBlock.appendChild(createTextNode(styles));
document.head.appendChild(styleBlock);
};
const calculateDelay = (delayArg) => {
if (!isArray(delayArg)) {
delayArg = [delayArg / 2, delayArg / 2];
}
return delayArg;
};
const randomInRange = (value, range) => {
return Math.abs(
Math.random() * (value + range - (value - range)) + (value - range)
);
};
let range = (val) => val / 2;
function calculatePace(options) {
let { speed, deleteSpeed, lifeLike } = options;
deleteSpeed = deleteSpeed !== null ? deleteSpeed : speed / 3;
return lifeLike ? [
randomInRange(speed, range(speed)),
randomInRange(deleteSpeed, range(deleteSpeed))
] : [speed, deleteSpeed];
}
const toArray = (val) => Array.from(val);
let expandTextNodes = (element) => {
[...element.childNodes].forEach((child) => {
if (child.nodeValue) {
[...child.nodeValue].forEach((c) => {
child.parentNode.insertBefore(createTextNode(c), child);
});
child.remove();
return;
}
expandTextNodes(child);
});
return element;
};
const getParsedBody = (content) => {
let doc = document.implementation.createHTMLDocument();
doc.body.innerHTML = content;
return expandTextNodes(doc.body);
};
function walkElementNodes(element, shouldReverse = false, shouldIncludeCursor = false) {
let cursor = element.querySelector(`.${CURSOR_CLASS}`);
let walker = document.createTreeWalker(element, NodeFilter.SHOW_ALL, {
acceptNode: (node) => {
if (cursor && shouldIncludeCursor) {
if (node.classList?.contains(CURSOR_CLASS)) {
return NodeFilter.FILTER_ACCEPT;
}
if (cursor.contains(node)) {
return NodeFilter.FILTER_REJECT;
}
}
return node.classList?.contains(CURSOR_CLASS) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
}
});
let nextNode;
let nodes = [];
while (nextNode = walker.nextNode()) {
if (!nextNode.originalParent) {
nextNode.originalParent = nextNode.parentNode;
}
nodes.push(nextNode);
}
return shouldReverse ? nodes.reverse() : nodes;
}
function chunkStringAsHtml(string) {
return walkElementNodes(getParsedBody(string));
}
function maybeChunkStringAsHtml(str, asHtml = true) {
return asHtml ? chunkStringAsHtml(str) : toArray(str).map(createTextNode);
}
const cleanUpSkipped = ({
index,
newIndex,
queueItems,
cleanUp
}) => {
for (let i = index + 1; i < newIndex + 1; i++) {
cleanUp(queueItems[i][0]);
}
};
const isNumber = (value) => Number.isInteger(value);
const countStepsToSelector = ({
queueItems,
selector,
cursorPosition,
to
}) => {
if (isNumber(selector)) {
return selector * -1;
}
let isMovingToEnd = new RegExp(END, "i").test(to);
let selectorIndex = selector ? [...queueItems].reverse().findIndex(({ char }) => {
let parentElement = char.parentElement;
let parentMatches = parentElement.matches(selector);
if (isMovingToEnd && parentMatches) {
return true;
}
return parentMatches && parentElement.firstChild.isSameNode(char);
}) : -1;
if (selectorIndex < 0) {
selectorIndex = isMovingToEnd ? 0 : queueItems.length - 1;
}
let offset = isMovingToEnd ? 0 : 1;
return selectorIndex - cursorPosition + offset;
};
const destroyTimeouts = (timeouts) => {
timeouts.forEach(clearTimeout);
return [];
};
const duplicate = (value, times) => new Array(times).fill(value);
let beforePaint = (cb) => {
return new Promise((resolve) => {
requestAnimationFrame(async () => {
resolve(await cb());
});
});
};
let getAnimationFromElement = (element) => {
return element?.getAnimations().find((animation) => {
return animation.id === element.dataset.tiAnimationId;
});
};
let setCursorAnimation = ({
cursor,
frames,
options
}) => {
let animation = cursor.animate(frames, options);
animation.pause();
animation.id = cursor.dataset.tiAnimationId;
beforePaint(() => {
beforePaint(() => {
animation.play();
});
});
return animation;
};
let rebuildCursorAnimation = ({
cursor,
options,
cursorOptions
}) => {
if (!cursor || !cursorOptions) return;
let animation = getAnimationFromElement(cursor);
let oldCurrentTime;
if (animation) {
options.delay = animation.effect.getComputedTiming().delay;
oldCurrentTime = animation.currentTime;
animation.cancel();
}
let newAnimation = setCursorAnimation({
cursor,
frames: cursorOptions.animation.frames,
options
});
if (oldCurrentTime) {
newAnimation.currentTime = oldCurrentTime;
}
return newAnimation;
};
let execute = (queueItem) => queueItem.func?.call(null);
let fireItem = async ({
index,
queueItems,
wait,
cursor,
cursorOptions
}) => {
let queueItem = queueItems[index][1];
let instantQueue = [];
let tempIndex = index;
let futureItem = queueItem;
let shouldBeGrouped = () => futureItem && !futureItem.delay;
let shouldPauseCursor = queueItem.shouldPauseCursor() && cursorOptions.autoPause;
while (shouldBeGrouped()) {
instantQueue.push(futureItem);
shouldBeGrouped() && tempIndex++;
futureItem = queueItems[tempIndex] ? queueItems[tempIndex][1] : null;
}
if (instantQueue.length) {
await beforePaint(async () => {
for (let q of instantQueue) {
await execute(q);
}
});
return tempIndex - 1;
}
let animation = getAnimationFromElement(cursor);
let options;
if (animation) {
options = {
...animation.effect.getComputedTiming(),
delay: shouldPauseCursor ? cursorOptions.autoPauseDelay : 0
};
}
await wait(async () => {
if (animation && shouldPauseCursor) {
animation.cancel();
}
await beforePaint(() => {
execute(queueItem);
});
}, queueItem.delay);
await rebuildCursorAnimation({
cursor,
options,
cursorOptions
});
return index;
};
const fireWhenVisible = (element, func) => {
let observer = new IntersectionObserver(
(entries, observer2) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
func();
observer2.unobserve(element);
}
});
},
{ threshold: 1 }
);
observer.observe(element);
};
const generateHash = () => Math.random().toString().substring(2, 9);
const isInput = (el) => {
return "value" in el;
};
let getAllChars = (element) => {
if (isInput(element)) {
return toArray(element.value);
}
return walkElementNodes(element, true).filter(
(c) => !(c.childNodes.length > 0)
);
};
let handleFunctionalArg = (arg) => {
return typeof arg === "function" ? arg() : arg;
};
let select = (selector, element = document, all = false) => {
return element[`querySelector${all ? "All" : ""}`](selector);
};
let isBodyElement = (node) => /body/i.test(node?.tagName);
let insertIntoElement = (originalTarget, character) => {
if (isInput(originalTarget)) {
originalTarget.value = `${originalTarget.value}${character.textContent}`;
return;
}
character.innerHTML = "";
let target = isBodyElement(character.originalParent) ? originalTarget : (
// If we add one-off fresh elements, there will be no
// "originalParent", so always fall back to the default target.
character.originalParent || originalTarget
);
let cursorNode = select("." + CURSOR_CLASS, target) || null;
if (cursorNode && cursorNode.parentElement !== target) {
target = cursorNode.parentElement;
}
target.insertBefore(character, cursorNode);
};
const isNonVoidElement = (el) => /<(.+)>(.*?)<\/(.+)>/.test(el.outerHTML);
const merge = (originalObj, newObj) => Object.assign({}, originalObj, newObj);
let processCursorOptions = (cursorOptions) => {
if (typeof cursorOptions === "object") {
let newOptions = {};
let { frames: defaultFrames, options: defaultOptions } = DEFAULT_OPTIONS.cursor.animation;
newOptions.animation = cursorOptions.animation || {};
newOptions.animation.frames = cursorOptions.animation?.frames || defaultFrames;
newOptions.animation.options = merge(
defaultOptions,
cursorOptions.animation?.options || {}
);
newOptions.autoPause = cursorOptions.autoPause ?? DEFAULT_OPTIONS.cursor.autoPause;
newOptions.autoPauseDelay = cursorOptions.autoPauseDelay || DEFAULT_OPTIONS.cursor.autoPauseDelay;
return newOptions;
}
if (cursorOptions === true) {
return DEFAULT_OPTIONS.cursor;
}
return cursorOptions;
};
const removeNode = (node, rootElement) => {
if (!node) return;
let nodeParent = node.parentNode;
let nodeToRemove = nodeParent.childNodes.length > 1 || nodeParent.isSameNode(rootElement) ? (
// This parent still needs to exist.
node
) : (
// There's nothing else in there, so just delete the entire thing.
// By doing this, we clean up markup as we go along.
nodeParent
);
nodeToRemove.remove();
};
const repositionCursor = (element, allChars, newCursorPosition) => {
let nodeToInsertBefore = allChars[newCursorPosition - 1];
let cursor = select(`.${CURSOR_CLASS}`, element);
element = nodeToInsertBefore?.parentNode || element;
element.insertBefore(cursor, nodeToInsertBefore || null);
};
function selectorToElement(thing) {
return typeof thing === "string" ? select(thing) : thing;
}
let cursorFontStyles = {
"font-family": "",
"font-weight": "",
"font-size": "",
"font-style": "",
"line-height": "",
color: "",
transform: "translateX(-.125em)"
};
let setCursorStyles = (id, element) => {
let rootSelector = `[${DATA_ATTRIBUTE}='${id}']`;
let cursorSelector = `${rootSelector} .${CURSOR_CLASS}`;
let computedStyles = getComputedStyle(element);
let customProperties = Object.entries(cursorFontStyles).reduce(
(accumulator, [item, value]) => {
return `${accumulator} ${item}: var(--ti-cursor-${item}, ${value || computedStyles[item]});`;
},
""
);
appendStyleBlock(
`${cursorSelector} { display: inline-block; width: 0; ${customProperties} }`,
id
);
};
function splitOnBreak(str) {
return str.replace(/<!--(.+?)-->/g, "").trim().split(/<br(?:\s*?)(?:\/)?>/);
}
let updateCursorPosition = (steps, cursorPosition, printedCharacters) => {
return Math.min(
Math.max(cursorPosition + steps, 0),
printedCharacters.length
);
};
let wait = (callback, delay, timeouts) => {
return new Promise((resolve) => {
let cb = async () => {
await callback();
resolve();
};
timeouts.push(setTimeout(cb, delay || 0));
});
};
class TypeIt {
element;
timeouts;
cursorPosition;
predictedCursorPosition;
statuses = {
started: false,
completed: false,
frozen: false,
destroyed: false,
firing: false
};
opts;
id;
queue;
cursor;
flushCallback = null;
unfreeze = () => {
};
constructor(element, options = {}) {
this.opts = merge(DEFAULT_OPTIONS, options);
this.element = selectorToElement(element);
this.timeouts = [];
this.cursorPosition = 0;
this.unfreeze = () => {
};
this.predictedCursorPosition = null;
this.statuses = merge({}, DEFAULT_STATUSES);
this.id = generateHash();
this.queue = Queue([{ delay: this.opts.startDelay }]);
this.#buildOptions(options);
this.cursor = this.#setUpCursor();
this.element.dataset.typeitId = this.id;
appendStyleBlock(PLACEHOLDER_CSS);
if (this.opts.strings.length) {
this.#generateQueue();
}
}
/**
* Can only be called once.
*/
go() {
if (this.statuses.started) {
return this;
}
this.#attachCursor();
if (!this.opts.waitUntilVisible) {
this.#fire();
return this;
}
fireWhenVisible(this.element, this.#fire.bind(this));
return this;
}
destroy(shouldRemoveCursor = true) {
this.timeouts = destroyTimeouts(this.timeouts);
handleFunctionalArg(shouldRemoveCursor) && this.cursor && this.#removeNode(this.cursor);
this.statuses.destroyed = true;
}
reset(rebuild) {
!this.is("destroyed") && this.destroy();
if (rebuild) {
this.queue.wipe();
rebuild(this);
} else {
this.queue.reset();
}
this.cursorPosition = 0;
for (let property in this.statuses) {
this.statuses[property] = false;
}
this.element[this.#elementIsInput() ? "value" : "innerHTML"] = "";
return this;
}
is = function(key) {
return this.statuses[key];
};
type(string, actionOpts = {}) {
string = handleFunctionalArg(string);
let { instant } = actionOpts;
let bookEndQueueItems = this.#generateTemporaryOptionQueueItems(actionOpts);
let chars = maybeChunkStringAsHtml(string, this.opts.html);
let charsAsQueueItems = chars.map((char) => {
return {
func: () => this.#type(char),
char,
delay: instant || isNonVoidElement(char) ? 0 : this.#getPace(),
typeable: char.nodeType === Node.TEXT_NODE
};
});
let itemsToQueue = [
bookEndQueueItems[0],
{ func: async () => await this.opts.beforeString(string, this) },
...charsAsQueueItems,
{ func: async () => await this.opts.afterString(string, this) },
bookEndQueueItems[1]
];
return this.#queueAndReturn(itemsToQueue, actionOpts);
}
break(actionOpts = {}) {
return this.#queueAndReturn(
{
func: () => this.#type(createElement("BR")),
typeable: true
},
actionOpts
);
}
move(movementArg, actionOpts = {}) {
movementArg = handleFunctionalArg(movementArg);
let bookEndQueueItems = this.#generateTemporaryOptionQueueItems(actionOpts);
let { instant, to } = actionOpts;
let numberOfSteps = countStepsToSelector({
queueItems: this.queue.getTypeable(),
selector: movementArg === null ? "" : movementArg,
to,
cursorPosition: this.#derivedCursorPosition
});
let directionalStep = numberOfSteps < 0 ? -1 : 1;
this.predictedCursorPosition = this.#derivedCursorPosition + numberOfSteps;
return this.#queueAndReturn(
[
bookEndQueueItems[0],
...duplicate(
{
func: () => this.#move(directionalStep),
delay: instant ? 0 : this.#getPace(),
cursorable: true
},
Math.abs(numberOfSteps)
),
bookEndQueueItems[1]
],
actionOpts
);
}
exec(func, actionOpts = {}) {
let bookEndQueueItems = this.#generateTemporaryOptionQueueItems(actionOpts);
return this.#queueAndReturn(
[bookEndQueueItems[0], { func: () => func(this) }, bookEndQueueItems[1]],
actionOpts
);
}
options(opts, actionOpts = {}) {
opts = handleFunctionalArg(opts);
this.#updateOptions(opts);
return this.#queueAndReturn({}, actionOpts);
}
pause(milliseconds, actionOpts = {}) {
return this.#queueAndReturn(
{ delay: handleFunctionalArg(milliseconds) },
actionOpts
);
}
delete(numCharacters = null, actionOpts = {}) {
numCharacters = handleFunctionalArg(numCharacters);
let bookEndQueueItems = this.#generateTemporaryOptionQueueItems(actionOpts);
let num = numCharacters;
let { instant, to } = actionOpts;
let typeableQueueItems = this.queue.getTypeable();
let rounds = (() => {
if (num === null) {
return typeableQueueItems.length;
}
if (isNumber(num)) {
return num;
}
return countStepsToSelector({
queueItems: typeableQueueItems,
selector: num,
cursorPosition: this.#derivedCursorPosition,
to
});
})();
return this.#queueAndReturn(
[
bookEndQueueItems[0],
...duplicate(
{
func: this.#delete.bind(this),
delay: instant ? 0 : this.#getPace(1),
deletable: true
},
rounds
),
bookEndQueueItems[1]
],
actionOpts
);
}
freeze() {
this.statuses.frozen = true;
}
/**
* Like `.go()`, but more... "off the grid."
*
* - won't trigger `afterComplete` callback
* - items won't be replayed after `.reset()`
*
* When called, all non-done items will be "flushed" --
* that is, executed, but not remembered.
*/
flush(cb = null) {
this.flushCallback = cb || this.flushCallback;
if (this.statuses.firing) {
return this;
}
this.#attachCursor();
this.#fire(false).then(() => {
if (this.queue.getPendingQueueItems().length > 0) {
return this.flush();
}
this.flushCallback();
this.flushCallback = null;
});
return this;
}
getQueue() {
return this.queue;
}
getOptions() {
return this.opts;
}
updateOptions(options) {
return this.#updateOptions(options);
}
getElement() {
return this.element;
}
empty(actionOpts = {}) {
return this.#queueAndReturn({ func: this.#empty.bind(this) }, actionOpts);
}
async #empty() {
if (this.#elementIsInput()) {
this.element.value = "";
return;
}
this.#allChars.forEach(this.#removeNode.bind(this));
return;
}
/**
* Execute items in the queue.
*
* @param remember If false, each queue item will be destroyed once executed.
* @returns
*/
async #fire(remember = true) {
this.statuses.started = true;
this.statuses.firing = true;
let cleanUp = (qKey) => {
this.queue.done(qKey, !remember);
};
try {
let queueItems = [...this.queue.getQueue()];
for (let index = 0; index < queueItems.length; index++) {
let [queueKey, queueItem] = queueItems[index];
if (queueItem.done) continue;
if (!queueItem.deletable || queueItem.deletable && this.#allChars.length) {
let newIndex = await this.#fireItemWithContext(index, queueItems);
cleanUpSkipped({
index,
newIndex,
queueItems,
cleanUp
});
index = newIndex;
}
cleanUp(queueKey);
}
if (!remember) {
this.statuses.firing = false;
return this;
}
this.statuses.completed = true;
this.statuses.firing = false;
await this.opts.afterComplete(this);
if (!this.opts.loop) {
throw "";
}
let delay = this.opts.loopDelay;
this.#wait(async () => {
await this.#prepLoop(delay[0]);
this.#fire();
}, delay[1]);
} catch (e) {
}
this.statuses.firing = false;
return this;
}
async #move(step) {
this.cursorPosition = updateCursorPosition(
step,
this.cursorPosition,
this.#allChars
);
repositionCursor(this.element, this.#allChars, this.cursorPosition);
}
/**
* 1. Reset queue.
* 2. Reset initial pause.
*/
async #prepLoop(delay) {
let derivedCursorPosition = this.#derivedCursorPosition;
derivedCursorPosition && await this.#move({ value: derivedCursorPosition });
let queueItems = this.#allChars.map((c) => {
return [
Symbol(),
{
func: this.#delete.bind(this),
delay: this.#getPace(1),
deletable: true,
shouldPauseCursor: () => true
}
];
});
for (let index = 0; index < queueItems.length; index++) {
await this.#fireItemWithContext(index, queueItems);
}
this.queue.reset();
this.queue.set(0, { delay });
}
#fireItemWithContext(index, queueItems) {
return fireItem({
index,
queueItems,
wait: this.#wait.bind(this),
cursor: this.cursor,
cursorOptions: this.opts.cursor
});
}
async #wait(callback, delay, silent = false) {
if (this.statuses.frozen) {
await new Promise((resolve) => {
this.unfreeze = () => {
this.statuses.frozen = false;
resolve();
};
});
}
silent || await this.opts.beforeStep(this);
await wait(callback, delay, this.timeouts);
silent || await this.opts.afterStep(this);
}
/**
* Attach it to the DOM so, along with the required CSS transition.
*/
async #attachCursor() {
!this.#elementIsInput() && this.cursor && this.element.appendChild(this.cursor);
if (this.#shouldRenderCursor) {
setCursorStyles(this.id, this.element);
this.cursor.dataset.tiAnimationId = this.id;
let { animation } = this.opts.cursor;
let { frames, options } = animation;
setCursorAnimation({
frames,
cursor: this.cursor,
options: {
duration: this.opts.cursorSpeed,
...options
}
});
}
}
#elementIsInput() {
return isInput(this.element);
}
#queueAndReturn(steps, opts) {
this.queue.add(steps);
this.#maybeAppendPause(opts);
return this;
}
#maybeAppendPause(opts = {}) {
let delay = opts.delay;
delay && this.queue.add({ delay });
}
#generateTemporaryOptionQueueItems(newOptions = {}) {
return [
{ func: () => this.#updateOptions(newOptions) },
{ func: () => this.#updateOptions(this.opts) }
];
}
async #updateOptions(opts) {
this.opts = merge(this.opts, opts);
}
/**
* Based on provided strings, generate a TypeIt queue
* to be fired for each character in the string.
*/
#generateQueue() {
let strings = this.opts.strings.filter((string) => !!string);
strings.forEach((string, index) => {
this.type(string);
if (index + 1 === strings.length) {
return;
}
let splitItems = this.opts.breakLines ? [{ func: () => this.#type(createElement("BR")), typeable: true }] : duplicate(
{
func: this.#delete.bind(this),
delay: this.#getPace(1)
},
this.queue.getTypeable().length
);
this.#addSplitPause(splitItems);
});
}
#buildOptions = (options) => {
this.opts.cursor = processCursorOptions(
options.cursor ?? DEFAULT_OPTIONS.cursor
);
this.opts.strings = this.#prependHardcodedStrings(
asArray(this.opts.strings)
);
this.opts = merge(this.opts, {
html: !this.#isInput && this.opts.html,
nextStringDelay: calculateDelay(this.opts.nextStringDelay),
loopDelay: calculateDelay(this.opts.loopDelay)
});
};
#prependHardcodedStrings(strings) {
let existingMarkup = this.element.innerHTML;
if (!existingMarkup) {
return strings;
}
this.element.innerHTML = "";
if (this.opts.startDelete) {
this.element.innerHTML = existingMarkup;
expandTextNodes(this.element);
this.#addSplitPause(
duplicate(
{
func: this.#delete.bind(this),
delay: this.#getPace(1),
deletable: true
},
this.#allChars.length
)
);
return strings;
}
return splitOnBreak(existingMarkup).concat(strings);
}
/**
* Provided it's a non-form element and the options is provided,
* set up the cursor element for the animation.
*/
#setUpCursor() {
if (this.#isInput) {
return null;
}
let cursor = createElement("span");
cursor.className = CURSOR_CLASS;
if (!this.#shouldRenderCursor) {
cursor.style.visibility = "hidden";
return cursor;
}
cursor.innerHTML = getParsedBody(this.opts.cursorChar).innerHTML;
return cursor;
}
#addSplitPause(items) {
let delay = this.opts.nextStringDelay;
this.queue.add([{ delay: delay[0] }, ...items, { delay: delay[1] }]);
}
#type(char) {
insertIntoElement(this.element, char);
}
#delete() {
if (!this.#allChars.length) return;
if (this.#isInput) {
this.element.value = this.element.value.slice(0, -1);
} else {
this.#removeNode(this.#allChars[this.cursorPosition]);
}
}
#removeNode(node) {
removeNode(node, this.element);
}
#getPace(index = 0) {
return calculatePace(this.opts)[index];
}
get #derivedCursorPosition() {
return this.predictedCursorPosition ?? this.cursorPosition;
}
get #isInput() {
return isInput(this.element);
}
get #shouldRenderCursor() {
return !!this.opts.cursor && !this.#isInput;
}
get #allChars() {
return getAllChars(this.element);
}
}
export { TypeIt as default };