418 lines
12 KiB
JavaScript
418 lines
12 KiB
JavaScript
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.typingComplete = false;
|
|
this.shuffleStringsIfNeeded(this);
|
|
this.insertCursor();
|
|
if (this.bindInputFocusEvents) this.bindFocusEvents();
|
|
this.timeout = setTimeout(() => {
|
|
// Check if there is some text in the element, if yes start by backspacing the default message
|
|
if (!this.currentElContent || this.currentElContent.length === 0) {
|
|
this.typewrite(this.strings[this.sequence[this.arrayPos]], this.strPos);
|
|
} else {
|
|
// Start typing
|
|
this.backspace(this.currentElContent, this.currentElContent.length);
|
|
}
|
|
}, 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.substr(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.substr(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.substr(0, curStrPos);
|
|
this.replaceText(nextString);
|
|
// loop the function
|
|
this.typewrite(curString, curStrPos);
|
|
}
|
|
|
|
/**
|
|
* We're done typing all strings
|
|
* @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, true);
|
|
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.substr(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.substr(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;
|
|
const status = isBlinking ? 'infinite' : 0;
|
|
this.cursor.style.animationIterationCount = status;
|
|
}
|
|
|
|
/**
|
|
* 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.innerHTML = this.cursorChar;
|
|
this.el.parentNode && this.el.parentNode.insertBefore(this.cursor, this.el.nextSibling);
|
|
}
|
|
}
|