264 lines
9.3 KiB
JavaScript
Executable File
264 lines
9.3 KiB
JavaScript
Executable File
/**
|
||
* termynal.js
|
||
* A lightweight, modern and extensible animated terminal window, using
|
||
* async/await.
|
||
*
|
||
* @author Ines Montani <ines@ines.io>
|
||
* @version 0.0.1
|
||
* @license MIT
|
||
*/
|
||
|
||
'use strict';
|
||
|
||
/** Generate a terminal widget. */
|
||
class Termynal {
|
||
/**
|
||
* Construct the widget's settings.
|
||
* @param {(string|Node)=} container - Query selector or container element.
|
||
* @param {Object=} options - Custom settings.
|
||
* @param {string} options.prefix - Prefix to use for data attributes.
|
||
* @param {number} options.startDelay - Delay before animation, in ms.
|
||
* @param {number} options.typeDelay - Delay between each typed character, in ms.
|
||
* @param {number} options.lineDelay - Delay between each line, in ms.
|
||
* @param {number} options.progressLength - Number of characters displayed as progress bar.
|
||
* @param {string} options.progressChar – Character to use for progress bar, defaults to █.
|
||
* @param {number} options.progressPercent - Max percent of progress.
|
||
* @param {string} options.cursor – Character to use for cursor, defaults to ▋.
|
||
* @param {Object[]} lineData - Dynamically loaded line data objects.
|
||
* @param {boolean} options.noInit - Don't initialise the animation.
|
||
*/
|
||
constructor(container = '#termynal', options = {}) {
|
||
this.container = (typeof container === 'string') ? document.querySelector(container) : container;
|
||
this.pfx = `data-${options.prefix || 'ty'}`;
|
||
this.originalStartDelay = this.startDelay = options.startDelay
|
||
|| parseFloat(this.container.getAttribute(`${this.pfx}-startDelay`)) || 600;
|
||
this.originalTypeDelay = this.typeDelay = options.typeDelay
|
||
|| parseFloat(this.container.getAttribute(`${this.pfx}-typeDelay`)) || 90;
|
||
this.originalLineDelay = this.lineDelay = options.lineDelay
|
||
|| parseFloat(this.container.getAttribute(`${this.pfx}-lineDelay`)) || 1500;
|
||
this.progressLength = options.progressLength
|
||
|| parseFloat(this.container.getAttribute(`${this.pfx}-progressLength`)) || 40;
|
||
this.progressChar = options.progressChar
|
||
|| this.container.getAttribute(`${this.pfx}-progressChar`) || '█';
|
||
this.progressPercent = options.progressPercent
|
||
|| parseFloat(this.container.getAttribute(`${this.pfx}-progressPercent`)) || 100;
|
||
this.cursor = options.cursor
|
||
|| this.container.getAttribute(`${this.pfx}-cursor`) || '▋';
|
||
this.lineData = this.lineDataToElements(options.lineData || []);
|
||
this.loadLines()
|
||
if (!options.noInit) this.init()
|
||
}
|
||
|
||
loadLines() {
|
||
// Load all the lines and create the container so that the size is fixed
|
||
// Otherwise it would be changing and the user viewport would be constantly
|
||
// moving as she/he scrolls
|
||
const finish = this.generateFinish()
|
||
finish.style.visibility = 'hidden'
|
||
this.container.appendChild(finish)
|
||
// Appends dynamically loaded lines to existing line elements.
|
||
this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(this.lineData);
|
||
for (let line of this.lines) {
|
||
line.style.visibility = 'hidden'
|
||
this.container.appendChild(line)
|
||
}
|
||
const restart = this.generateRestart()
|
||
restart.style.visibility = 'hidden'
|
||
this.container.appendChild(restart)
|
||
this.container.setAttribute('data-termynal', '');
|
||
}
|
||
|
||
/**
|
||
* Initialise the widget, get lines, clear container and start animation.
|
||
*/
|
||
init() {
|
||
/**
|
||
* Calculates width and height of Termynal container.
|
||
* If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS.
|
||
*/
|
||
const containerStyle = getComputedStyle(this.container);
|
||
this.container.style.width = containerStyle.width !== '0px' ?
|
||
containerStyle.width : undefined;
|
||
this.container.style.minHeight = containerStyle.height !== '0px' ?
|
||
containerStyle.height : undefined;
|
||
|
||
this.container.setAttribute('data-termynal', '');
|
||
this.container.innerHTML = '';
|
||
for (let line of this.lines) {
|
||
line.style.visibility = 'visible'
|
||
}
|
||
this.start();
|
||
}
|
||
|
||
/**
|
||
* Start the animation and rener the lines depending on their data attributes.
|
||
*/
|
||
async start() {
|
||
this.addFinish()
|
||
await this._wait(this.startDelay);
|
||
|
||
for (let line of this.lines) {
|
||
const type = line.getAttribute(this.pfx);
|
||
const delay = line.getAttribute(`${this.pfx}-delay`) || this.lineDelay;
|
||
|
||
if (type == 'input') {
|
||
line.setAttribute(`${this.pfx}-cursor`, this.cursor);
|
||
await this.type(line);
|
||
await this._wait(delay);
|
||
}
|
||
|
||
else if (type == 'progress') {
|
||
await this.progress(line);
|
||
await this._wait(delay);
|
||
}
|
||
|
||
else {
|
||
this.container.appendChild(line);
|
||
await this._wait(delay);
|
||
}
|
||
|
||
line.removeAttribute(`${this.pfx}-cursor`);
|
||
}
|
||
this.addRestart()
|
||
this.finishElement.style.visibility = 'hidden'
|
||
this.lineDelay = this.originalLineDelay
|
||
this.typeDelay = this.originalTypeDelay
|
||
this.startDelay = this.originalStartDelay
|
||
}
|
||
|
||
generateRestart() {
|
||
const restart = document.createElement('a')
|
||
restart.onclick = (e) => {
|
||
e.preventDefault()
|
||
this.container.innerHTML = ''
|
||
this.init()
|
||
}
|
||
restart.href = '#'
|
||
restart.setAttribute('data-terminal-control', '')
|
||
restart.innerHTML = "restart ↻"
|
||
return restart
|
||
}
|
||
|
||
generateFinish() {
|
||
const finish = document.createElement('a')
|
||
finish.onclick = (e) => {
|
||
e.preventDefault()
|
||
this.lineDelay = 0
|
||
this.typeDelay = 0
|
||
this.startDelay = 0
|
||
}
|
||
finish.href = '#'
|
||
finish.setAttribute('data-terminal-control', '')
|
||
this.finishElement = finish
|
||
return finish
|
||
}
|
||
|
||
addRestart() {
|
||
const restart = this.generateRestart()
|
||
this.container.appendChild(restart)
|
||
}
|
||
|
||
addFinish() {
|
||
const finish = this.generateFinish()
|
||
this.container.appendChild(finish)
|
||
}
|
||
|
||
/**
|
||
* Animate a typed line.
|
||
* @param {Node} line - The line element to render.
|
||
*/
|
||
async type(line) {
|
||
const chars = [...line.textContent];
|
||
line.textContent = '';
|
||
this.container.appendChild(line);
|
||
|
||
for (let char of chars) {
|
||
const delay = line.getAttribute(`${this.pfx}-typeDelay`) || this.typeDelay;
|
||
await this._wait(delay);
|
||
line.textContent += char;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Animate a progress bar.
|
||
* @param {Node} line - The line element to render.
|
||
*/
|
||
async progress(line) {
|
||
const progressLength = line.getAttribute(`${this.pfx}-progressLength`)
|
||
|| this.progressLength;
|
||
const progressChar = line.getAttribute(`${this.pfx}-progressChar`)
|
||
|| this.progressChar;
|
||
const chars = progressChar.repeat(progressLength);
|
||
const progressPercent = line.getAttribute(`${this.pfx}-progressPercent`)
|
||
|| this.progressPercent;
|
||
line.textContent = '';
|
||
this.container.appendChild(line);
|
||
|
||
for (let i = 1; i < chars.length + 1; i++) {
|
||
await this._wait(this.typeDelay);
|
||
const percent = Math.round(i / chars.length * 100);
|
||
line.textContent = `${chars.slice(0, i)} ${percent}%`;
|
||
if (percent>progressPercent) {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Helper function for animation delays, called with `await`.
|
||
* @param {number} time - Timeout, in ms.
|
||
*/
|
||
_wait(time) {
|
||
return new Promise(resolve => setTimeout(resolve, time));
|
||
}
|
||
|
||
/**
|
||
* Converts line data objects into line elements.
|
||
*
|
||
* @param {Object[]} lineData - Dynamically loaded lines.
|
||
* @param {Object} line - Line data object.
|
||
* @returns {Element[]} - Array of line elements.
|
||
*/
|
||
lineDataToElements(lineData) {
|
||
return lineData.map(line => {
|
||
let div = document.createElement('div');
|
||
div.innerHTML = `<span ${this._attributes(line)}>${line.value || ''}</span>`;
|
||
|
||
return div.firstElementChild;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Helper function for generating attributes string.
|
||
*
|
||
* @param {Object} line - Line data object.
|
||
* @returns {string} - String of attributes.
|
||
*/
|
||
_attributes(line) {
|
||
let attrs = '';
|
||
for (let prop in line) {
|
||
// Custom add class
|
||
if (prop === 'class') {
|
||
attrs += ` class=${line[prop]} `
|
||
continue
|
||
}
|
||
if (prop === 'type') {
|
||
attrs += `${this.pfx}="${line[prop]}" `
|
||
} else if (prop !== 'value') {
|
||
attrs += `${this.pfx}-${prop}="${line[prop]}" `
|
||
}
|
||
}
|
||
|
||
return attrs;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* HTML API: If current script has container(s) specified, initialise Termynal.
|
||
*/
|
||
if (document.currentScript.hasAttribute('data-termynal-container')) {
|
||
const containers = document.currentScript.getAttribute('data-termynal-container');
|
||
containers.split('|')
|
||
.forEach(container => new Termynal(container))
|
||
}
|