terminal.tag 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. /**
  2. * # Terminal Riot Tag
  3. *
  4. * A pretend commandline interface capable of displaying output from a "shell"
  5. * Javascript class. Multiple tags can be used on a page, each will work
  6. * completely independently.
  7. *
  8. * ## Dependencies
  9. *
  10. * - Riot 2.3+ (http://riotjs.com/)
  11. *
  12. * ## Usage
  13. *
  14. * <terminal shell='myshellclass' welcome='text' prompt='text'></terminal>
  15. *
  16. * <script src='riot+compiler.min.js'></script>
  17. * <script src='myshell.js'></script>
  18. * <script src='terminal.tag' type='riot/tag'></script>
  19. * <script>riot.mount('terminal')</script>
  20. *
  21. * ## Configuring
  22. *
  23. * The terminal tag accepts 3 optional parameters:
  24. *
  25. * - `shell` - Text specifying the shell class to interact with.
  26. * - `welcome` - Text/HTML displayed when a terminal is first mounted.
  27. * - `prompt` - Text/HTML before the commandline input. Defaults to `'$ '`.
  28. *
  29. * With no shell logic, the terminal tag will simply print commands entered.
  30. *
  31. * Note: `welcome` and `prompt` can also be specified in the shell. Shell values
  32. * take priority over parameter values.
  33. *
  34. * ## Making A Shell Class
  35. *
  36. * A shell class should be defined before the tag is mounted by riot. It should
  37. * expect a single parameter (the terminal tag itself) which will be an
  38. * observable object.
  39. *
  40. * This observable will emit `'cmd_entered'` events containing input for
  41. * processing. Here is a minimal shell:
  42. *
  43. * // Contents of myshell.js
  44. * function myshellclass(events) {
  45. * this.prompt = ''
  46. * this.welcome = ''
  47. * events.on('cmd_entered', function(input) {
  48. * // Do processing
  49. * })
  50. * }
  51. *
  52. * ### Events
  53. *
  54. * The observable provides events to make things happen:
  55. *
  56. * events.trigger('disp_add', text) // Append `text` to the display
  57. * events.trigger('disp_set', text) // Display only `text`
  58. * events.trigger('disp_clear') // Clear the display
  59. * events.trigger('prompt_set', text) // Change the prompt to `text`
  60. * events.trigger('prompt_hide') // Hide the command prompt
  61. * events.trigger('prompt_show') // Show the command prompt
  62. * events.trigger('cmd_add', text) // Append `text` to the command line
  63. * events.trigger('cmd_set', text) // Set the command line to `text`
  64. * events.trigger('cli_hide') // Hide the command line and prompt
  65. * events.trigger('cli_show') // Show the command line and prompt
  66. *
  67. * There is also an event to swap between sets of displays and prompts:
  68. *
  69. * events.trigger('context_swap', name)
  70. *
  71. * The starting context name is `'default'`. Calling `'context_swap'` with:
  72. *
  73. * - a new name - initialises a new, empty set.
  74. * - an existing name - loads that named set.
  75. * - no name - returns to the `'default'` set.
  76. *
  77. * ### Terminal ID
  78. *
  79. * The observable also provides the id of the terminal tag via an `id` property.
  80. * This can be used, for example, for attaching handlers:
  81. *
  82. * document.getElementById(events.id).onkeypress = function() {}
  83. *
  84. */
  85. <terminal id={ id } tabindex='1'>
  86. <display welcome={ welcome } events={ this } />
  87. <commandline prompt={ prompt } events={ this } />
  88. <style>
  89. terminal { outline: none; }
  90. terminal * { padding: 0; margin: 0; line-height: normal; font-size: 100%; }
  91. </style>
  92. /**
  93. * Create a new shell with the class name given to the terminal tag.
  94. * The terminal tag object passes events between the shell and the other tags.
  95. */
  96. var shell = window[opts.shell] ? new window[opts.shell](this) : {}
  97. this.welcome = shell.welcome || opts.welcome
  98. this.prompt = shell.prompt || opts.prompt
  99. this.id = 'term-' + Math.floor(Math.random() * 10000)
  100. </terminal>
  101. <display>
  102. <div each={ output }>
  103. <raw content={ content } />
  104. </div>
  105. var ev = opts.events
  106. var self = this
  107. this.contexts = {}
  108. this.output = this.contexts['default'] = []
  109. this.on('mount', function() {
  110. this.add(opts.welcome)
  111. })
  112. ev.on('disp_add', function(text) {
  113. self.add(text)
  114. })
  115. ev.on('disp_set', function(text) {
  116. if (text) {
  117. self.clear()
  118. self.add(text)
  119. }
  120. })
  121. ev.on('disp_clear', function() {
  122. self.clear()
  123. })
  124. ev.on('context_swap', function(name) {
  125. name = name || 'default'
  126. if (!(name in self.contexts)) {
  127. self.contexts[name] = []
  128. }
  129. self.update({ output: self.contexts[name] })
  130. })
  131. add(text) {
  132. if (text) {
  133. text = text.replace(/\r\n|\r|\n/g, '<br />')
  134. this.output.push({ 'content': text })
  135. this.update()
  136. }
  137. }
  138. clear() {
  139. this.output.length = 0
  140. this.update()
  141. }
  142. </display>
  143. <commandline>
  144. <form autocomplete='off' onsubmit={ process } show={ visible }>
  145. <raw name='lhs' content={ prompt } show={ prompt_visible }>
  146. </raw><input type='text' name='command' />
  147. </form>
  148. <style>
  149. commandline input[name='command'],
  150. commandline input[name='command']:hover,
  151. commandline input[name='command']:focus {
  152. padding: 0; margin: 0; line-height: normal; font-size: 100%;
  153. background-color: transparent; border: none; outline: none;
  154. height: auto; width: 70%;
  155. display: inline;
  156. }
  157. </style>
  158. var ev = opts.events
  159. var self = this
  160. this.contexts = {}
  161. this.current = 'default'
  162. this.visible = true
  163. this.prompt = opts.prompt || '$ '
  164. this.prompt_visible = true
  165. this.on('mount', function() {
  166. this.command.focus()
  167. })
  168. ev.on('prompt_set', function(text) {
  169. self.prompt = text
  170. self.tags.lhs.write(text)
  171. })
  172. ev.on('prompt_hide', function() {
  173. self.update({ prompt_visible: false })
  174. })
  175. ev.on('prompt_show', function() {
  176. self.update({ prompt_visible: true })
  177. })
  178. ev.on('cmd_add', function(text) {
  179. self.command.value += text
  180. })
  181. ev.on('cmd_set', function(text) {
  182. self.command.value = text
  183. })
  184. ev.on('cli_hide', function() {
  185. self.update({ visible: false })
  186. })
  187. ev.on('cli_show', function() {
  188. self.update({ visible: true })
  189. self.command.focus()
  190. })
  191. ev.on('context_swap', function(name) {
  192. name = name || 'default'
  193. if (!(name in self.contexts)) {
  194. self.contexts[name] = { visible: true, prompt: '', prompt_visible: true }
  195. }
  196. // Save current values.
  197. self.contexts[self.current] = {
  198. visible: self.visible,
  199. prompt: self.prompt,
  200. prompt_visible: self.prompt_visible
  201. }
  202. // Load next context.
  203. self.visible = self.contexts[name].visible
  204. self.prompt = self.contexts[name].prompt
  205. self.prompt_visible = self.contexts[name].prompt_visible
  206. // Update the display.
  207. self.current = name
  208. self.tags.lhs.write(self.prompt)
  209. if (self.visible) {
  210. self.command.focus()
  211. }
  212. })
  213. process() {
  214. var prompt = this.prompt_visible ? this.prompt : ''
  215. var command = this.encode(this.command.value)
  216. ev.trigger('disp_add', prompt + command + '\n')
  217. ev.trigger('cmd_entered', command)
  218. this.command.value = ''
  219. this.command.blur()
  220. this.command.focus() // Refocus to scroll display and keep input in view.
  221. }
  222. encode(text) {
  223. return text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
  224. }
  225. </commandline>
  226. <raw>
  227. <span></span>
  228. <style>
  229. raw { white-space: pre-wrap }
  230. </style>
  231. // Set initial html using `content` option, and...
  232. this.on('mount', function() {
  233. this.write(opts.content)
  234. })
  235. // Call `write()` manually to update the html.
  236. write(text) {
  237. this.root.innerHTML = text
  238. }
  239. </raw>