termynal.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. /**
  2. * termynal.js
  3. * A lightweight, modern and extensible animated terminal window, using
  4. * async/await.
  5. *
  6. * @author Ines Montani <ines@ines.io>
  7. * @version 0.0.1
  8. * @license MIT
  9. */
  10. 'use strict';
  11. /** Generate a terminal widget. */
  12. class Termynal {
  13. /**
  14. * Construct the widget's settings.
  15. * @param {(string|Node)=} container - Query selector or container element.
  16. * @param {Object=} options - Custom settings.
  17. * @param {string} options.prefix - Prefix to use for data attributes.
  18. * @param {number} options.startDelay - Delay before animation, in ms.
  19. * @param {number} options.typeDelay - Delay between each typed character, in ms.
  20. * @param {number} options.lineDelay - Delay between each line, in ms.
  21. * @param {number} options.progressLength - Number of characters displayed as progress bar.
  22. * @param {string} options.progressChar – Character to use for progress bar, defaults to █.
  23. * @param {number} options.progressPercent - Max percent of progress.
  24. * @param {string} options.cursor – Character to use for cursor, defaults to ▋.
  25. * @param {Object[]} lineData - Dynamically loaded line data objects.
  26. * @param {boolean} options.noInit - Don't initialise the animation.
  27. */
  28. constructor(container = '#termynal', options = {}) {
  29. this.container = (typeof container === 'string') ? document.querySelector(container) : container;
  30. this.pfx = `data-${options.prefix || 'ty'}`;
  31. this.originalStartDelay = this.startDelay = options.startDelay
  32. || parseFloat(this.container.getAttribute(`${this.pfx}-startDelay`)) || 300;
  33. this.originalTypeDelay = this.typeDelay = options.typeDelay
  34. || parseFloat(this.container.getAttribute(`${this.pfx}-typeDelay`)) || 30;
  35. this.originalLineDelay = this.lineDelay = options.lineDelay
  36. || parseFloat(this.container.getAttribute(`${this.pfx}-lineDelay`)) || 1500;
  37. this.progressLength = options.progressLength
  38. || parseFloat(this.container.getAttribute(`${this.pfx}-progressLength`)) || 40;
  39. this.progressChar = options.progressChar
  40. || this.container.getAttribute(`${this.pfx}-progressChar`) || '█';
  41. this.progressPercent = options.progressPercent
  42. || parseFloat(this.container.getAttribute(`${this.pfx}-progressPercent`)) || 100;
  43. this.cursor = options.cursor
  44. || this.container.getAttribute(`${this.pfx}-cursor`) || '▋';
  45. this.lineData = this.lineDataToElements(options.lineData || []);
  46. this.loadLines()
  47. if (!options.noInit) this.init()
  48. }
  49. loadLines() {
  50. // Load all the lines and create the container so that the size is fixed
  51. // Otherwise it would be changing and the user viewport would be constantly
  52. // moving as she/he scrolls
  53. const finish = this.generateFinish()
  54. finish.style.visibility = 'hidden'
  55. this.container.appendChild(finish)
  56. // Appends dynamically loaded lines to existing line elements.
  57. this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(this.lineData);
  58. for (let line of this.lines) {
  59. line.style.visibility = 'hidden'
  60. this.container.appendChild(line)
  61. }
  62. const restart = this.generateRestart()
  63. restart.style.visibility = 'hidden'
  64. this.container.appendChild(restart)
  65. this.container.setAttribute('data-termynal', '');
  66. }
  67. /**
  68. * Initialise the widget, get lines, clear container and start animation.
  69. */
  70. init() {
  71. /**
  72. * Calculates width and height of Termynal container.
  73. * If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS.
  74. */
  75. const containerStyle = getComputedStyle(this.container);
  76. this.container.style.width = containerStyle.width !== '0px' ?
  77. containerStyle.width : undefined;
  78. this.container.style.minHeight = containerStyle.height !== '0px' ?
  79. containerStyle.height : undefined;
  80. this.container.setAttribute('data-termynal', '');
  81. this.container.innerHTML = '';
  82. for (let line of this.lines) {
  83. line.style.visibility = 'visible'
  84. }
  85. this.start();
  86. }
  87. /**
  88. * Start the animation and rener the lines depending on their data attributes.
  89. */
  90. async start() {
  91. this.addFinish()
  92. await this._wait(this.startDelay);
  93. for (let line of this.lines) {
  94. const type = line.getAttribute(this.pfx);
  95. const delay = line.getAttribute(`${this.pfx}-delay`) || this.lineDelay;
  96. if (type == 'input') {
  97. line.setAttribute(`${this.pfx}-cursor`, this.cursor);
  98. await this.type(line);
  99. await this._wait(delay);
  100. }
  101. else if (type == 'progress') {
  102. await this.progress(line);
  103. await this._wait(delay);
  104. }
  105. else {
  106. this.container.appendChild(line);
  107. await this._wait(delay);
  108. }
  109. line.removeAttribute(`${this.pfx}-cursor`);
  110. }
  111. this.addRestart()
  112. this.finishElement.style.visibility = 'hidden'
  113. this.lineDelay = this.originalLineDelay
  114. this.typeDelay = this.originalTypeDelay
  115. this.startDelay = this.originalStartDelay
  116. }
  117. generateRestart() {
  118. const restart = document.createElement('a')
  119. restart.onclick = (e) => {
  120. e.preventDefault()
  121. this.container.innerHTML = ''
  122. this.init()
  123. }
  124. restart.href = '#'
  125. restart.setAttribute('data-terminal-control', '')
  126. restart.innerHTML = "restart ↻"
  127. return restart
  128. }
  129. generateFinish() {
  130. const finish = document.createElement('a')
  131. finish.onclick = (e) => {
  132. e.preventDefault()
  133. this.lineDelay = 0
  134. this.typeDelay = 0
  135. this.startDelay = 0
  136. }
  137. finish.href = '#'
  138. finish.setAttribute('data-terminal-control', '')
  139. finish.innerHTML = "fast →"
  140. this.finishElement = finish
  141. return finish
  142. }
  143. addRestart() {
  144. const restart = this.generateRestart()
  145. this.container.appendChild(restart)
  146. }
  147. addFinish() {
  148. const finish = this.generateFinish()
  149. this.container.appendChild(finish)
  150. }
  151. /**
  152. * Animate a typed line.
  153. * @param {Node} line - The line element to render.
  154. */
  155. async type(line) {
  156. const chars = [...line.textContent];
  157. line.textContent = '';
  158. this.container.appendChild(line);
  159. for (let char of chars) {
  160. const delay = line.getAttribute(`${this.pfx}-typeDelay`) || this.typeDelay;
  161. await this._wait(delay);
  162. line.textContent += char;
  163. }
  164. }
  165. /**
  166. * Animate a progress bar.
  167. * @param {Node} line - The line element to render.
  168. */
  169. async progress(line) {
  170. const progressLength = line.getAttribute(`${this.pfx}-progressLength`)
  171. || this.progressLength;
  172. const progressChar = line.getAttribute(`${this.pfx}-progressChar`)
  173. || this.progressChar;
  174. const chars = progressChar.repeat(progressLength);
  175. const progressPercent = line.getAttribute(`${this.pfx}-progressPercent`)
  176. || this.progressPercent;
  177. line.textContent = '';
  178. this.container.appendChild(line);
  179. for (let i = 1; i < chars.length + 1; i++) {
  180. await this._wait(this.typeDelay);
  181. const percent = Math.round(i / chars.length * 100);
  182. line.textContent = `${chars.slice(0, i)} ${percent}%`;
  183. if (percent>progressPercent) {
  184. break;
  185. }
  186. }
  187. }
  188. /**
  189. * Helper function for animation delays, called with `await`.
  190. * @param {number} time - Timeout, in ms.
  191. */
  192. _wait(time) {
  193. return new Promise(resolve => setTimeout(resolve, time));
  194. }
  195. /**
  196. * Converts line data objects into line elements.
  197. *
  198. * @param {Object[]} lineData - Dynamically loaded lines.
  199. * @param {Object} line - Line data object.
  200. * @returns {Element[]} - Array of line elements.
  201. */
  202. lineDataToElements(lineData) {
  203. return lineData.map(line => {
  204. let div = document.createElement('div');
  205. div.innerHTML = `<span ${this._attributes(line)}>${line.value || ''}</span>`;
  206. return div.firstElementChild;
  207. });
  208. }
  209. /**
  210. * Helper function for generating attributes string.
  211. *
  212. * @param {Object} line - Line data object.
  213. * @returns {string} - String of attributes.
  214. */
  215. _attributes(line) {
  216. let attrs = '';
  217. for (let prop in line) {
  218. // Custom add class
  219. if (prop === 'class') {
  220. attrs += ` class=${line[prop]} `
  221. continue
  222. }
  223. if (prop === 'type') {
  224. attrs += `${this.pfx}="${line[prop]}" `
  225. } else if (prop !== 'value') {
  226. attrs += `${this.pfx}-${prop}="${line[prop]}" `
  227. }
  228. }
  229. return attrs;
  230. }
  231. }
  232. /**
  233. * HTML API: If current script has container(s) specified, initialise Termynal.
  234. */
  235. if (document.currentScript.hasAttribute('data-termynal-container')) {
  236. const containers = document.currentScript.getAttribute('data-termynal-container');
  237. containers.split('|')
  238. .forEach(container => new Termynal(container))
  239. }