| /** | |
| * 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.startDelay = options.startDelay | |
| || parseFloat(this.container.getAttribute(`${this.pfx}-startDelay`)) || 600; | |
| this.typeDelay = options.typeDelay | |
| || parseFloat(this.container.getAttribute(`${this.pfx}-typeDelay`)) || 90; | |
| 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 || []); | |
| if (!options.noInit) this.init() | |
| } | |
| /** | |
| * Initialise the widget, get lines, clear container and start animation. | |
| */ | |
| init() { | |
| // Appends dynamically loaded lines to existing line elements. | |
| this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(this.lineData); | |
| /** | |
| * 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 = ''; | |
| this.start(); | |
| } | |
| /** | |
| * Start the animation and rener the lines depending on their data attributes. | |
| */ | |
| async start() { | |
| 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`); | |
| } | |
| } | |
| /** | |
| * Animate a typed line. | |
| * @param {Node} line - The line element to render. | |
| */ | |
| async type(line) { | |
| const chars = [...line.textContent]; | |
| const delay = line.getAttribute(`${this.pfx}-typeDelay`) || this.typeDelay; | |
| line.textContent = ''; | |
| this.container.appendChild(line); | |
| for (let char of chars) { | |
| 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) { | |
| attrs += this.pfx; | |
| if (prop === 'type') { | |
| attrs += `="${line[prop]}" ` | |
| } else if (prop !== 'value') { | |
| attrs += `-${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)) | |
| } |