Skip to content
Snippets Groups Projects
termynal.js 7.26 KiB
Newer Older
/**
 * 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))
 }