Restructuring
This commit is contained in:
+130
@@ -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)
|
||||
}
|
||||
})
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 + '?' }
|
||||
Executable
+703
@@ -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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/',
|
||||
'`': '`',
|
||||
'=': '='
|
||||
};
|
||||
|
||||
chibi.htmlEscape = function(string) {
|
||||
return String(string).replace(/[&<>"'`=\/]/g, function (s) {
|
||||
return entityMap[s];
|
||||
});
|
||||
};
|
||||
|
||||
// Set Chibi's global namespace here ($)
|
||||
w.$ = chibi;
|
||||
}());
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
})()
|
||||
@@ -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())
|
||||
}
|
||||
Vendored
+1144
File diff suppressed because it is too large
Load Diff
+48
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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 = {})
|
||||
Reference in New Issue
Block a user