| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- /**
- * # Terminal Riot Tag
- *
- * A pretend commandline interface capable of displaying output from a "shell"
- * Javascript class. Multiple tags can be used on a page, each will work
- * completely independently.
- *
- * ## Dependencies
- *
- * - Riot 2.3+ (http://riotjs.com/)
- *
- * ## Usage
- *
- * <terminal shell='myshellclass' welcome='text' prompt='text'></terminal>
- *
- * <script src='riot+compiler.min.js'></script>
- * <script src='myshell.js'></script>
- * <script src='terminal.tag' type='riot/tag'></script>
- * <script>riot.mount('terminal')</script>
- *
- * ## Configuring
- *
- * The terminal tag accepts 3 optional parameters:
- *
- * - `shell` - Text specifying the shell class to interact with.
- * - `welcome` - Text/HTML displayed when a terminal is first mounted.
- * - `prompt` - Text/HTML before the commandline input. Defaults to `'$ '`.
- *
- * With no shell logic, the terminal tag will simply print commands entered.
- *
- * Note: `welcome` and `prompt` can also be specified in the shell. Shell values
- * take priority over parameter values.
- *
- * ## Making A Shell Class
- *
- * A shell class should be defined before the tag is mounted by riot. It should
- * expect a single parameter (the terminal tag itself) which will be an
- * observable object.
- *
- * This observable will emit `'cmd_entered'` events containing input for
- * processing. Here is a minimal shell:
- *
- * // Contents of myshell.js
- * function myshellclass(events) {
- * this.prompt = ''
- * this.welcome = ''
- * events.on('cmd_entered', function(input) {
- * // Do processing
- * })
- * }
- *
- * ### Events
- *
- * The observable provides events to make things happen:
- *
- * events.trigger('disp_add', text) // Append `text` to the display
- * events.trigger('disp_set', text) // Display only `text`
- * events.trigger('disp_clear') // Clear the display
- * events.trigger('prompt_set', text) // Change the prompt to `text`
- * events.trigger('prompt_hide') // Hide the command prompt
- * events.trigger('prompt_show') // Show the command prompt
- * events.trigger('cmd_add', text) // Append `text` to the command line
- * events.trigger('cmd_set', text) // Set the command line to `text`
- * events.trigger('cli_hide') // Hide the command line and prompt
- * events.trigger('cli_show') // Show the command line and prompt
- *
- * There is also an event to swap between sets of displays and prompts:
- *
- * events.trigger('context_swap', name)
- *
- * The starting context name is `'default'`. Calling `'context_swap'` with:
- *
- * - a new name - initialises a new, empty set.
- * - an existing name - loads that named set.
- * - no name - returns to the `'default'` set.
- *
- * ### Terminal ID
- *
- * The observable also provides the id of the terminal tag via an `id` property.
- * This can be used, for example, for attaching handlers:
- *
- * document.getElementById(events.id).onkeypress = function() {}
- *
- */
- <terminal id={ id } tabindex='1'>
- <display welcome={ welcome } events={ this } />
- <commandline prompt={ prompt } events={ this } />
- <style>
- terminal { outline: none; }
- terminal * { padding: 0; margin: 0; line-height: normal; font-size: 100%; }
- </style>
- /**
- * Create a new shell with the class name given to the terminal tag.
- * The terminal tag object passes events between the shell and the other tags.
- */
- var shell = window[opts.shell] ? new window[opts.shell](this) : {}
- this.welcome = shell.welcome || opts.welcome
- this.prompt = shell.prompt || opts.prompt
- this.id = 'term-' + Math.floor(Math.random() * 10000)
- </terminal>
- <display>
- <div each={ output }>
- <raw content={ content } />
- </div>
- var ev = opts.events
- var self = this
- this.contexts = {}
- this.output = this.contexts['default'] = []
- this.on('mount', function() {
- this.add(opts.welcome)
- })
- ev.on('disp_add', function(text) {
- self.add(text)
- })
- ev.on('disp_set', function(text) {
- if (text) {
- self.clear()
- self.add(text)
- }
- })
- ev.on('disp_clear', function() {
- self.clear()
- })
- ev.on('context_swap', function(name) {
- name = name || 'default'
- if (!(name in self.contexts)) {
- self.contexts[name] = []
- }
- self.update({ output: self.contexts[name] })
- })
- add(text) {
- if (text) {
- text = text.replace(/\r\n|\r|\n/g, '<br />')
- this.output.push({ 'content': text })
- this.update()
- }
- }
- clear() {
- this.output.length = 0
- this.update()
- }
- </display>
- <commandline>
- <form autocomplete='off' onsubmit={ process } show={ visible }>
- <raw name='lhs' content={ prompt } show={ prompt_visible }>
- </raw><input type='text' name='command' />
- </form>
- <style>
- commandline input[name='command'],
- commandline input[name='command']:hover,
- commandline input[name='command']:focus {
- padding: 0; margin: 0; line-height: normal; font-size: 100%;
- background-color: transparent; border: none; outline: none;
- height: auto; width: 70%;
- display: inline;
- }
- </style>
- var ev = opts.events
- var self = this
- this.contexts = {}
- this.current = 'default'
- this.visible = true
- this.prompt = opts.prompt || '$ '
- this.prompt_visible = true
- this.on('mount', function() {
- this.command.focus()
- })
- ev.on('prompt_set', function(text) {
- self.prompt = text
- self.tags.lhs.write(text)
- })
- ev.on('prompt_hide', function() {
- self.update({ prompt_visible: false })
- })
- ev.on('prompt_show', function() {
- self.update({ prompt_visible: true })
- })
- ev.on('cmd_add', function(text) {
- self.command.value += text
- })
- ev.on('cmd_set', function(text) {
- self.command.value = text
- })
- ev.on('cli_hide', function() {
- self.update({ visible: false })
- })
- ev.on('cli_show', function() {
- self.update({ visible: true })
- self.command.focus()
- })
- ev.on('context_swap', function(name) {
- name = name || 'default'
- if (!(name in self.contexts)) {
- self.contexts[name] = { visible: true, prompt: '', prompt_visible: true }
- }
- // Save current values.
- self.contexts[self.current] = {
- visible: self.visible,
- prompt: self.prompt,
- prompt_visible: self.prompt_visible
- }
- // Load next context.
- self.visible = self.contexts[name].visible
- self.prompt = self.contexts[name].prompt
- self.prompt_visible = self.contexts[name].prompt_visible
- // Update the display.
- self.current = name
- self.tags.lhs.write(self.prompt)
- if (self.visible) {
- self.command.focus()
- }
- })
- process() {
- var prompt = this.prompt_visible ? this.prompt : ''
- var command = this.encode(this.command.value)
- ev.trigger('disp_add', prompt + command + '\n')
- ev.trigger('cmd_entered', command)
- this.command.value = ''
- this.command.blur()
- this.command.focus() // Refocus to scroll display and keep input in view.
- }
- encode(text) {
- return text.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
- }
- </commandline>
- <raw>
- <span></span>
- <style>
- raw { white-space: pre-wrap }
- </style>
- // Set initial html using `content` option, and...
- this.on('mount', function() {
- this.write(opts.content)
- })
- // Call `write()` manually to update the html.
- write(text) {
- this.root.innerHTML = text
- }
- </raw>
|