Restructuring

This commit is contained in:
2017-09-11 17:20:53 +02:00
parent e923fa1746
commit acae3992f5
26 changed files with 48 additions and 27 deletions
View File
+130
View File
@@ -0,0 +1,130 @@
/** Global generic init */
$.ready(function () {
// Checkbox UI (checkbox CSS and hidden input with int value)
$('.Row.checkbox').forEach(function (x) {
let inp = x.querySelector('input')
let box = x.querySelector('.box')
$(box).toggleClass('checked', inp.value)
let hdl = function () {
inp.value = 1 - inp.value
$(box).toggleClass('checked', inp.value)
}
$(x).on('click', hdl).on('keypress', cr(hdl))
})
// Expanding boxes on mobile
$('.Box.mobcol,.Box.fold').forEach(function (x) {
let h = x.querySelector('h2')
let hdl = function () {
$(x).toggleClass('expanded')
}
$(h).on('click', hdl).on('keypress', cr(hdl))
})
$('form').forEach(function (x) {
$(x).on('keypress', function (e) {
if ((e.keyCode === 10 || e.keyCode === 13) && e.ctrlKey) {
x.submit()
}
})
})
// loader dots...
setInterval(function () {
$('.anim-dots').each(function (x) {
let $x = $(x)
let dots = $x.html() + '.'
if (dots.length === 5) dots = '.'
$x.html(dots)
})
}, 1000)
// flipping number boxes with the mouse wheel
$('input[type=number]').on('mousewheel', function (e) {
let $this = $(this)
let val = +$this.val()
if (isNaN(val)) val = 1
const step = +($this.attr('step') || 1)
const min = +$this.attr('min')
const max = +$this.attr('max')
if (e.wheelDelta > 0) {
val += step
} else {
val -= step
}
if (undef(min)) val = Math.max(val, +min)
if (undef(max)) val = Math.min(val, +max)
$this.val(val)
if ('createEvent' in document) {
let evt = document.createEvent('HTMLEvents')
evt.initEvent('change', false, true)
$this[0].dispatchEvent(evt)
} else {
$this[0].fireEvent('onchange')
}
e.preventDefault()
})
// populate the form errors box from GET arg ?err=...
// (a way to pass errors back from server via redirect)
let errAt = location.search.indexOf('err=')
if (errAt !== -1 && qs('.Box.errors')) {
let errs = location.search.substr(errAt + 4).split(',')
let hres = []
errs.forEach(function (er) {
let lbl = qs('label[for="' + er + '"]')
if (lbl) {
lbl.classList.add('error')
hres.push(lbl.childNodes[0].textContent.trim().replace(/: ?$/, ''))
} else {
hres.push(er)
}
})
qs('.Box.errors .list').innerHTML = hres.join(', ')
qs('.Box.errors').classList.remove('hidden')
}
Modal.init()
Notify.init()
// remove tabindixes from h2 if wide
if (window.innerWidth > 550) {
$('.Box h2').forEach(function (x) {
x.removeAttribute('tabindex')
})
// brand works as a link back to term in widescreen mode
let br = qs('#brand')
br && br.addEventListener('click', function () {
location.href = '/' // go to terminal
})
}
})
// setup the ajax loader
$._loader = function (vis) {
$('#loader').toggleClass('show', vis)
}
// reveal content on load
function showPage () {
$('#content').addClass('load')
}
// Auto reveal pages other than the terminal (sets window.noAutoShow)
$.ready(function () {
if (window.noAutoShow !== true) {
setTimeout(function () {
showPage()
}, 1)
}
})
+106
View File
@@ -0,0 +1,106 @@
window.attachDebugScreen = function (screen) {
const debugCanvas = mk('canvas')
const ctx = debugCanvas.getContext('2d')
debugCanvas.style.position = 'absolute'
// hackity hack should probably set this in CSS
debugCanvas.style.top = '6px'
debugCanvas.style.left = '6px'
debugCanvas.style.pointerEvents = 'none'
let addCanvas = function () {
if (!debugCanvas.parentNode) screen.canvas.parentNode.appendChild(debugCanvas)
}
let removeCanvas = function () {
if (debugCanvas.parentNode) debugCanvas.parentNode.removeChild(debugCanvas)
}
let updateCanvasSize = function () {
let { width, height, devicePixelRatio } = screen.window
let cellSize = screen.getCellSize()
debugCanvas.width = width * cellSize.width * devicePixelRatio
debugCanvas.height = height * cellSize.height * devicePixelRatio
debugCanvas.style.width = `${width * cellSize.width}px`
debugCanvas.style.height = `${height * cellSize.height}px`
}
let startTime, endTime, lastReason
let cells = new Map()
let startDrawing
screen._debug = {
drawStart (reason) {
lastReason = reason
startTime = Date.now()
},
drawEnd () {
endTime = Date.now()
console.log(`Draw: ${lastReason} (${(endTime - startTime)} ms) with fancy graphics: ${screen.window.graphics}`)
startDrawing()
},
setCell (cell, flags) {
cells.set(cell, [flags, Date.now()])
}
}
let isDrawing = false
let drawLoop = function () {
if (isDrawing) requestAnimationFrame(drawLoop)
let { devicePixelRatio, width, height } = screen.window
let { width: cellWidth, height: cellHeight } = screen.getCellSize()
let screenLength = width * height
let now = Date.now()
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0)
ctx.clearRect(0, 0, width * cellWidth, height * cellHeight)
let activeCells = 0
for (let cell = 0; cell < screenLength; cell++) {
if (!cells.has(cell) || cells.get(cell)[0] === 0) continue
let [flags, timestamp] = cells.get(cell)
let elapsedTime = (now - timestamp) / 1000
if (elapsedTime > 1) continue
activeCells++
ctx.globalAlpha = 0.5 * Math.max(0, 1 - elapsedTime)
let x = cell % width
let y = Math.floor(cell / width)
if (flags & 1) {
// redrawn
ctx.fillStyle = '#f0f'
}
if (flags & 2) {
// updated
ctx.fillStyle = '#0f0'
}
ctx.fillRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight)
if (flags & 4) {
// wide cell
ctx.lineWidth = 2
ctx.strokeStyle = '#f00'
ctx.strokeRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight)
}
}
if (activeCells === 0) {
isDrawing = false
removeCanvas()
}
}
startDrawing = function () {
if (isDrawing) return
addCanvas()
updateCanvasSize()
isDrawing = true
drawLoop()
}
}
+8
View File
@@ -0,0 +1,8 @@
// Generated from PHP locale file
let _tr = {
"wifi.connected_ip_is": "Connected, IP is ",
"wifi.not_conn": "Not connected.",
"wifi.enter_passwd": "Enter password for \":ssid:\""
};
function tr (key) { return _tr[key] || '?' + key + '?' }
+703
View File
@@ -0,0 +1,703 @@
/*!chibi 3.0.7, Copyright 2012-2016 Kyle Barrow, released under MIT license */
// MODIFIED VERSION.
(function () {
'use strict';
var readyfn = [],
loadedfn = [],
domready = false,
pageloaded = false,
d = document,
w = window;
// Fire any function calls on ready event
function fireReady() {
var i;
domready = true;
for (i = 0; i < readyfn.length; i += 1) {
readyfn[i]();
}
readyfn = [];
}
// Fire any function calls on loaded event
function fireLoaded() {
var i;
pageloaded = true;
// For browsers with no DOM loaded support
if (!domready) {
fireReady();
}
for (i = 0; i < loadedfn.length; i += 1) {
loadedfn[i]();
}
loadedfn = [];
}
// Check DOM ready, page loaded
if (d.addEventListener) {
// Standards
d.addEventListener('DOMContentLoaded', fireReady, false);
w.addEventListener('load', fireLoaded, false);
} else if (d.attachEvent) {
// IE
d.attachEvent('onreadystatechange', fireReady);
// IE < 9
w.attachEvent('onload', fireLoaded);
} else {
// Anything else
w.onload = fireLoaded;
}
// Utility functions
// Loop through node array
function nodeLoop(fn, nodes) {
var i;
// Good idea to walk up the DOM
for (i = nodes.length - 1; i >= 0; i -= 1) {
fn(nodes[i]);
}
}
// Convert to camel case
function cssCamel(property) {
return property.replace(/-\w/g, function (result) {
return result.charAt(1).toUpperCase();
});
}
// Get computed style
function computeStyle(elm, property) {
// IE, everything else or null
return (elm.currentStyle) ? elm.currentStyle[cssCamel(property)] : (w.getComputedStyle) ? w.getComputedStyle(elm, null).getPropertyValue(property) : null;
}
// Returns URI encoded query string pair
function queryPair(name, value) {
return encodeURIComponent(name).replace(/%20/g, '+') + '=' + encodeURIComponent(value).replace(/%20/g, '+');
}
// Set CSS, important to wrap in try to prevent error thown on unsupported property
function setCss(elm, property, value) {
try {
elm.style[cssCamel(property)] = value;
} catch (e) {
}
}
// Show CSS
function showCss(elm) {
elm.style.display = '';
// For elements still hidden by style block
if (computeStyle(elm, 'display') === 'none') {
elm.style.display = 'block';
}
}
// Serialize form & JSON values
function serializeData(nodes) {
var querystring = '', subelm, i, j;
if (nodes.constructor === Object) { // Serialize JSON data
for (subelm in nodes) {
if (nodes.hasOwnProperty(subelm)) {
if (nodes[subelm].constructor === Array) {
for (i = 0; i < nodes[subelm].length; i += 1) {
querystring += '&' + queryPair(subelm, nodes[subelm][i]);
}
} else {
querystring += '&' + queryPair(subelm, nodes[subelm]);
}
}
}
} else { // Serialize node data
nodeLoop(function (elm) {
if (elm.nodeName === 'FORM') {
for (i = 0; i < elm.elements.length; i += 1) {
subelm = elm.elements[i];
if (!subelm.disabled) {
switch (subelm.type) {
// Ignore buttons, unsupported XHR 1 form fields
case 'button':
case 'image':
case 'file':
case 'submit':
case 'reset':
break;
case 'select-one':
if (subelm.length > 0) {
querystring += '&' + queryPair(subelm.name, subelm.value);
}
break;
case 'select-multiple':
for (j = 0; j < subelm.length; j += 1) {
if (subelm[j].selected) {
querystring += '&' + queryPair(subelm.name, subelm[j].value);
}
}
break;
case 'checkbox':
case 'radio':
if (subelm.checked) {
querystring += '&' + queryPair(subelm.name, subelm.value);
}
break;
// Everything else including shinny new HTML5 input types
default:
querystring += '&' + queryPair(subelm.name, subelm.value);
}
}
}
}
}, nodes);
}
// Tidy up first &
return (querystring.length > 0) ? querystring.substring(1) : '';
}
// Class helper
function classHelper(classes, action, nodes) {
var classarray, search, i, replace, has = false;
if (classes) {
// Trim any whitespace
classarray = classes.split(/\s+/);
nodeLoop(function (elm) {
for (i = 0; i < classarray.length; i += 1) {
var clz = classarray[i];
if (action === 'remove') {
elm.classList.remove(clz);
}
else if (action === 'add') {
elm.classList.add(clz);
}
else if (action === 'toggle') {
elm.classList.toggle(clz);
}
else if (action === 'has') {
if (elm.classList.contains(clz)) {
has = true;
break;
}
}
// search = new RegExp('\\b' + classarray[i] + '\\b', 'g');
// replace = new RegExp(' *' + classarray[i] + '\\b', 'g');
// if (action === 'remove') {
// elm.className = elm.className.replace(search, '');
// } else if (action === 'toggle') {
// elm.className = (elm.className.match(search)) ? elm.className.replace(replace, '') : elm.className + ' ' + classarray[i];
// } else if (action === 'has') {
// if (elm.className.match(search)) {
// has = true;
// break;
// }
// }
}
}, nodes);
}
return has;
}
// HTML insertion helper
function insertHtml(value, position, nodes) {
var tmpnodes, tmpnode;
if (value) {
nodeLoop(function (elm) {
// No insertAdjacentHTML support for FF < 8 and IE doesn't allow insertAdjacentHTML table manipulation, so use this instead
// Convert string to node. We can't innerHTML on a document fragment
tmpnodes = d.createElement('div');
tmpnodes.innerHTML = value;
while ((tmpnode = tmpnodes.lastChild) !== null) {
// Catch error in unlikely case elm has been removed
try {
if (position === 'before') {
elm.parentNode.insertBefore(tmpnode, elm);
} else if (position === 'after') {
elm.parentNode.insertBefore(tmpnode, elm.nextSibling);
} else if (position === 'append') {
elm.appendChild(tmpnode);
} else if (position === 'prepend') {
elm.insertBefore(tmpnode, elm.firstChild);
}
} catch (e) {
break;
}
}
}, nodes);
}
}
// Get nodes and return chibi
function chibi(selector) {
var cb, nodes = [], json = false, nodelist, i;
if (selector) {
// Element node, would prefer to use (selector instanceof HTMLElement) but no IE support
if (selector.nodeType && selector.nodeType === 1) {
nodes = [selector]; // return element as node list
} else if (typeof selector === 'object') {
// JSON, document object or node list, would prefer to use (selector instanceof NodeList) but no IE support
json = (typeof selector.length !== 'number');
nodes = selector;
} else if (typeof selector === 'string') {
nodelist = d.querySelectorAll(selector);
// Convert node list to array so results have full access to array methods
// Array.prototype.slice.call not supported in IE < 9 and often slower than loop anyway
for (i = 0; i < nodelist.length; i += 1) {
nodes[i] = nodelist[i];
}
}
}
// Only attach nodes if not JSON
cb = json ? {} : nodes;
// Public functions
// Executes a function on nodes
cb.each = function (fn) {
if (typeof fn === 'function') {
nodeLoop(function (elm) {
// <= IE 8 loses scope so need to apply
return fn.apply(elm, arguments);
}, nodes);
}
return cb;
};
// Find first
cb.first = function () {
return chibi(nodes.shift());
};
// Find last
cb.last = function () {
return chibi(nodes.pop());
};
// Find odd
cb.odd = function () {
var odds = [], i;
for (i = 0; i < nodes.length; i += 2) {
odds.push(nodes[i]);
}
return chibi(odds);
};
// Find even
cb.even = function () {
var evens = [], i;
for (i = 1; i < nodes.length; i += 2) {
evens.push(nodes[i]);
}
return chibi(evens);
};
// Hide node
cb.hide = function () {
nodeLoop(function (elm) {
elm.style.display = 'none';
}, nodes);
return cb;
};
// Show node
cb.show = function () {
nodeLoop(function (elm) {
showCss(elm);
}, nodes);
return cb;
};
// Toggle node display
cb.toggle = function (state) {
if (typeof state != 'undefined') { // ADDED
if (state)
cb.show();
else
cb.hide();
} else {
nodeLoop(function (elm) {
// computeStyle instead of style.display == 'none' catches elements that are hidden via style block
if (computeStyle(elm, 'display') === 'none') {
showCss(elm);
} else {
elm.style.display = 'none';
}
}, nodes);
}
return cb;
};
// Remove node
cb.remove = function () {
nodeLoop(function (elm) {
// Catch error in unlikely case elm has been removed
try {
elm.parentNode.removeChild(elm);
} catch (e) {
}
}, nodes);
return chibi();
};
// Get/Set CSS
cb.css = function (property, value) {
if (property) {
if (value || value === '') {
nodeLoop(function (elm) {
setCss(elm, property, value);
}, nodes);
return cb;
}
if (nodes[0]) {
if (nodes[0].style[cssCamel(property)]) {
return nodes[0].style[cssCamel(property)];
}
if (computeStyle(nodes[0], property)) {
return computeStyle(nodes[0], property);
}
}
}
};
// Get class(es)
cb.getClass = function () {
if (nodes[0] && nodes[0].className.length > 0) {
// Weak IE trim support
return nodes[0].className.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '').replace(/\s+/, ' ');
}
};
// Set (replaces) classes
cb.setClass = function (classes) {
if (classes || classes === '') {
nodeLoop(function (elm) {
elm.className = classes;
}, nodes);
}
return cb;
};
// Add class
cb.addClass = function (classes) {
classHelper(classes, 'add', nodes);
// if (classes) {
// nodeLoop(function (elm) {
// elm.className += ' ' + classes;
// }, nodes);
// }
return cb;
};
// Remove class
cb.removeClass = function (classes) {
classHelper(classes, 'remove', nodes);
return cb;
};
// Toggle class
cb.toggleClass = function (classes, set) {
var method = ((typeof set === 'undefined') ? 'toggle' : (+set ? 'add' : 'remove'));
classHelper(classes, method, nodes);
return cb;
};
// Has class
cb.hasClass = function (classes) {
return classHelper(classes, 'has', nodes);
};
// Get/set HTML
cb.html = function (value) {
if (value || value === '') {
nodeLoop(function (elm) {
elm.innerHTML = value;
}, nodes);
return cb;
}
if (nodes[0]) {
return nodes[0].innerHTML;
}
};
// Insert HTML before selector
cb.htmlBefore = function (value) {
insertHtml(value, 'before', nodes);
return cb;
};
// Insert HTML after selector
cb.htmlAfter = function (value) {
insertHtml(value, 'after', nodes);
return cb;
};
// Insert HTML after selector innerHTML
cb.htmlAppend = function (value) {
insertHtml(value, 'append', nodes);
return cb;
};
// Insert HTML before selector innerHTML
cb.htmlPrepend = function (value) {
insertHtml(value, 'prepend', nodes);
return cb;
};
// Get/Set HTML attributes
cb.attr = function (property, value) {
if (property) {
property = property.toLowerCase();
// IE < 9 doesn't allow style or class via get/setAttribute so switch. cssText returns prettier CSS anyway
if (typeof value !== 'undefined') {//FIXED BUG HERE
nodeLoop(function (elm) {
if (property === 'style') {
elm.style.cssText = value;
} else if (property === 'class') {
elm.className = value;
} else {
elm.setAttribute(property, value);
}
}, nodes);
return cb;
}
if (nodes[0]) {
if (property === 'style') {
if (nodes[0].style.cssText) {
return nodes[0].style.cssText;
}
} else if (property === 'class') {
if (nodes[0].className) {
return nodes[0].className;
}
} else {
if (nodes[0].getAttribute(property)) {
return nodes[0].getAttribute(property);
}
}
}
}
};
// Get/Set HTML data property
cb.data = function (key, value) {
if (key) {
return cb.attr('data-' + key, value);
}
};
// Get/Set form element values
cb.val = function (value) {
var values, i, j;
if (typeof value != 'undefined') { // FIXED A BUG HERE
nodeLoop(function (elm) {
switch (elm.nodeName) {
case 'SELECT':
if (typeof value === 'string' || typeof value === 'number') {
value = [value];
}
for (i = 0; i < elm.length; i += 1) {
// Multiple select
for (j = 0; j < value.length; j += 1) {
elm[i].selected = '';
if (elm[i].value === ""+value[j]) {
elm[i].selected = 'selected';
break;
}
}
}
break;
case 'INPUT':
case 'TEXTAREA':
case 'BUTTON':
elm.value = value;
break;
}
}, nodes);
return cb;
}
if (nodes[0]) {
switch (nodes[0].nodeName) {
case 'SELECT':
values = [];
for (i = 0; i < nodes[0].length; i += 1) {
if (nodes[0][i].selected) {
values.push(nodes[0][i].value);
}
}
return (values.length > 1) ? values : values[0];
case 'INPUT':
case 'TEXTAREA':
case 'BUTTON':
return nodes[0].value;
}
}
};
// Return matching checked checkbox or radios
cb.checked = function (check) {
if (typeof check === 'boolean') {
nodeLoop(function (elm) {
if (elm.nodeName === 'INPUT' && (elm.type === 'checkbox' || elm.type === 'radio')) {
elm.checked = check;
}
}, nodes);
return cb;
}
if (nodes[0] && nodes[0].nodeName === 'INPUT' && (nodes[0].type === 'checkbox' || nodes[0].type === 'radio')) {
return (!!nodes[0].checked);
}
};
// Add event handler
cb.on = function (event, fn) {
if (selector === w || selector === d) {
nodes = [selector];
}
nodeLoop(function (elm) {
if (d.addEventListener) {
elm.addEventListener(event, fn, false);
} else if (d.attachEvent) {
// <= IE 8 loses scope so need to apply, we add this to object so we can detach later (can't detach anonymous functions)
elm[event + fn] = function () {
return fn.apply(elm, arguments);
};
elm.attachEvent('on' + event, elm[event + fn]);
}
}, nodes);
return cb;
};
// Remove event handler
cb.off = function (event, fn) {
if (selector === w || selector === d) {
nodes = [selector];
}
nodeLoop(function (elm) {
if (d.addEventListener) {
elm.removeEventListener(event, fn, false);
} else if (d.attachEvent) {
elm.detachEvent('on' + event, elm[event + fn]);
// Tidy up
elm[event + fn] = null;
}
}, nodes);
return cb;
};
return cb;
}
// Basic XHR
chibi.ajax = function (options) { // if options is a number, it's timeout in ms
var opts = Object.assign({
method: 'GET',
nocache: true,
timeout: 5000,
loader: true,
callback: null
}, options);
opts.method = opts.method.toUpperCase();
var loaderFn = opts.loader ? chibi._loader : function(){};
var url = opts.url;
var method = opts.method;
var query = null;
if (opts.data) {
query = serializeData(opts.data);
}
if (query && (method === 'GET')) {
url += (url.indexOf('?') === -1) ? '?' + query : '&' + query;
query = null;
}
var xhr = new XMLHttpRequest();
// prevent caching
if (opts.nocache) {
var ts = (+(new Date())).toString(36);
url += ((url.indexOf('?') === -1) ? '?' : '&') + '_=' + ts;
}
loaderFn(true);
xhr.open(method, url, true);
xhr.timeout = opts.timeout;
// Abort after given timeout
var abortTmeo = setTimeout(function () {
console.error("XHR timed out.");
xhr.abort();
loaderFn(false);
}, opts.timeout + 10);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
loaderFn(false);
opts.callback && opts.callback(xhr.responseText, xhr.status);
clearTimeout(abortTmeo);
}
};
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
if (method === 'POST') {
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
}
xhr.send(query);
return xhr;
};
chibi._loader = function(){};
// Alias to cb.ajax(url, 'get', callback)
chibi.get = function (url, callback, opts) {
opts = opts || {};
opts.url = url;
opts.callback = callback;
opts.method = 'GET';
return chibi.ajax(opts);
};
// Alias to cb.ajax(url, 'post', callback)
chibi.post = function (url, callback, opts) {
opts = opts || {};
opts.url = url;
opts.callback = callback;
opts.method = 'POST';
return chibi.ajax(opts);
};
// Fire on DOM ready
chibi.ready = function (fn) {
if (fn) {
if (domready) {
fn();
return chibi;
} else {
readyfn.push(fn);
}
}
};
// Fire on page loaded
chibi.loaded = function (fn) {
if (fn) {
if (pageloaded) {
fn();
return chibi;
} else {
loadedfn.push(fn);
}
}
};
var entityMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'/': '&#x2F;',
'`': '&#x60;',
'=': '&#x3D;'
};
chibi.htmlEscape = function(string) {
return String(string).replace(/[&<>"'`=\/]/g, function (s) {
return entityMap[s];
});
};
// Set Chibi's global namespace here ($)
w.$ = chibi;
}());
+311
View File
@@ -0,0 +1,311 @@
// keymaster.js
// (c) 2011-2013 Thomas Fuchs
// keymaster.js may be freely distributed under the MIT license.
;(function(global){
var k,
_handlers = {},
_mods = { 16: false, 18: false, 17: false, 91: false },
_scope = 'all',
// modifier keys
_MODIFIERS = {
'⇧': 16, shift: 16,
'⌥': 18, alt: 18, option: 18,
'⌃': 17, ctrl: 17, control: 17,
'⌘': 91, command: 91
},
// special keys
_MAP = {
backspace: 8, tab: 9, clear: 12,
enter: 13, 'return': 13,
esc: 27, escape: 27, space: 32,
left: 37, up: 38,
right: 39, down: 40,
del: 46, 'delete': 46,
home: 36, end: 35,
pageup: 33, pagedown: 34,
',': 188, '.': 190, '/': 191,
'`': 192, '-': 189, '=': 187,
';': 186, '\'': 222,
'[': 219, ']': 221, '\\': 220,
// added:
insert: 45,
np_0: 96, np_1: 97, np_2: 98, np_3: 99, np_4: 100, np_5: 101,
np_6: 102, np_7: 103, np_8: 104, np_9: 105, np_mul: 106,
np_add: 107, np_sub: 109, np_point: 110, np_div: 111, numlock: 144,
},
code = function(x){
return _MAP[x] || x.toUpperCase().charCodeAt(0);
},
_downKeys = [];
for(k=1;k<20;k++) _MAP['f'+k] = 111+k;
// IE doesn't support Array#indexOf, so have a simple replacement
function index(array, item){
var i = array.length;
while(i--) if(array[i]===item) return i;
return -1;
}
// for comparing mods before unassignment
function compareArray(a1, a2) {
if (a1.length != a2.length) return false;
for (var i = 0; i < a1.length; i++) {
if (a1[i] !== a2[i]) return false;
}
return true;
}
var modifierMap = {
16:'shiftKey',
18:'altKey',
17:'ctrlKey',
91:'metaKey'
};
function updateModifierKey(event) {
for(k in _mods) _mods[k] = event[modifierMap[k]];
};
function isModifierPressed(mod) {
if (mod=='control'||mod=='ctrl') return _mods[17];
if (mod=='shift') return _mods[16];
if (mod=='meta') return _mods[91];
if (mod=='alt') return _mods[18];
return false;
}
// handle keydown event
function dispatch(event) {
var key, handler, k, i, modifiersMatch, scope;
key = event.keyCode;
if (index(_downKeys, key) == -1) {
_downKeys.push(key);
}
// if a modifier key, set the key.<modifierkeyname> property to true and return
if(key == 93 || key == 224) key = 91; // right command on webkit, command on Gecko
if(key in _mods) {
_mods[key] = true;
// 'assignKey' from inside this closure is exported to window.key
for(k in _MODIFIERS) if(_MODIFIERS[k] == key) assignKey[k] = true;
return;
}
updateModifierKey(event);
// see if we need to ignore the keypress (filter() can can be overridden)
// by default ignore key presses if a select, textarea, or input is focused
if(!assignKey.filter.call(this, event)) return;
// abort if no potentially matching shortcuts found
if (!(key in _handlers)) return;
scope = getScope();
// for each potential shortcut
for (i = 0; i < _handlers[key].length; i++) {
handler = _handlers[key][i];
// see if it's in the current scope
if(handler.scope == scope || handler.scope == 'all'){
// check if modifiers match if any
modifiersMatch = handler.mods.length > 0;
for(k in _mods)
if((!_mods[k] && index(handler.mods, +k) > -1) ||
(_mods[k] && index(handler.mods, +k) == -1)) modifiersMatch = false;
// call the handler and stop the event if neccessary
if((handler.mods.length == 0 && !_mods[16] && !_mods[18] && !_mods[17] && !_mods[91]) || modifiersMatch){
if(handler.method(event, handler)===false){
if(event.preventDefault) event.preventDefault();
else event.returnValue = false;
if(event.stopPropagation) event.stopPropagation();
if(event.cancelBubble) event.cancelBubble = true;
}
}
}
}
};
// unset modifier keys on keyup
function clearModifier(event){
var key = event.keyCode, k,
i = index(_downKeys, key);
// remove key from _downKeys
if (i >= 0) {
_downKeys.splice(i, 1);
}
if(key == 93 || key == 224) key = 91;
if(key in _mods) {
_mods[key] = false;
for(k in _MODIFIERS) if(_MODIFIERS[k] == key) assignKey[k] = false;
}
};
function resetModifiers() {
for(k in _mods) _mods[k] = false;
for(k in _MODIFIERS) assignKey[k] = false;
};
// parse and assign shortcut
function assignKey(key, scope, method){
var keys, mods;
keys = getKeys(key);
if (method === undefined) {
method = scope;
scope = 'all';
}
// for each shortcut
for (var i = 0; i < keys.length; i++) {
// set modifier keys if any
mods = [];
key = keys[i].split('+');
if (key.length > 1){
mods = getMods(key);
key = [key[key.length-1]];
}
// convert to keycode and...
key = key[0]
key = code(key);
// ...store handler
if (!(key in _handlers)) _handlers[key] = [];
_handlers[key].push({ shortcut: keys[i], scope: scope, method: method, key: keys[i], mods: mods });
}
};
// unbind all handlers for given key in current scope
function unbindKey(key, scope) {
var multipleKeys, keys,
mods = [],
i, j, obj;
multipleKeys = getKeys(key);
for (j = 0; j < multipleKeys.length; j++) {
keys = multipleKeys[j].split('+');
if (keys.length > 1) {
mods = getMods(keys);
}
key = keys[keys.length - 1];
key = code(key);
if (scope === undefined) {
scope = getScope();
}
if (!_handlers[key]) {
return;
}
for (i = 0; i < _handlers[key].length; i++) {
obj = _handlers[key][i];
// only clear handlers if correct scope and mods match
if (obj.scope === scope && compareArray(obj.mods, mods)) {
_handlers[key][i] = {};
}
}
}
};
// Returns true if the key with code 'keyCode' is currently down
// Converts strings into key codes.
function isPressed(keyCode) {
if (typeof(keyCode)=='string') {
keyCode = code(keyCode);
}
return index(_downKeys, keyCode) != -1;
}
function getPressedKeyCodes() {
return _downKeys.slice(0);
}
function filter(event){
var tagName = (event.target || event.srcElement).tagName;
// ignore keypressed in any elements that support keyboard data input
return !(tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA');
}
// initialize key.<modifier> to false
for(k in _MODIFIERS) assignKey[k] = false;
// set current scope (default 'all')
function setScope(scope){ _scope = scope || 'all' };
function getScope(){ return _scope || 'all' };
// delete all handlers for a given scope
function deleteScope(scope){
var key, handlers, i;
for (key in _handlers) {
handlers = _handlers[key];
for (i = 0; i < handlers.length; ) {
if (handlers[i].scope === scope) handlers.splice(i, 1);
else i++;
}
}
};
// abstract key logic for assign and unassign
function getKeys(key) {
var keys;
key = key.replace(/\s/g, '');
keys = key.split(',');
if ((keys[keys.length - 1]) == '') {
keys[keys.length - 2] += ',';
}
return keys;
}
// abstract mods logic for assign and unassign
function getMods(key) {
var mods = key.slice(0, key.length - 1);
for (var mi = 0; mi < mods.length; mi++)
mods[mi] = _MODIFIERS[mods[mi]];
return mods;
}
// cross-browser events
function addEvent(object, event, method) {
if (object.addEventListener)
object.addEventListener(event, method, false);
else if(object.attachEvent)
object.attachEvent('on'+event, function(){ method(window.event) });
};
// set the handlers globally on document
addEvent(document, 'keydown', function(event) { dispatch(event) }); // Passing _scope to a callback to ensure it remains the same by execution. Fixes #48
addEvent(document, 'keyup', clearModifier);
// reset modifiers to false whenever the window is (re)focused.
addEvent(window, 'focus', resetModifiers);
// store previously defined key
var previousKey = global.key;
// restore previously defined key and return reference to our key object
function noConflict() {
var k = global.key;
global.key = previousKey;
return k;
}
// set window.key and window.key.set/get/deleteScope, and the default filter
global.key = assignKey;
global.key.setScope = setScope;
global.key.getScope = getScope;
global.key.deleteScope = deleteScope;
global.key.filter = filter;
global.key.isPressed = isPressed;
global.key.isModifier = isModifierPressed;
global.key.getPressedKeyCodes = getPressedKeyCodes;
global.key.noConflict = noConflict;
global.key.unbind = unbindKey;
if(typeof module !== 'undefined') module.exports = assignKey;
})(window);
+63
View File
@@ -0,0 +1,63 @@
/*! http://mths.be/fromcodepoint v0.1.0 by @mathias */
if (!String.fromCodePoint) {
(function () {
var defineProperty = (function () {
// IE 8 only supports `Object.defineProperty` on DOM elements
try {
var object = {};
var $defineProperty = Object.defineProperty;
var result = $defineProperty(object, object, object) && $defineProperty;
} catch (error) {
}
return result;
}());
var stringFromCharCode = String.fromCharCode;
var floor = Math.floor;
var fromCodePoint = function () {
var MAX_SIZE = 0x4000;
var codeUnits = [];
var highSurrogate;
var lowSurrogate;
var index = -1;
var length = arguments.length;
if (!length) {
return '';
}
var result = '';
while (++index < length) {
var codePoint = Number(arguments[index]);
if (
!isFinite(codePoint) || // `NaN`, `+Infinity`, or `-Infinity`
codePoint < 0 || // not a valid Unicode code point
codePoint > 0x10FFFF || // not a valid Unicode code point
floor(codePoint) != codePoint // not an integer
) {
throw RangeError('Invalid code point: ' + codePoint);
}
if (codePoint <= 0xFFFF) { // BMP code point
codeUnits.push(codePoint);
} else { // Astral code point; split in surrogate halves
// http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
codePoint -= 0x10000;
highSurrogate = (codePoint >> 10) + 0xD800;
lowSurrogate = (codePoint % 0x400) + 0xDC00;
codeUnits.push(highSurrogate, lowSurrogate);
}
if (index + 1 == length || codeUnits.length > MAX_SIZE) {
result += stringFromCharCode.apply(null, codeUnits);
codeUnits.length = 0;
}
}
return result;
};
if (defineProperty) {
defineProperty(String, 'fromCodePoint', {
'value': fromCodePoint,
'configurable': true,
'writable': true
});
} else {
String.fromCodePoint = fromCodePoint;
}
}());
}
+44
View File
@@ -0,0 +1,44 @@
/** Module for toggling a modal overlay */
(function () {
let modal = {}
let curCloseCb = null
modal.show = function (sel, closeCb) {
let $m = $(sel)
$m.removeClass('hidden visible')
setTimeout(function () {
$m.addClass('visible')
}, 1)
curCloseCb = closeCb
}
modal.hide = function (sel) {
let $m = $(sel)
$m.removeClass('visible')
setTimeout(function () {
$m.addClass('hidden')
if (curCloseCb) curCloseCb()
}, 500) // transition time
}
modal.init = function () {
// close modal by click outside the dialog
$('.Modal').on('click', function () {
if ($(this).hasClass('no-close')) return // this is a no-close modal
modal.hide(this)
})
$('.Dialog').on('click', function (e) {
e.stopImmediatePropagation()
})
// Hide all modals on esc
$(window).on('keydown', function (e) {
if (e.which === 27) {
modal.hide('.Modal')
}
})
}
window.Modal = modal
})()
+35
View File
@@ -0,0 +1,35 @@
window.Notify = (function () {
let nt = {}
const sel = '#notif'
let hideTmeo1 // timeout to start hiding (transition)
let hideTmeo2 // timeout to add the hidden class
nt.show = function (message, timeout) {
$(sel).html(message)
Modal.show(sel)
clearTimeout(hideTmeo1)
clearTimeout(hideTmeo2)
if (undef(timeout)) timeout = 2500
hideTmeo1 = setTimeout(nt.hide, timeout)
}
nt.hide = function () {
let $m = $(sel)
$m.removeClass('visible')
hideTmeo2 = setTimeout(function () {
$m.addClass('hidden')
}, 250) // transition time
}
nt.init = function () {
$(sel).on('click', function () {
nt.hide(this)
})
}
return nt
})()
+108
View File
@@ -0,0 +1,108 @@
window.initSoftKeyboard = function (screen, input) {
const keyInput = qs('#softkb-input')
if (!keyInput) return // abort, we're not on the terminal page
let keyboardOpen = false
let updateInputPosition = function () {
if (!keyboardOpen) return
let [x, y] = screen.gridToScreen(screen.cursor.x, screen.cursor.y, true)
keyInput.style.transform = `translate(${x}px, ${y}px)`
}
keyInput.addEventListener('focus', () => {
keyboardOpen = true
updateInputPosition()
})
keyInput.addEventListener('blur', () => (keyboardOpen = false))
screen.on('cursor-moved', updateInputPosition)
window.kbOpen = function openSoftKeyboard (open) {
keyboardOpen = open
updateInputPosition()
if (open) keyInput.focus()
else keyInput.blur()
}
// Chrome for Android doesn't send proper keydown/keypress events with
// real key values instead of 229 “Unidentified,” so here's a workaround
// that deals with the input composition events.
let lastCompositionString = ''
let compositing = false
// sends the difference between the last and the new composition string
let sendInputDelta = function (newValue) {
let resend = false
if (newValue.length > lastCompositionString.length) {
if (newValue.startsWith(lastCompositionString)) {
// characters have been added at the end
input.sendString(newValue.substr(lastCompositionString.length))
} else resend = true
} else if (newValue.length < lastCompositionString.length) {
if (lastCompositionString.startsWith(newValue)) {
// characters have been removed at the end
input.sendString('\b'.repeat(lastCompositionString.length -
newValue.length))
} else resend = true
} else if (newValue !== lastCompositionString) resend = true
if (resend) {
// the entire string changed; resend everything
input.sendString('\b'.repeat(lastCompositionString.length) +
newValue)
}
lastCompositionString = newValue
}
keyInput.addEventListener('keydown', e => {
if (e.key === 'Unidentified') return
keyInput.value = ''
if (e.key === 'Backspace') {
e.preventDefault()
input.sendString('\b')
} else if (e.key === 'Enter') {
e.preventDefault()
input.sendString('\x0d')
}
})
keyInput.addEventListener('keypress', e => {
// prevent key duplication on iOS (because Safari *does* send proper events)
e.stopPropagation()
})
keyInput.addEventListener('input', e => {
e.stopPropagation()
if (e.isComposing) {
sendInputDelta(e.data)
} else {
if (e.inputType === 'insertCompositionText') input.sendString(e.data)
else if (e.inputType === 'deleteContentBackward') {
lastCompositionString = ''
sendInputDelta('')
} else if (e.inputType === 'insertText') {
input.sendString(e.data)
}
}
})
keyInput.addEventListener('compositionstart', e => {
lastCompositionString = ''
compositing = true
})
keyInput.addEventListener('compositionend', e => {
lastCompositionString = ''
compositing = false
keyInput.value = ''
})
screen.on('open-soft-keyboard', () => keyInput.focus())
}
+1144
View File
File diff suppressed because it is too large Load Diff
+48
View File
@@ -0,0 +1,48 @@
/** Init the terminal sub-module - called from HTML */
window.termInit = function (labels, theme) {
const screen = new window.TermScreen()
const conn = window.Conn(screen)
const input = window.Input(conn)
const termUpload = window.TermUpl(conn, input)
screen.input = input
conn.init()
input.init()
termUpload.init()
qs('#screen').appendChild(screen.canvas)
screen.load(labels, theme) // load labels and theme
{
let fitScreen = false
let fitScreenIfNeeded = function fitScreenIfNeeded () {
screen.window.fitIntoWidth = fitScreen ? window.innerWidth - 20 : 0
screen.window.fitIntoHeight = fitScreen ? window.innerHeight : 0
}
fitScreenIfNeeded()
window.addEventListener('resize', fitScreenIfNeeded)
window.toggleFitScreen = function () {
fitScreen = !fitScreen
const resizeButtonIcon = qs('#resize-button-icon')
if (fitScreen) {
resizeButtonIcon.classList.remove('icn-resize-small')
resizeButtonIcon.classList.add('icn-resize-full')
} else {
resizeButtonIcon.classList.remove('icn-resize-full')
resizeButtonIcon.classList.add('icn-resize-small')
}
fitScreenIfNeeded()
}
}
window.initSoftKeyboard(screen, input)
if (window.attachDebugScreen) window.attachDebugScreen(screen)
// for debugging
window.termScreen = screen
window.conn = conn
window.input = input
window.termUpl = termUpload
}
+137
View File
@@ -0,0 +1,137 @@
/** Handle connections */
window.Conn = function (screen) {
let ws
let heartbeatTout
let pingIv
let xoff = false
let autoXoffTout
let reconTout
let pageShown = false
function onOpen (evt) {
console.log('CONNECTED')
heartbeat()
doSend('i')
}
function onClose (evt) {
console.warn('SOCKET CLOSED, code ' + evt.code + '. Reconnecting...')
clearTimeout(reconTout)
reconTout = setTimeout(function () {
init()
}, 2000)
// this happens when the buffer gets fucked up via invalid unicode.
// we basically use polling instead of socket then
}
function onMessage (evt) {
try {
// . = heartbeat
switch (evt.data.charAt(0)) {
case '.':
// heartbeat, no-op message
break
case '-':
// console.log('xoff');
xoff = true
autoXoffTout = setTimeout(function () {
xoff = false
}, 250)
break
case '+':
// console.log('xon');
xoff = false
clearTimeout(autoXoffTout)
break
default:
screen.load(evt.data)
if (!pageShown) {
showPage()
pageShown = true
}
break
}
heartbeat()
} catch (e) {
console.error(e)
}
}
function canSend () {
return !xoff
}
function doSend (message) {
if (_demo) {
console.log('TX: ', message)
return true // Simulate success
}
if (xoff) {
// TODO queue
console.log("Can't send, flood control.")
return false
}
if (!ws) return false // for dry testing
if (ws.readyState !== 1) {
console.error('Socket not ready')
return false
}
if (typeof message != 'string') {
message = JSON.stringify(message)
}
ws.send(message)
return true
}
function init () {
if (window._demo) {
console.log('Demo mode!')
screen.load(_demo_screen)
showPage()
return
}
clearTimeout(reconTout)
clearTimeout(heartbeatTout)
ws = new WebSocket('ws://' + _root + '/term/update.ws')
ws.onopen = onOpen
ws.onclose = onClose
ws.onmessage = onMessage
console.log('Opening socket.')
heartbeat()
}
function heartbeat () {
clearTimeout(heartbeatTout)
heartbeatTout = setTimeout(heartbeatFail, 2000)
}
function heartbeatFail () {
console.error('Heartbeat lost, probing server...')
pingIv = setInterval(function () {
console.log('> ping')
$.get('http://' + _root + '/system/ping', function (resp, status) {
if (status === 200) {
clearInterval(pingIv)
console.info('Server ready, reloading page...')
location.reload()
}
}, {
timeout: 100
})
}, 1000)
}
return {
ws: null,
init: init,
send: doSend,
canSend: canSend // check flood control
}
}
+296
View File
@@ -0,0 +1,296 @@
/**
* User input
*
* --- Rx messages: ---
* S - screen content (binary encoding of the entire screen with simple compression)
* T - text labels - Title and buttons, \0x01-separated
* B - beep
* . - heartbeat
*
* --- Tx messages ---
* s - string
* b - action button
* p - mb press
* r - mb release
* m - mouse move
*/
window.Input = function (conn) {
let opts = {
np_alt: false,
cu_alt: false,
fn_alt: false,
mt_click: false,
mt_move: false,
no_keys: false,
crlf_mode: false
}
/** Send a literal message */
function sendStrMsg (str) {
return conn.send('s' + str)
}
/** Send a button event */
function sendBtnMsg (n) {
conn.send('b' + Chr(n))
}
/** Fn alt choice for key message */
function fa (alt, normal) {
return opts.fn_alt ? alt : normal
}
/** Cursor alt choice for key message */
function ca (alt, normal) {
return opts.cu_alt ? alt : normal
}
/** Numpad alt choice for key message */
function na (alt, normal) {
return opts.np_alt ? alt : normal
}
function _bindFnKeys () {
const keymap = {
'tab': '\x09',
'backspace': '\x08',
'enter': opts.crlf_mode ? '\x0d\x0a' : '\x0d',
'ctrl+enter': '\x0a',
'esc': '\x1b',
'up': ca('\x1bOA', '\x1b[A'),
'down': ca('\x1bOB', '\x1b[B'),
'right': ca('\x1bOC', '\x1b[C'),
'left': ca('\x1bOD', '\x1b[D'),
'home': ca('\x1bOH', fa('\x1b[H', '\x1b[1~')),
'insert': '\x1b[2~',
'delete': '\x1b[3~',
'end': ca('\x1bOF', fa('\x1b[F', '\x1b[4~')),
'pageup': '\x1b[5~',
'pagedown': '\x1b[6~',
'f1': fa('\x1bOP', '\x1b[11~'),
'f2': fa('\x1bOQ', '\x1b[12~'),
'f3': fa('\x1bOR', '\x1b[13~'),
'f4': fa('\x1bOS', '\x1b[14~'),
'f5': '\x1b[15~', // note the disconnect
'f6': '\x1b[17~',
'f7': '\x1b[18~',
'f8': '\x1b[19~',
'f9': '\x1b[20~',
'f10': '\x1b[21~', // note the disconnect
'f11': '\x1b[23~',
'f12': '\x1b[24~',
'shift+f1': fa('\x1bO1;2P', '\x1b[25~'),
'shift+f2': fa('\x1bO1;2Q', '\x1b[26~'), // note the disconnect
'shift+f3': fa('\x1bO1;2R', '\x1b[28~'),
'shift+f4': fa('\x1bO1;2S', '\x1b[29~'), // note the disconnect
'shift+f5': fa('\x1b[15;2~', '\x1b[31~'),
'shift+f6': fa('\x1b[17;2~', '\x1b[32~'),
'shift+f7': fa('\x1b[18;2~', '\x1b[33~'),
'shift+f8': fa('\x1b[19;2~', '\x1b[34~'),
'shift+f9': fa('\x1b[20;2~', '\x1b[35~'), // 35-38 are not standard - but what is?
'shift+f10': fa('\x1b[21;2~', '\x1b[36~'),
'shift+f11': fa('\x1b[22;2~', '\x1b[37~'),
'shift+f12': fa('\x1b[23;2~', '\x1b[38~'),
'np_0': na('\x1bOp', '0'),
'np_1': na('\x1bOq', '1'),
'np_2': na('\x1bOr', '2'),
'np_3': na('\x1bOs', '3'),
'np_4': na('\x1bOt', '4'),
'np_5': na('\x1bOu', '5'),
'np_6': na('\x1bOv', '6'),
'np_7': na('\x1bOw', '7'),
'np_8': na('\x1bOx', '8'),
'np_9': na('\x1bOy', '9'),
'np_mul': na('\x1bOR', '*'),
'np_add': na('\x1bOl', '+'),
'np_sub': na('\x1bOS', '-'),
'np_point': na('\x1bOn', '.'),
'np_div': na('\x1bOQ', '/')
// we don't implement numlock key (should change in numpad_alt mode, but it's even more useless than the rest)
}
for (let k in keymap) {
if (keymap.hasOwnProperty(k)) {
bind(k, keymap[k])
}
}
}
/** Bind a keystroke to message */
function bind (combo, str) {
// mac fix - allow also cmd
if (combo.indexOf('ctrl+') !== -1) {
combo += ',' + combo.replace('ctrl', 'command')
}
// unbind possible old binding
key.unbind(combo)
key(combo, function (e) {
if (opts.no_keys) return
e.preventDefault()
sendStrMsg(str)
})
}
/** Bind/rebind key messages */
function _initKeys () {
// This takes care of text characters typed
window.addEventListener('keypress', function (evt) {
if (opts.no_keys) return
let str = ''
if (evt.key) str = evt.key
else if (evt.which) str = String.fromCodePoint(evt.which)
if (str.length > 0 && str.charCodeAt(0) >= 32) {
// console.log("Typed ", str);
// prevent space from scrolling
if (evt.which === 32) evt.preventDefault()
sendStrMsg(str)
}
})
// ctrl-letter codes are sent as simple low ASCII codes
for (let i = 1; i <= 26; i++) {
bind('ctrl+' + String.fromCharCode(96 + i), String.fromCharCode(i))
}
/* eslint-disable */
bind('ctrl+]', '\x1b') // alternate way to enter ESC
bind('ctrl+\\', '\x1c')
bind('ctrl+[', '\x1d')
bind('ctrl+^', '\x1e')
bind('ctrl+_', '\x1f')
// extra ctrl-
bind('ctrl+left', '\x1f[1;5D')
bind('ctrl+right', '\x1f[1;5C')
bind('ctrl+up', '\x1f[1;5A')
bind('ctrl+down', '\x1f[1;5B')
bind('ctrl+home', '\x1f[1;5H')
bind('ctrl+end', '\x1f[1;5F')
// extra shift-
bind('shift+left', '\x1f[1;2D')
bind('shift+right', '\x1f[1;2C')
bind('shift+up', '\x1f[1;2A')
bind('shift+down', '\x1f[1;2B')
bind('shift+home', '\x1f[1;2H')
bind('shift+end', '\x1f[1;2F')
// macOS editing commands
bind('⌥+left', '\x1fb') // ⌥← to go back a word (^[b)
bind('⌥+right', '\x1ff') // ⌥→ to go forward one word (^[f)
bind('⌘+left', '\x01') // ⌘← to go to the beginning of a line (^A)
bind('⌘+right', '\x05') // ⌘→ to go to the end of a line (^E)
bind('⌥+backspace', '\x17') // ⌥⌫ to delete a word (^W, I think)
bind('⌘+backspace', '\x15') // ⌘⌫ to delete to the beginning of a line (possibly ^U)
/* eslint-enable */
_bindFnKeys()
}
// mouse button states
let mb1 = 0
let mb2 = 0
let mb3 = 0
/** Init the Input module */
function init () {
_initKeys()
// Button presses
$('#action-buttons button').forEach(function (s) {
s.addEventListener('click', function () {
sendBtnMsg(+this.dataset['n'])
})
})
// global mouse state tracking - for motion reporting
window.addEventListener('mousedown', function (evt) {
if (evt.button === 0) mb1 = 1
if (evt.button === 1) mb2 = 1
if (evt.button === 2) mb3 = 1
})
window.addEventListener('mouseup', function (evt) {
if (evt.button === 0) mb1 = 0
if (evt.button === 1) mb2 = 0
if (evt.button === 2) mb3 = 0
})
}
/** Prepare modifiers byte for mouse message */
function packModifiersForMouse () {
return (key.isModifier('ctrl') ? 1 : 0) |
(key.isModifier('shift') ? 2 : 0) |
(key.isModifier('alt') ? 4 : 0) |
(key.isModifier('meta') ? 8 : 0)
}
return {
/** Init the Input module */
init: init,
/** Send a literal string message */
sendString: sendStrMsg,
/** Enable alternate key modes (cursors, numpad, fn) */
setAlts: function (cu, np, fn, crlf) {
if (opts.cu_alt !== cu || opts.np_alt !== np || opts.fn_alt !== fn || opts.crlf_mode !== crlf) {
opts.cu_alt = cu
opts.np_alt = np
opts.fn_alt = fn
opts.crlf_mode = crlf
// rebind keys - codes have changed
_bindFnKeys()
}
},
setMouseMode: function (click, move) {
opts.mt_click = click
opts.mt_move = move
},
// Mouse events
onMouseMove: function (x, y) {
if (!opts.mt_move) return
const b = mb1 ? 1 : mb2 ? 2 : mb3 ? 3 : 0
const m = packModifiersForMouse()
conn.send('m' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m))
},
onMouseDown: function (x, y, b) {
if (!opts.mt_click) return
if (b > 3 || b < 1) return
const m = packModifiersForMouse()
conn.send('p' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m))
// console.log("B ",b," M ",m);
},
onMouseUp: function (x, y, b) {
if (!opts.mt_click) return
if (b > 3 || b < 1) return
const m = packModifiersForMouse()
conn.send('r' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m))
// console.log("B ",b," M ",m);
},
onMouseWheel: function (x, y, dir) {
if (!opts.mt_click) return
// -1 ... btn 4 (away from user)
// +1 ... btn 5 (towards user)
const m = packModifiersForMouse()
const b = (dir < 0 ? 4 : 5)
conn.send('p' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m))
// console.log("B ",b," M ",m);
},
mouseTracksClicks: function () {
return opts.mt_click
},
blockKeys: function (yes) {
opts.no_keys = yes
}
}
}
+1352
View File
File diff suppressed because it is too large Load Diff
+146
View File
@@ -0,0 +1,146 @@
/** File upload utility */
window.TermUpl = function (conn, input) {
let lines, // array of lines without newlines
line_i, // current line index
fuTout, // timeout handle for line sending
send_delay_ms, // delay between lines (ms)
nl_str, // newline string to use
curLine, // current line (when using fuOil)
inline_pos // Offset in line (for long lines)
// lines longer than this are split to chunks
// sending a super-ling string through the socket is not a good idea
const MAX_LINE_LEN = 128
function fuOpen () {
fuStatus('Ready...')
Modal.show('#fu_modal', onClose)
$('#fu_form').toggleClass('busy', false)
input.blockKeys(true)
}
function onClose () {
console.log('Upload modal closed.')
clearTimeout(fuTout)
line_i = 0
input.blockKeys(false)
}
function fuStatus (msg) {
qs('#fu_prog').textContent = msg
}
function fuSend () {
let v = qs('#fu_text').value
if (!v.length) {
fuClose()
return
}
lines = v.split('\n')
line_i = 0
inline_pos = 0 // offset in line
send_delay_ms = qs('#fu_delay').value
// sanitize - 0 causes overflows
if (send_delay_ms < 0) {
send_delay_ms = 0
qs('#fu_delay').value = send_delay_ms
}
nl_str = {
'CR': '\r',
'LF': '\n',
'CRLF': '\r\n'
}[qs('#fu_crlf').value]
$('#fu_form').toggleClass('busy', true)
fuStatus('Starting...')
fuSendLine()
}
function fuSendLine () {
if (!$('#fu_modal').hasClass('visible')) {
// Modal is closed, cancel
return
}
if (!conn.canSend()) {
// postpone
fuTout = setTimeout(fuSendLine, 1)
return
}
if (inline_pos === 0) {
curLine = lines[line_i++] + nl_str
}
let chunk
if ((curLine.length - inline_pos) <= MAX_LINE_LEN) {
chunk = curLine.substr(inline_pos, MAX_LINE_LEN)
inline_pos = 0
} else {
chunk = curLine.substr(inline_pos, MAX_LINE_LEN)
inline_pos += MAX_LINE_LEN
}
if (!input.sendString(chunk)) {
fuStatus('FAILED!')
return
}
let all = lines.length
fuStatus(line_i + ' / ' + all + ' (' + (Math.round((line_i / all) * 1000) / 10) + '%)')
if (lines.length > line_i || inline_pos > 0) {
fuTout = setTimeout(fuSendLine, send_delay_ms)
} else {
closeWhenReady()
}
}
function closeWhenReady () {
if (!conn.canSend()) {
// stuck in XOFF still, wait to process...
fuStatus('Waiting for Tx buffer...')
setTimeout(closeWhenReady, 100)
} else {
fuStatus('Done.')
// delay to show it
setTimeout(function () {
fuClose()
}, 100)
}
}
function fuClose () {
Modal.hide('#fu_modal')
}
return {
init: function () {
qs('#fu_file').addEventListener('change', function (evt) {
let reader = new FileReader()
let file = evt.target.files[0]
console.log('Selected file type: ' + file.type)
if (!file.type.match(/text\/.*|application\/(json|csv|.*xml.*|.*script.*)/)) {
// Deny load of blobs like img - can crash browser and will get corrupted anyway
if (!confirm('This does not look like a text file: ' + file.type + '\nReally load?')) {
qs('#fu_file').value = ''
return
}
}
reader.onload = function (e) {
const txt = e.target.result.replace(/[\r\n]+/, '\n')
qs('#fu_text').value = txt
}
console.log('Loading file...')
reader.readAsText(file)
}, false)
},
close: fuClose,
start: fuSend,
open: fuOpen
}
}
Executable
+90
View File
@@ -0,0 +1,90 @@
/** Make a node */
function mk (e) {
return document.createElement(e)
}
/** Find one by query */
function qs (s) {
return document.querySelector(s)
}
/** Find all by query */
function qsa (s) {
return document.querySelectorAll(s)
}
/** Convert any to bool safely */
function bool (x) {
return (x === 1 || x === '1' || x === true || x === 'true')
}
/**
* Filter 'spacebar' and 'return' from keypress handler,
* and when they're pressed, fire the callback.
* use $(...).on('keypress', cr(handler))
*/
function cr (hdl) {
return function (e) {
if (e.which === 10 || e.which === 13 || e.which === 32) {
hdl()
}
}
}
/** HTML escape */
function esc (str) {
return $.htmlEscape(str)
}
/** Check for undefined */
function undef (x) {
return typeof x == 'undefined'
}
/** Safe json parse */
function jsp (str) {
try {
return JSON.parse(str)
} catch (e) {
console.error(e)
return null
}
}
/** Create a character from ASCII code */
function Chr (n) {
return String.fromCharCode(n)
}
/** Decode number from 2B encoding */
function parse2B (s, i = 0) {
return (s.charCodeAt(i++) - 1) + (s.charCodeAt(i) - 1) * 127
}
/** Decode number from 3B encoding */
function parse3B (s, i = 0) {
return (s.charCodeAt(i) - 1) + (s.charCodeAt(i + 1) - 1) * 127 + (s.charCodeAt(i + 2) - 1) * 127 * 127
}
/** Encode using 2B encoding, returns string. */
function encode2B (n) {
let lsb, msb
lsb = (n % 127)
n = ((n - lsb) / 127)
lsb += 1
msb = (n + 1)
return Chr(lsb) + Chr(msb)
}
/** Encode using 3B encoding, returns string. */
function encode3B (n) {
let lsb, msb, xsb
lsb = (n % 127)
n = (n - lsb) / 127
lsb += 1
msb = (n % 127)
n = (n - msb) / 127
msb += 1
xsb = (n + 1)
return Chr(lsb) + Chr(msb) + Chr(xsb)
}
+163
View File
@@ -0,0 +1,163 @@
(function (w) {
const authStr = ['Open', 'WEP', 'WPA', 'WPA2', 'WPA/WPA2']
let curSSID
// Get XX % for a slider input
function rangePt (inp) {
return Math.round(((inp.value / inp.max) * 100)) + '%'
}
// Display selected STA SSID etc
function selectSta (name, password, ip) {
$('#sta_ssid').val(name)
$('#sta_password').val(password)
$('#sta-nw').toggleClass('hidden', name.length === 0)
$('#sta-nw-nil').toggleClass('hidden', name.length > 0)
$('#sta-nw .essid').html(esc(name))
const nopw = undef(password) || password.length === 0
$('#sta-nw .passwd').toggleClass('hidden', nopw)
$('#sta-nw .nopasswd').toggleClass('hidden', !nopw)
$('#sta-nw .ip').html(ip.length > 0 ? tr('wifi.connected_ip_is') + ip : tr('wifi.not_conn'))
}
/** Update display for received response */
function onScan (resp, status) {
// var ap_json = {
// "result": {
// "inProgress": "0",
// "APs": [
// {"essid": "Chlivek", "bssid": "88:f7:c7:52:b3:99", "rssi": "204", "enc": "4", "channel": "1"},
// {"essid": "TyNikdy", "bssid": "5c:f4:ab:0d:f1:1b", "rssi": "164", "enc": "3", "channel": "1"},
// ]
// }
// };
if (status !== 200) {
// bad response
rescan(5000) // wait 5sm then retry
return
}
try {
resp = JSON.parse(resp)
} catch (e) {
console.log(e)
rescan(5000)
return
}
const done = !bool(resp.result.inProgress) && (resp.result.APs.length > 0)
rescan(done ? 15000 : 1000)
if (!done) return // no redraw yet
// clear the AP list
let $list = $('#ap-list')
// remove old APs
$('#ap-list .AP').remove()
$list.toggleClass('hidden', !done)
$('#ap-loader').toggleClass('hidden', done)
// scan done
resp.result.APs.sort(function (a, b) {
return b.rssi - a.rssi
}).forEach(function (ap) {
ap.enc = parseInt(ap.enc)
if (ap.enc > 4) return // hide unsupported auths
let item = mk('div')
let $item = $(item)
.data('ssid', ap.essid)
.data('pwd', ap.enc)
.attr('tabindex', 0)
.addClass('AP')
// mark current SSID
if (ap.essid === curSSID) {
$item.addClass('selected')
}
let inner = mk('div')
let escapedSSID = $.htmlEscape(ap.essid)
$(inner).addClass('inner')
.htmlAppend(`<div class="rssi">${ap.rssi_perc}</div>`)
.htmlAppend(`<div class="essid" title="${escapedSSID}">${escapedSSID}</div>`)
.htmlAppend(`<div class="auth">${authStr[ap.enc]}</div>`)
$item.on('click', function () {
let $th = $(this)
const conn_ssid = $th.data('ssid')
let conn_pass = ''
if (+$th.data('pwd')) {
// this AP needs a password
conn_pass = prompt(tr('wifi.enter_passwd').replace(':ssid:', conn_ssid))
if (!conn_pass) return
}
$('#sta_password').val(conn_pass)
$('#sta_ssid').val(conn_ssid)
selectSta(conn_ssid, conn_pass, '')
})
item.appendChild(inner)
$list[0].appendChild(item)
})
}
function startScanning () {
$('#ap-loader').removeClass('hidden')
$('#ap-scan').addClass('hidden')
$('#ap-loader .anim-dots').html('.')
scanAPs()
}
/** Ask the CGI what APs are visible (async) */
function scanAPs () {
if (_demo) {
onScan(_demo_aps, 200)
} else {
$.get('http://' + _root + '/cfg/wifi/scan', onScan)
}
}
function rescan (time) {
setTimeout(scanAPs, time)
}
/** Set up the WiFi page */
function wifiInit (cfg) {
// Update slider value displays
$('.Row.range').forEach(function (x) {
let inp = x.querySelector('input')
let disp1 = x.querySelector('.x-disp1')
let disp2 = x.querySelector('.x-disp2')
let t = rangePt(inp)
$(disp1).html(t)
$(disp2).html(t)
$(inp).on('input', function () {
t = rangePt(inp)
$(disp1).html(t)
$(disp2).html(t)
})
})
// Forget STA credentials
$('#forget-sta').on('click', function () {
selectSta('', '', '')
return false
})
selectSta(cfg.sta_ssid, cfg.sta_password, cfg.sta_active_ip)
curSSID = cfg.sta_active_ssid
}
w.init = wifiInit
w.startScanning = startScanning
})(window.WiFi = {})