Browse Source
* 🎨 Update terminal examples with Termynal * 🍱 Add Termynal scripts and styles from Typer for terminal examplespull/1146/head
committed by
GitHub
22 changed files with 801 additions and 77 deletions
@ -0,0 +1,108 @@ |
|||
/** |
|||
* termynal.js |
|||
* |
|||
* @author Ines Montani <ines@ines.io> |
|||
* @version 0.0.1 |
|||
* @license MIT |
|||
*/ |
|||
|
|||
:root { |
|||
--color-bg: #252a33; |
|||
--color-text: #eee; |
|||
--color-text-subtle: #a2a2a2; |
|||
} |
|||
|
|||
[data-termynal] { |
|||
width: 750px; |
|||
max-width: 100%; |
|||
background: var(--color-bg); |
|||
color: var(--color-text); |
|||
font-size: 18px; |
|||
/* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */ |
|||
font-family: 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; |
|||
border-radius: 4px; |
|||
padding: 75px 45px 35px; |
|||
position: relative; |
|||
-webkit-box-sizing: border-box; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
[data-termynal]:before { |
|||
content: ''; |
|||
position: absolute; |
|||
top: 15px; |
|||
left: 15px; |
|||
display: inline-block; |
|||
width: 15px; |
|||
height: 15px; |
|||
border-radius: 50%; |
|||
/* A little hack to display the window buttons in one pseudo element. */ |
|||
background: #d9515d; |
|||
-webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; |
|||
box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; |
|||
} |
|||
|
|||
[data-termynal]:after { |
|||
content: 'bash'; |
|||
position: absolute; |
|||
color: var(--color-text-subtle); |
|||
top: 5px; |
|||
left: 0; |
|||
width: 100%; |
|||
text-align: center; |
|||
} |
|||
|
|||
a[data-terminal-control] { |
|||
text-align: right; |
|||
display: block; |
|||
color: #aebbff; |
|||
} |
|||
|
|||
[data-ty] { |
|||
display: block; |
|||
line-height: 2; |
|||
} |
|||
|
|||
[data-ty]:before { |
|||
/* Set up defaults and ensure empty lines are displayed. */ |
|||
content: ''; |
|||
display: inline-block; |
|||
vertical-align: middle; |
|||
} |
|||
|
|||
[data-ty="input"]:before, |
|||
[data-ty-prompt]:before { |
|||
margin-right: 0.75em; |
|||
color: var(--color-text-subtle); |
|||
} |
|||
|
|||
[data-ty="input"]:before { |
|||
content: '$'; |
|||
} |
|||
|
|||
[data-ty][data-ty-prompt]:before { |
|||
content: attr(data-ty-prompt); |
|||
} |
|||
|
|||
[data-ty-cursor]:after { |
|||
content: attr(data-ty-cursor); |
|||
font-family: monospace; |
|||
margin-left: 0.5em; |
|||
-webkit-animation: blink 1s infinite; |
|||
animation: blink 1s infinite; |
|||
} |
|||
|
|||
|
|||
/* Cursor animation */ |
|||
|
|||
@-webkit-keyframes blink { |
|||
50% { |
|||
opacity: 0; |
|||
} |
|||
} |
|||
|
|||
@keyframes blink { |
|||
50% { |
|||
opacity: 0; |
|||
} |
|||
} |
@ -0,0 +1,264 @@ |
|||
/** |
|||
* 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', '') |
|||
finish.innerHTML = "fast →" |
|||
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)) |
|||
} |
Loading…
Reference in new issue