/**
* # 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
*
*
*
*
*
*
*
*
* ## 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() {}
*
*/
/**
* 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)
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, '
')
this.output.push({ 'content': text })
this.update()
}
}
clear() {
this.output.length = 0
this.update()
}
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,'>')
}
// 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
}