src/typed.js
import { initializer } from './initializer.js';
import { htmlParser } from './html-parser.js';
/**
* Welcome to Typed.js!
* @param {string} elementId HTML element ID _OR_ HTML element
* @param {object} options options object
* @returns {object} a new Typed object
*/
export default class Typed {
constructor(elementId, options) {
// Initialize it up
initializer.load(this, options, elementId);
// All systems go!
this.begin();
}
/**
* Toggle start() and stop() of the Typed instance
* @public
*/
toggle() {
this.pause.status ? this.start() : this.stop();
}
/**
* Stop typing / backspacing and enable cursor blinking
* @public
*/
stop() {
if (this.typingComplete) return;
if (this.pause.status) return;
this.toggleBlinking(true);
this.pause.status = true;
this.options.onStop(this.arrayPos, this);
}
/**
* Start typing / backspacing after being stopped
* @public
*/
start() {
if (this.typingComplete) return;
if (!this.pause.status) return;
this.pause.status = false;
if (this.pause.typewrite) {
this.typewrite(this.pause.curString, this.pause.curStrPos);
} else {
this.backspace(this.pause.curString, this.pause.curStrPos);
}
this.options.onStart(this.arrayPos, this);
}
/**
* Destroy this instance of Typed
* @public
*/
destroy() {
this.reset(false);
this.options.onDestroy(this);
}
/**
* Reset Typed and optionally restarts
* @param {boolean} restart
* @public
*/
reset(restart = true) {
clearInterval(this.timeout);
this.replaceText('');
if (this.cursor && this.cursor.parentNode) {
this.cursor.parentNode.removeChild(this.cursor);
this.cursor = null;
}
this.strPos = 0;
this.arrayPos = 0;
this.curLoop = 0;
if (restart) {
this.insertCursor();
this.options.onReset(this);
this.begin();
}
}
/**
* Begins the typing animation
* @private
*/
begin() {
this.options.onBegin(this);
this.typingComplete = false;
this.shuffleStringsIfNeeded(this);
this.insertCursor();
if (this.bindInputFocusEvents) this.bindFocusEvents();
this.timeout = setTimeout(() => {
// If the strPos is 0, we're starting from the beginning of a string
// else, we're starting with a previous string that needs to be backspaced first
if (this.strPos === 0) {
this.typewrite(this.strings[this.sequence[this.arrayPos]], this.strPos);
} else {
this.backspace(this.strings[this.sequence[this.arrayPos]], this.strPos);
}
}, this.startDelay);
}
/**
* Called for each character typed
* @param {string} curString the current string in the strings array
* @param {number} curStrPos the current position in the curString
* @private
*/
typewrite(curString, curStrPos) {
if (this.fadeOut && this.el.classList.contains(this.fadeOutClass)) {
this.el.classList.remove(this.fadeOutClass);
if (this.cursor) this.cursor.classList.remove(this.fadeOutClass);
}
const humanize = this.humanizer(this.typeSpeed);
let numChars = 1;
if (this.pause.status === true) {
this.setPauseStatus(curString, curStrPos, true);
return;
}
// contain typing function in a timeout humanize'd delay
this.timeout = setTimeout(() => {
// skip over any HTML chars
curStrPos = htmlParser.typeHtmlChars(curString, curStrPos, this);
let pauseTime = 0;
let substr = curString.substring(curStrPos);
// check for an escape character before a pause value
// format: \^\d+ .. eg: ^1000 .. should be able to print the ^ too using ^^
// single ^ are removed from string
if (substr.charAt(0) === '^') {
if (/^\^\d+/.test(substr)) {
let skip = 1; // skip at least 1
substr = /\d+/.exec(substr)[0];
skip += substr.length;
pauseTime = parseInt(substr);
this.temporaryPause = true;
this.options.onTypingPaused(this.arrayPos, this);
// strip out the escape character and pause value so they're not printed
curString =
curString.substring(0, curStrPos) +
curString.substring(curStrPos + skip);
this.toggleBlinking(true);
}
}
// check for skip characters formatted as
// "this is a `string to print NOW` ..."
if (substr.charAt(0) === '`') {
while (curString.substring(curStrPos + numChars).charAt(0) !== '`') {
numChars++;
if (curStrPos + numChars > curString.length) break;
}
// strip out the escape characters and append all the string in between
const stringBeforeSkip = curString.substring(0, curStrPos);
const stringSkipped = curString.substring(
stringBeforeSkip.length + 1,
curStrPos + numChars
);
const stringAfterSkip = curString.substring(curStrPos + numChars + 1);
curString = stringBeforeSkip + stringSkipped + stringAfterSkip;
numChars--;
}
// timeout for any pause after a character
this.timeout = setTimeout(() => {
// Accounts for blinking while paused
this.toggleBlinking(false);
// We're done with this sentence!
if (curStrPos >= curString.length) {
this.doneTyping(curString, curStrPos);
} else {
this.keepTyping(curString, curStrPos, numChars);
}
// end of character pause
if (this.temporaryPause) {
this.temporaryPause = false;
this.options.onTypingResumed(this.arrayPos, this);
}
}, pauseTime);
// humanized value for typing
}, humanize);
}
/**
* Continue to the next string & begin typing
* @param {string} curString the current string in the strings array
* @param {number} curStrPos the current position in the curString
* @private
*/
keepTyping(curString, curStrPos, numChars) {
// call before functions if applicable
if (curStrPos === 0) {
this.toggleBlinking(false);
this.options.preStringTyped(this.arrayPos, this);
}
// start typing each new char into existing string
// curString: arg, this.el.html: original text inside element
curStrPos += numChars;
const nextString = curString.substring(0, curStrPos);
this.replaceText(nextString);
// loop the function
this.typewrite(curString, curStrPos);
}
/**
* We're done typing the current string
* @param {string} curString the current string in the strings array
* @param {number} curStrPos the current position in the curString
* @private
*/
doneTyping(curString, curStrPos) {
// fires callback function
this.options.onStringTyped(this.arrayPos, this);
this.toggleBlinking(true);
// is this the final string
if (this.arrayPos === this.strings.length - 1) {
// callback that occurs on the last typed string
this.complete();
// quit if we wont loop back
if (this.loop === false || this.curLoop === this.loopCount) {
return;
}
}
this.timeout = setTimeout(() => {
this.backspace(curString, curStrPos);
}, this.backDelay);
}
/**
* Backspaces 1 character at a time
* @param {string} curString the current string in the strings array
* @param {number} curStrPos the current position in the curString
* @private
*/
backspace(curString, curStrPos) {
if (this.pause.status === true) {
this.setPauseStatus(curString, curStrPos, false);
return;
}
if (this.fadeOut) return this.initFadeOut();
this.toggleBlinking(false);
const humanize = this.humanizer(this.backSpeed);
this.timeout = setTimeout(() => {
curStrPos = htmlParser.backSpaceHtmlChars(curString, curStrPos, this);
// replace text with base text + typed characters
const curStringAtPosition = curString.substring(0, curStrPos);
this.replaceText(curStringAtPosition);
// if smartBack is enabled
if (this.smartBackspace) {
// the remaining part of the current string is equal of the same part of the new string
let nextString = this.strings[this.arrayPos + 1];
if (
nextString &&
curStringAtPosition === nextString.substring(0, curStrPos)
) {
this.stopNum = curStrPos;
} else {
this.stopNum = 0;
}
}
// if the number (id of character in current string) is
// less than the stop number, keep going
if (curStrPos > this.stopNum) {
// subtract characters one by one
curStrPos--;
// loop the function
this.backspace(curString, curStrPos);
} else if (curStrPos <= this.stopNum) {
// if the stop number has been reached, increase
// array position to next string
this.arrayPos++;
// When looping, begin at the beginning after backspace complete
if (this.arrayPos === this.strings.length) {
this.arrayPos = 0;
this.options.onLastStringBackspaced();
this.shuffleStringsIfNeeded();
this.begin();
} else {
this.typewrite(this.strings[this.sequence[this.arrayPos]], curStrPos);
}
}
// humanized value for typing
}, humanize);
}
/**
* Full animation is complete
* @private
*/
complete() {
this.options.onComplete(this);
if (this.loop) {
this.curLoop++;
} else {
this.typingComplete = true;
}
}
/**
* Has the typing been stopped
* @param {string} curString the current string in the strings array
* @param {number} curStrPos the current position in the curString
* @param {boolean} isTyping
* @private
*/
setPauseStatus(curString, curStrPos, isTyping) {
this.pause.typewrite = isTyping;
this.pause.curString = curString;
this.pause.curStrPos = curStrPos;
}
/**
* Toggle the blinking cursor
* @param {boolean} isBlinking
* @private
*/
toggleBlinking(isBlinking) {
if (!this.cursor) return;
// if in paused state, don't toggle blinking a 2nd time
if (this.pause.status) return;
if (this.cursorBlinking === isBlinking) return;
this.cursorBlinking = isBlinking;
if (isBlinking) {
this.cursor.classList.add('typed-cursor--blink');
} else {
this.cursor.classList.remove('typed-cursor--blink');
}
}
/**
* Speed in MS to type
* @param {number} speed
* @private
*/
humanizer(speed) {
return Math.round((Math.random() * speed) / 2) + speed;
}
/**
* Shuffle the sequence of the strings array
* @private
*/
shuffleStringsIfNeeded() {
if (!this.shuffle) return;
this.sequence = this.sequence.sort(() => Math.random() - 0.5);
}
/**
* Adds a CSS class to fade out current string
* @private
*/
initFadeOut() {
this.el.className += ` ${this.fadeOutClass}`;
if (this.cursor) this.cursor.className += ` ${this.fadeOutClass}`;
return setTimeout(() => {
this.arrayPos++;
this.replaceText('');
// Resets current string if end of loop reached
if (this.strings.length > this.arrayPos) {
this.typewrite(this.strings[this.sequence[this.arrayPos]], 0);
} else {
this.typewrite(this.strings[0], 0);
this.arrayPos = 0;
}
}, this.fadeOutDelay);
}
/**
* Replaces current text in the HTML element
* depending on element type
* @param {string} str
* @private
*/
replaceText(str) {
if (this.attr) {
this.el.setAttribute(this.attr, str);
} else {
if (this.isInput) {
this.el.value = str;
} else if (this.contentType === 'html') {
this.el.innerHTML = str;
} else {
this.el.textContent = str;
}
}
}
/**
* If using input elements, bind focus in order to
* start and stop the animation
* @private
*/
bindFocusEvents() {
if (!this.isInput) return;
this.el.addEventListener('focus', (e) => {
this.stop();
});
this.el.addEventListener('blur', (e) => {
if (this.el.value && this.el.value.length !== 0) {
return;
}
this.start();
});
}
/**
* On init, insert the cursor element
* @private
*/
insertCursor() {
if (!this.showCursor) return;
if (this.cursor) return;
this.cursor = document.createElement('span');
this.cursor.className = 'typed-cursor';
this.cursor.setAttribute('aria-hidden', true);
this.cursor.innerHTML = this.cursorChar;
this.el.parentNode &&
this.el.parentNode.insertBefore(this.cursor, this.el.nextSibling);
}
}