OVMS3/OVMS.V3/components/ovms_webserver/assets/ovms.js

1999 lines
65 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ovms.js | (c) Michael Balzer | https://github.com/openvehicles/Open-Vehicle-Monitoring-System-3 */
const monthnames = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
const supportsTouch = 'ontouchstart' in window || navigator.msMaxTouchPoints;
if (window.loggedin == undefined)
window.loggedin = false;
/**
* Utilities
*/
function after(seconds, fn) {
return window.setTimeout(fn, seconds*1000);
}
function now() {
return Math.floor((new Date()).getTime() / 1000);
}
function encode_html(s) {
return String(s)
.replace(/&/g, '&')
.replace(/'/g, ''')
.replace(/"/g, '"')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
function unwrapLogLine(s) {
return String(s)
.replace(/(\S)\|+(.)/g, "$1\n……: $2");
}
function fix_minheight($el) {
if ($el.css("resize") != "none") return;
var mh = parseInt($el.css("max-height")), h = $el.outerHeight();
$el.css("min-height", mh ? Math.min(h, mh) : h);
}
function getPathURL(path) {
// TODO: use actual http.server config
if (path.startsWith('/sd/'))
return path.substr(3);
else
return '';
}
/**
* Hexadecimal encoding & decoding
* based on: https://stackoverflow.com/a/54099484 by Aaron Watters
*/
var HEX = {
to_hex_array: [],
to_byte_map: {},
// Init lookup tables:
init: function () {
for (var ord=0; ord<=0xff; ord++) {
var s = ord.toString(16);
if (s.length < 2) {
s = "0" + s;
}
this.to_hex_array.push(s);
this.to_byte_map[s] = ord;
}
},
// Encode ArrayBuffer to hexadecimal string:
// Usage example: HEX.encode(CBOR.encode({a: 42})) = "a16161182a"
encode: function (arraybuffer) {
if (!this.to_hex_array.length) this.init();
var buffer = new Uint8Array(arraybuffer);
var hex_array = [];
for (var i=0; i<buffer.length; i++) {
hex_array.push(this.to_hex_array[buffer[i]]);
}
return hex_array.join('');
},
// Decode hexadecimal string into ArrayBuffer:
// Usage example: CBOR.decode(HEX.decode("a16161182a")) = {a: 42}
decode: function (s) {
if (!this.to_hex_array.length) this.init();
var length2 = s.length;
if ((length2 % 2) != 0) {
console.error("HEX.decode: string must have length a multiple of 2");
return null;
}
var length = length2 / 2;
var buffer = new Uint8Array(length);
for (var i=0; i<length; i++) {
var i2 = i * 2;
var b = s.substring(i2, i2 + 2);
buffer[i] = this.to_byte_map[b];
}
return buffer.buffer;
}
};
/**
* AJAX Pages & Commands
*/
var page = {
uri: null,
path: null,
search: null,
params: {}
};
function setPage(uri) {
page.uri = uri;
var uriparts = uri.split("?");
page.path = uriparts[0];
page.search = uriparts[1];
page.params = {};
if (page.search) {
page.search.split("&").map(function(kv) {
var v = kv.split("=");
page.params[decodeURIComponent(v[0])] = (v[1] != null) ? decodeURIComponent(v[1]) : true;
});
}
if (page.params["nm"] == 1) $("body").addClass("night");
else if (page.params["nm"] == 0) $("body").removeClass("night");
}
function updateLocation(reload) {
page.search = '';
for (v in page.params)
page.search += "&" + encodeURIComponent(v) + "=" + encodeURIComponent(page.params[v]);
page.search = page.search.slice(1);
var hash = "#" + page.path + (page.search ? "?" + page.search : "");
if (!reload) $("#main").data("uri", hash.substr(1));
location.hash = hash;
}
function readLocation() {
var uri = location.hash.substr(1);
if (!uri.match("^/?[a-zA-Z0-9_]"))
uri = "/home";
return uri;
}
function loadPage(uri) {
if (typeof uri != "string")
uri = readLocation();
if ($("#main").data("uri") != uri) {
loaduri("#main", "get", uri, {});
}
}
function reloadpage() {
var uri = readLocation();
loaduri("#main", "get", uri, {});
}
function reloadmenu() {
$("#menu").load("/menu");
}
function login(dsturi) {
if (!dsturi)
dsturi = page.uri || "/home";
loadPage("/login?uri=" + encodeURIComponent(dsturi));
}
function logout() {
loadPage("/logout");
}
function xhrErrorInfo(request, textStatus, errorThrown) {
var txt = "";
if (request.status == 401 || request.status == 403)
txt = "Unauthorized. <a class=\"btn btn-sm btn-default\" href=\"javascript:login()\">Login</a>";
else if (request.status >= 400)
txt = "Error " + request.status + " " + request.statusText;
else if (textStatus)
txt = "Request " + textStatus + ", please retry";
else if (errorThrown)
txt = errorThrown;
return txt;
}
function setcontent(tgt, uri, text){
if (!tgt || !tgt.length) return;
tgt.find(".receiver").unsubscribe();
tgt.chart("destroy");
tgt.table("destroy");
if (tgt[0].id == "main") {
$("#nav .dropdown.open .dropdown-toggle").dropdown("toggle");
$("#nav .navbar-collapse").collapse("hide");
$("#nav li").removeClass("active");
var mi = $("#nav [href='"+uri+"']");
mi.parents("li").addClass("active");
if (tgt[0].id == "main")
window.scrollTo(0,0);
else
tgt[0].scrollIntoView();
tgt.html(text);
var $p = tgt.find(">.panel");
if ($p.length == 1) $p.addClass("panel-single");
var moduleid = $("title").data("moduleid") || "OVMS";
if (mi.length > 0)
document.title = moduleid + " " + (mi.attr("title") || mi.text());
else
document.title = moduleid + " Console";
} else {
if (tgt[0].id == "main")
window.scrollTo(0,0);
else
tgt[0].scrollIntoView();
tgt.html(text);
}
tgt.find(".get-window-resize").trigger('window-resize');
tgt.find(".receiver").subscribe().trigger("msg:metrics", metrics);
tgt.trigger("load");
}
function loaduri(target, method, uri, data){
var tgt = $(target), cont;
if (tgt.length != 1)
return false;
tgt.data("uri", uri);
if (tgt[0].id == "main") {
cont = $("html");
location.hash = "#" + uri;
setPage(uri);
} else {
cont = tgt.closest(".modal") || tgt.closest("form") || tgt;
}
$.ajax({ "type": method, "url": uri, "data": data,
"timeout": 15000,
"beforeSend": function(){
cont.addClass("loading");
},
"complete": function(){
cont.removeClass("loading");
},
"success": function(response){
setcontent(tgt, uri, response);
},
"error": function(response, xhrerror, httperror){
var text = response.responseText || httperror+"\n" || xhrerror+"\n";
if (text.search("alert") == -1) {
text = '<div id="alert" class="alert alert-danger alert-dismissable">'
+ '<a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>'
+ '<strong>' + text + '</strong>'
+ '</div>';
}
setcontent(tgt, uri, text);
},
});
return true;
}
$.fn.loaduri = function(method, uri, data) {
return this.each(function() {
loaduri(this, method, uri, data);
});
};
function standardTextFilter(msg) {
if (msg.error)
return '<div class="bg-danger">'+msg.error+'</div>';
else
return $('<div/>').text(msg.text).html();
}
function loadjs(command, target, filter, timeout) {
if (!command) return null;
var args = {};
if (typeof command == "object") {
args = command;
} else {
args.command = command;
}
args.type = "js";
return loadcmd(args, target, filter, timeout);
}
function loadcmd(command, target, filter, timeout) {
var $output, outmode = "", args = {};
if (!command) return null;
if (typeof command == "object") {
args = command;
} else {
args.command = command;
}
if (typeof filter == "number") {
timeout = filter; filter = null;
}
if (typeof target == "function") {
filter = target; target = null;
}
if (target == null) {
$output = $(null);
}
else if (typeof target == "object") {
$output = target;
}
else if (target.startsWith("+")) {
outmode = "+";
$output = $(target.substr(1));
} else {
$output = $(target);
}
if (!filter)
filter = standardTextFilter;
if (!timeout) {
if (args["type"] != "js" && /^(test |ota |copen .* scan)/.test(args.command))
timeout = 300;
else
timeout = 20;
}
var lastlen = 0, xhr, timeouthd;
var checkabort = function() {
if (xhr.readyState != 4)
xhr.abort("timeout");
};
var add_output = function(addhtml) {
if (addhtml == null || !$output.length) {
outmode = "+";
return;
}
if (outmode == "") { $output.empty(); outmode = "+"; }
var autoscroll = ($output.get(0).scrollTop + $output.innerHeight()) >= $output.get(0).scrollHeight;
$output.append(addhtml);
$output.closest('.get-window-resize').trigger('window-resize');
if (autoscroll) $output.scrollTop($output.get(0).scrollHeight);
};
xhr = $.ajax({ "type": "post", "url": "/api/execute", "data": args,
"timeout": 0,
"beforeSend": function() {
if ($output.length) {
$output.addClass("loading");
fix_minheight($output);
}
timeouthd = window.setTimeout(checkabort, timeout*1000);
},
"complete": function() {
window.clearTimeout(timeouthd);
if ($output.length) {
$output.removeClass("loading");
fix_minheight($output);
}
},
"xhrFields": {
onprogress: function(ev) {
var request = ev.currentTarget;
if (request.status != 200) return;
var addtext = request.response.substring(lastlen);
lastlen = request.response.length;
add_output(filter({ "request": request, "text": addtext }));
window.clearTimeout(timeouthd);
timeouthd = window.setTimeout(checkabort, timeout*1000);
},
},
"success": function(response, textStatus, request) {
var addtext = response.substring(lastlen);
add_output(filter({ "request": request, "text": addtext }));
},
"error": function(request, textStatus, errorThrown) {
var cmdinfo = (args.command.length > 200) ? args.command.substr(0,200)+"[…]" : args.command;
console.log("loadcmd '" + cmdinfo + "' ERROR: status=" + textStatus + ", httperror=" + errorThrown);
var txt = xhrErrorInfo(request, textStatus, errorThrown);
add_output(filter({ "request": request, "error": txt }));
},
});
return xhr;
}
$.fn.loadcmd = function(command, filter, timeout) {
return this.each(function() {
loadcmd(command, $(this), filter, timeout);
});
};
/**
* WebSocket Connection
*/
var monitorTimer, last_monotonic = 0;
var ws, ws_inhibit = 0;
var metrics = {};
var units = { metrics: {}, prefs: {} };
mi_to_km = function(mi) { return mi * 1.609347; }
km_to_mi = function(km) { return km * 0.6213700; }
pkm_to_pmi = function(pkm) { return pkm * 1.609347; }
pmi_to_pkm = function(pmi) { return pmi * 0.6213700; }
no_conversion = function (value) { return value;}
x_to_kx = function (value) { return value/1000; }
kx_to_x = function (value) { return value*1000; }
const feet_per_mile = 5280;
var unit_conversions = {
"native": no_conversion,
"km>miles": km_to_mi,
"km>meters": kx_to_x,
"km>feet": function (value) { return km_to_mi(value) * feet_per_mile; },
"miles>km": mi_to_km,
"miles>meters": function (value) { return (mi_to_km(value)*1000); },
"miles>feet": function (value) { return value * feet_per_mile; },
"meters>miles": function (value) { return km_to_mi(value/1000); },
"meters>km": x_to_kx,
"meters>feet": function (value) { return km_to_mi(value/1000) * feet_per_mile; },
"feet>km": function (value) { return mi_to_km(value/feet_per_mile); },
"feet>meters": function (value) { return (mi_to_km(value/feet_per_mile)*1000); },
"feet>miles": function (value) { return value / feet_per_mile; },
"kmphps>miphps": km_to_mi,
"kmphps>mpss": function (value) { return value/3.6; },
"kmphps>ftpss": function (value) { return km_to_mi(value)*feet_per_mile/3600; },
"miphps>kmphps": mi_to_km,
"miphps>mpss": function (value) { return mi_to_km(value)/3.6; },
"miphps>ftpss": function (value) { return value*feet_per_mile/3600; },
"mpss>kmphps": function (value) { return (value*3.6); },
"mpss>miphps": function (value) { return km_to_mi(value)*3.6; },
"mpss>ftpss": function (value) { return km_to_mi(value)*feet_per_mile; },
"ftpss>kmphps": function (value) { return (mi_to_km(value/feet_per_mile)*3.6); },
"ftpss>miphps": function (value) { return value *3600/feet_per_mile; },
"ftpss>mpss": function (value) { return mi_to_km(value/feet_per_mile)*1000; },
"kw>watts": kx_to_x,
"watts>kw": x_to_kx,
"kwh>watthours": kx_to_x,
"watthours>kwh": x_to_kx,
"whpkm>whpmi": pkm_to_pmi,
"whpkm>kwhp100km": function (value) { return value / 10; },
"whpkm>kmpkwh": function (value) { return value ? 1000.0 / value : 0; },
"whpkm>mipkwh": function (value) { return value ? (km_to_mi(1000.0 / value)) : 0; },
"whpmi>whpkm": pmi_to_pkm,
"whpmi>kwhp100km": function (value) { return pmi_to_pkm(value) / 10; },
"whpmi>kmpkwh": function (value) { return value ? (mi_to_km(1000.0 / value)) : 0; },
"whpmi>mipkwh": function (value) { return value ? (1000.0 / value) : 0; },
"kwhp100km>whpmi": function (value) { return pkm_to_pmi(value * 10); },
"kwhp100km>whpkm": function (value) { return value * 10; },
"kwhp100km>kmpkwh": function (value) { return value ? (100.0 / value) : 0; },
"kwhp100km>mipkwh": function (value) { return value ? km_to_mi(100.0 / value) : 0; },
"kmpkwh>whpmi": function (value) { return value ? (1000.0 / km_to_mi(value)) : 0;},
"kmpkwh>whpkm": function (value) { return value ? (1/(1000.0 * value)) : 0;},
"kmpkwh>kwhp100km": function (value) { return value ? (100.0/value) : 0;},
"kmpkwh>mipkwh": km_to_mi,
"mipkwh>whpmi": function (value) { return value ? 1000/value : 0;},
"mipkwh>whpkm": function (value) { return value ? (1000 / mi_to_km(value)) : 0;},
"mipkwh>kwhp100km": function (value) { return value ? (100.0/mi_to_km(value)) : 0;},
"mipkwh>kmpkwh": mi_to_km,
"celcius>fahrenheit": function (value) { return ((value*9)/5) + 32; },
"fahrenheit>celcius": function (value) { return ((value-32)*5)/9; },
"kpa>pa": kx_to_x,
"kpa>bar": function (value) { return value/100; },
"kpa>psi": function (value) { return value * 0.14503773773020923; },
"pa>kpa": x_to_kx,
"pa>bar": function (value) { return value/100000; },
"pa>psi": function (value) { return value * 0.00014503773773020923; },
"psi>kpa": function (value) { return value * 6.894757293168361; },
"psi>pa": function (value) { return value * 6894.757293168361; },
"psi>bar": function (value) { return value * 0.06894757293168361; },
"bar>pa": function (value) { return value*100000; },
"bar>kpa": function (value) { return value*100; },
"bar>psi": function (value) { return value * 14.503773773020923; },
"seconds>minutes": function (value) { return value/60; },
"seconds>hours": function (value) { return value/3600; },
"minutes>seconds": function (value) { return value*60; },
"minutes>hours": function (value) { return value/60; },
"hours>seconds": function (value) { return value*3600; },
"hours>minutes": function (value) { return value*60; },
"kmph>miph": km_to_mi,
"miph>kmph": mi_to_km,
"dbm>sq": function (value) { return Math.round((value <= -51) ? ((value + 113)/2) : 0); },
"sq>dbm": function (value) { return Math.round((value <= 31) ? (-113 + (value*2)) : 0); },
"percent>permille": function (value) { return value*10.0; },
"permille>percent": function (value) { return value*0.10; }
}
convertUnitFunction = function (from, to) {
return unit_conversions[from + ">" + to] || no_conversion;
}
convertUnits = function (from, to, value) {
return convertUnitFunction(from, to)(value);
}
units.convertMetricToUserUnits = function (value, name) {
if (value == undefined)
return value
var unit_entry = this.metrics[name];
if (unit_entry == undefined)
return value
var cnvfn = unit_entry.user_fn;
if (cnvfn == undefined) {
cnvfn = convertUnitFunction(unit_entry.native, unit_entry.code);
this.metrics[name].user_fn = cnvfn;
}
return cnvfn(value);
}
units.userUnitLabelFromMetric = function (name) {
var unit_entry = this.metrics[name];
if (unit_entry == undefined)
return "";
return unit_entry.label;
}
units.unitLabelToUser = function (unitType, defaultLabel) {
var res = this.prefs[unitType];
return (res && res.label) ? res.label : defaultLabel
}
units.unitValueToUser = function (unitType, value) {
var entry = this.prefs[unitType];
if (!entry)
return value;
return convertUnits(value, unitType, entry.unit);
}
// Works for units and metrics collection.
metricsProxyHas = function(target, name) {
return target[name] != undefined
}
var metrics_all = new Proxy(metrics, {
get: function(target, name) {
if (name == Symbol.toStringTag)
return 'metrics_all[]';
if (!(typeof name === "string" || name instanceof String))
return undefined;
var names = name.split('#',2);
var name = names[0];
var value_type = names[1]
if (value_type === "unit")
return units.userUnitLabelFromMetric(name);
var value = target[name];
if (value_type === "label")
value = units.convertMetricToUserUnits(value, name)
return value;
},
has: metricsProxyHas
});
var metrics_user = new Proxy(metrics, {
get:
function(target, name) {
if (name == Symbol.toStringTag)
return 'metrics_user[]';
return units.convertMetricToUserUnits(target[name], name)
},
has: metricsProxyHas
});
var metrics_label = new Proxy(units.metrics, {
get:
function(target, name) {
if (name == Symbol.toStringTag)
return 'metrics_label[]';
var unit_entry = target[name];
if (unit_entry == undefined) {
return "";
}
return unit_entry.label;
},
has: metricsProxyHas
});
var shellhist = [""], shellhpos = 0;
var loghist = [];
const loghist_maxsize = 100;
function initSocketConnection(){
if (location.protocol == "https:") {
ws = new WebSocket('wss://' + location.host + '/msg');
} else {
ws = new WebSocket('ws://' + location.host + '/msg');
}
ws.onopen = function(ev) {
console.log("WebSocket OPENED", ev);
$(".receiver").subscribe();
subscribeToTopic("units/#");
};
ws.onerror = function(ev) { console.log("WebSocket ERROR", ev); };
ws.onclose = function(ev) { console.log("WebSocket CLOSED", ev); };
ws.onmessage = function(ev) {
var msg;
try {
msg = JSON.parse(ev.data);
} catch (e) {
console.error("WebSocket msg: " + e + ": " + ev.data);
return;
}
for (msgtype in msg) {
if (msgtype == "event") {
$(".receiver").trigger("msg:event", msg.event);
$(".monitor[data-events]").each(function(){
var cmd = $(this).data("updcmd");
var js = $(this).data("updjs");
var evf = $(this).data("events");
if ((cmd || js) && evf && msg.event.match(evf)) {
$(this).data("updlast", now());
loadcmd({ command: js ? js : cmd, type: js ? "js" : "cmd" }, $(this));
}
});
}
else if (msgtype == "metrics") {
$.extend(metrics, msg.metrics);
$(".receiver").trigger("msg:metrics", msg.metrics);
}
else if (msgtype == "units") {
for (var subtype in msg.units) {
if (subtype == "metrics") {
$.extend(units.metrics, msg.units.metrics);
$(".receiver").trigger("msg:units:metrics", msg.units.metrics);
var msgmetrics = {};
for (metricname in msg.units.metrics)
msgmetrics[metricname] = metrics[metricname];
$(".receiver").trigger("msg:metrics", msgmetrics);
} else if (subtype == "prefs") {
$.extend(units.prefs, msg.units.prefs);
$(".receiver").trigger("msg:units:prefs", msg.units.prefs);
}
}
}
else if (msgtype == "notify") {
processNotification(msg.notify);
$(".receiver").trigger("msg:notify", msg.notify);
}
else if (msgtype == "log") {
loghist.push(msg.log);
if (loghist.length > loghist_maxsize) loghist.shift();
$(".receiver").trigger("msg:log", msg.log);
}
}
};
}
function monitorInit(force){
$(".monitor").each(function(){
var cmd = $(this).data("updcmd");
var js = $(this).data("updjs");
var txt = $(this).text();
if ((cmd || js) && (force || !txt)) {
$(this).data("updlast", now());
loadcmd({ command: js ? js : cmd, type: js ? "js" : "cmd" }, $(this));
}
});
}
function monitorUpdate(){
if (!ws || ws.readyState == ws.CLOSED){
if (ws_inhibit != 0)
--ws_inhibit;
if (ws_inhibit == 0)
initSocketConnection();
}
var new_monotonic = parseInt(metrics["m.monotonic"]) || 0;
if (new_monotonic < last_monotonic)
location.reload();
else
last_monotonic = new_monotonic;
$(".monitor").each(function(){
var cnt = $(this).data("updcnt");
var int = $(this).data("updint");
var last = $(this).data("updlast");
var cmd = $(this).data("updcmd");
var js = $(this).data("updjs");
if (!cnt || (!cmd && !js) || (now()-last) < int)
return;
$(this).data("updcnt", cnt-1);
$(this).data("updlast", now());
loadcmd({ command: js ? js : cmd, type: js ? "js" : "cmd" }, $(this));
});
}
function processNotification(msg) {
var opts = { timeout: 0 };
if (msg.type == "info") {
opts.title = '<span class="lead text-info"><i>ⓘ</i> ' + msg.subtype + ' Info</span>';
opts.timeout = 60;
}
else if (msg.type == "alert") {
opts.title = '<span class="lead text-danger"><i>⚠</i> ' + msg.subtype + ' Alert</span>';
}
else if (msg.type == "error") {
opts.title = '<span class="lead text-warning"><i>⛍</i> ' + msg.subtype + ' Error</span>';
}
else
return;
opts.body = '<pre>' + msg.value + '</pre>';
confirmdialog(opts.title, opts.body, ["OK"], opts.timeout);
}
subscribeToTopic = function (topic) {
try {
console.debug("subscribe " + topic);
if (ws) ws.send("subscribe " + topic);
} catch (e) {
console.log(e);
}
}
$.fn.subscribe = function(topics) {
return this.each(function() {
var subscriptions = $(this).data("subscriptions");
if (!topics) {
// init from data attr:
topics = subscriptions;
subscriptions = "";
}
var subs = subscriptions ? subscriptions.split(' ') : [];
var tops = topics ? topics.split(' ') : [];
for (var i = 0; i < tops.length; i++) {
if (tops[i] && !subs.includes(tops[i])) {
subscribeToTopic(tops[i]);
}
}
$(this).data("subscriptions", subs.join(' '));
});
};
$.fn.unsubscribe = function(topics) {
return this.each(function() {
var subscriptions = $(this).data("subscriptions");
if (!topics) {
// cleanup:
topics = subscriptions;
}
var subs = subscriptions ? subscriptions.split(' ') : [];
var tops = topics ? topics.split(' ') : [];
var i, j;
for (i = 0; i < tops.length; i++) {
if (tops[i] && (j = subs.indexOf(tops[i])) >= 0) {
try {
console.log("unsubscribe " + tops[i]);
if (ws) ws.send("unsubscribe " + tops[i]);
subs.splice(j, 1);
} catch (e) {
console.log(e);
}
}
}
$(this).data("subscriptions", subs.join(' '));
});
};
$.fn.reconnectTicker = function(msg) {
$("html").addClass("loading disabled");
ws_inhibit = 10;
if (ws) ws.close();
window.setInterval(function(){
if (ws && ws.readyState == ws.OPEN)
location.reload();
else
$("#reconnectTickerDots").append("•");
}, 1000);
return this.append(
(msg ? msg : '<p class="lead">Rebooting now…</p>') +
'<p>The window will automatically reload when the browser reconnects to the module.</p>' +
'<p id="reconnectTickerDots">•</p>');
};
/**
* UI Widgets
*/
// Plugin Maker
// credits: https://www.bitovi.com/blog/writing-the-perfect-jquery-plugin
$.pluginMaker = function(plugin) {
$.fn[plugin.prototype.cname] = function(options) {
var args = $.makeArray(arguments), after = args.slice(1);
var ismethod = (typeof options == "string");
var result;
this.each(function() {
// see if we have an instance
var instance = $.data(this, plugin.prototype.cname);
if (instance) {
if (ismethod) {
// call a method on the instance
if ($.isFunction(instance[options]))
result = instance[options].apply(instance, after);
else
throw "UndefinedMethod: " + plugin.prototype.cname + "." + options;
} else if (instance.update) {
// call update on the instance
instance.update.apply(instance, args);
}
} else {
// create the plugin
new plugin(this, options);
}
});
return (ismethod && result != undefined) ? result : this;
};
};
// OVMS namespace:
var ovms = {uid:0};
// Widget root class:
ovms.Widget = function(el, options) {
if (el) this.init(el, options);
}
$.extend(ovms.Widget.prototype, {
cname: "widget",
options: {},
init: function(el, options) {
this.uid = ++ovms.uid;
this.$el = $(el);
this.$el.data(this.cname, this);
this.$el.addClass(this.cname);
var dataoptions = this.$el.data("options");
if (dataoptions != null && typeof dataoptions != "object") {
console.error("Invalid JSON syntax: " + this.$el.data("options"));
dataoptions = null;
}
this.options = $.extend({}, this.options, dataoptions, options);
},
update: function(options) {
$.extend(this.options, options);
},
});
// Dialog:
ovms.Dialog = function(el, options) {
if (el) this.init(el, options);
}
$.extend(ovms.Dialog.prototype, ovms.Widget.prototype, {
cname: "dialog",
options: {
title: '',
body: '',
show: false,
remote: false,
backdrop: true,
keyboard: true,
transition: 'fade',
size: '',
contentClass: '',
onShow: null,
onHide: null,
onShown: null,
onHidden: null,
onUpdate: null,
buttons: [{}],
timeout: 0,
input: null,
},
init: function(el, options) {
if ($(el).parent().length == 0) {
options = $.extend(options, { show: true, isDynamic: true });
}
ovms.Widget.prototype.init.call(this, el, options);
this.input = options.input ? options.input : {};
this.data = { showing: false };
this.$buttons = [];
// convert element to modal if not predefined by user:
if (this.$el.children().length == 0) {
this.$el.html('<div class="modal-dialog"><div class="modal-content"><div class="modal-header"><button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button><h4 class="modal-title"></h4></div><div class="modal-body"></div><div class="modal-footer"></div></div></div></div>');
this.$el.addClass("modal");
}
this.$el.on('show.bs.modal', $.proxy(this.onShow, this));
this.$el.on('hide.bs.modal', $.proxy(this.onHide, this));
this.$el.on('shown.bs.modal', $.proxy(this.onShown, this));
this.$el.on('hidden.bs.modal', $.proxy(this.onHidden, this));
if (this.options.isDynamic) this.$el.appendTo('body');
this.update(this.options);
},
update: function(options) {
$.extend(this.options, options);
// configure modal:
this.$el.removeClass('fade').addClass(this.options.transition);
this.$el.find('.modal-dialog').attr("class", "modal-dialog " + (this.options.size ? ("modal-" + this.options.size) : ""));
this.$el.find('.modal-content').attr("class", "modal-content " + (this.options.contentClass || ""));
if (options.title != null)
this.$el.find('.modal-title').html(options.title);
if (options.body != null)
this.$el.find('.modal-body').html(options.body);
if (options.buttons != null) {
var footer = this.$el.find('.modal-footer');
footer.empty();
this.$buttons = [];
for (var i = 0; i < this.options.buttons.length; i++) {
var btn = $.extend({ label: 'Close', btnClass: 'default', autoHide: true, action: null }, this.options.buttons[i], { index: i });
this.options.buttons[i] = btn;
this.$buttons[i] = $('<button type="button" class="btn btn-'+btn.btnClass+'" value="'+btn.index+'">'+btn.label+'</button>')
.appendTo(footer).on('click', $.proxy(this.onClick, this, btn));
}
}
if (this.options.onUpdate)
this.options.onUpdate.call(this.$el, this.input);
this.$el.modal(this.options);
},
show: function(options) {
if (options) this.update(options);
this.$el.modal('show');
},
hide: function() {
this.$el.modal('hide');
},
onShow: function() {
if (this.options.onShow)
this.options.onShow.call(this.$el, this.input);
},
onHide: function() {
if (this.options.onHide)
this.options.onHide.call(this.$el, this.input);
},
onShown: function() {
this.data.showing = true;
this.input.button = null;
if (!supportsTouch) this.$el.find('.form-control, .btn').first().focus();
if (this.options.onShown)
this.options.onShown.call(this.$el, this.input);
if (this.options.timeout)
after(this.options.timeout, $.proxy(this.hide, this));
},
onHidden: function() {
this.data.showing = false;
if (this.options.isDynamic)
this.$el.detach();
if (this.options.onHidden)
this.options.onHidden.call(this.$el, this.input);
if (this.input.button != null && this.input.button.action) {
this.input.button.action.call(this.$el, this.input);
}
},
isShowing: function() {
return this.data.showing;
},
triggerButton: function(button) {
var btn;
if (typeof button == "number") {
btn = this.$buttons[button];
}
else if (typeof button == "string") {
for (var i = 0; i < this.options.buttons.length; i++) {
if (this.options.buttons[i].label == button) {
btn = this.$buttons[i];
break;
}
}
}
if (btn && !btn.prop("disabled")) {
btn.trigger('click');
return true;
}
return false;
},
onClick: function(button) {
this.input.button = button;
if (button.autoHide)
this.hide();
else if (button.action) {
button.action.call(this.$el, this.input);
this.input.button = null;
}
},
});
$.pluginMaker(ovms.Dialog);
// Dialog utility wrappers:
$.fn.confirmdialog = function(_title, _body, _buttons, _action, _timeout) {
if (typeof _action == "number") { _timeout = _action; _action = null; }
var options = { show: true, title: _title, body: _body, buttons: [], timeout: _timeout };
if (_buttons) {
for (var i = 0; i < _buttons.length; i++) {
options.buttons.push({ label: _buttons[i],
btnClass: (_buttons.length <= 2 && i==_buttons.length-1) ? "primary" : "default" });
}
}
if (_action) {
options.onHidden = function(input) { _action(input.button ? input.button.index : null); };
}
return this.dialog(options);
};
var confirmdialog = function() { return $.fn.confirmdialog.apply($('<div />'), arguments); };
$.fn.promptdialog = function(_type, _title, _prompt, _buttons, _action) {
var options = { show: true, title: _title, buttons: [] };
var uid = ++ovms.uid;
options.body = '<label for="prompt'+uid+'">'+_prompt+'</label><input id="prompt'+uid+'" type="'+_type+'" class="form-control">';
if (_buttons) {
for (var i = 0; i < _buttons.length; i++) {
options.buttons.push({ label: _buttons[i],
btnClass: (_buttons.length <= 2 && i==_buttons.length-1) ? "primary" : "default" });
}
}
options.onUpdate = function(input) {
var footer = $(this).find('.modal-footer');
$(this).find('input').val(input.text).on('keydown', function(ev) {
if (ev.which == 13) footer.find('.btn-primary').trigger('click');
});
};
options.onShown = function(input) {
$(this).find('input').first().focus();
}
options.onHidden = function(input) {
if (input.button) input.text = $(this).find('input').val();
if (_action) _action(input.button ? input.button.index : null, input.text);
};
return this.dialog(options);
};
var promptdialog = function() { return $.fn.promptdialog.apply($('<div />'), arguments); };
// FileBrowser:
ovms.FileBrowser = function(el, options) {
if (el) this.init(el, options);
}
$.extend(ovms.FileBrowser.prototype, ovms.Widget.prototype, {
cname: "filebrowser",
options: {
path: '',
quicknav: ['/sd/', '/store/'],
filter: null,
sortBy: null,
sortDir: 1,
onUpdate: null,
onPathChange: null,
onAction: null,
input: null,
},
init: function(el, options) {
ovms.Widget.prototype.init.call(this, el, options);
this.input = options.input ? options.input : {};
$.extend(this.input, {
path: '',
dir: '',
file: '',
noload: false,
});
this.data = {
lastpath: '',
lastdir: '',
listdir: '',
listsel: '',
};
this.xhr = null;
if (this.$el.children().length == 0) {
this.$el.html('<div class="fb-pathbox form-group"><label class="control-label" for="input-path-${uid}">Path (trailing slash = dir):</label><div class="input-group"><input type="text" class="form-control font-monospace" name="path" id="input-path-${uid}" value=""><div class="input-group-btn"><button type="button" class="btn btn-default fb-path-stop" disabled title="Stop">&times;</button><button type="button" class="btn btn-default fb-path-reload" title="Reload">⟲</button><button type="button" class="btn btn-default fb-path-up" title="Up">↰</button></div></div></div><div class="fb-quicknav form-group"/><div class="fb-files"><table class="table table-condensed table-hover table-scrollable font-monospace get-window-resize"><thead><tr><th class="col-xs-3 col-sm-2" data-key="size">Size</th><th class="hidden-xs col-sm-4" data-key="date">Date</th><th class="col-xs-9 col-sm-6" data-key="name">Name</th></tr></thead><tbody/></table></div>'.replace(/\$\{uid\}/g, this.uid));
}
this.$pathinput = this.$el.find("input[name=path]");
this.$pathinput.on('change', $.proxy(this.setPath, this)).on('keydown', $.proxy(function(ev) {
if (ev.which == 13) {
var lastdir = this.input.dir;
this.setPath();
if (this.input.file || this.input.dir == lastdir)
this.onAction();
ev.preventDefault();
}
else if (ev.which == 27) {
this.stopLoad();
}
}, this));
this.$el.find('.fb-path-up').on('click', $.proxy(function(ev) {
var parent = this.input.dir.replace(/\/[^/]+$/, '');
if (parent) this.setPath(parent+'/');
else this.setPath(this.input.dir+'/');
}, this));
this.$el.find('.fb-path-reload').on('click', $.proxy(function(ev) {
this.setPath(this.input.path, true);
}, this));
this.$btnstop = this.$el.find('.fb-path-stop');
this.$btnstop.on('click', $.proxy(function(ev) { this.stopLoad(); }, this));
this.$quicknav = this.$el.find(".fb-quicknav");
this.$filetable = this.$el.find(".fb-files>table");
this.$filecols = this.$filetable.find("thead th");
this.$filecols.on('click', $.proxy(function(ev) {
var by = $(ev.currentTarget).data("key");
var dir = $(ev.currentTarget).hasClass('sort-down') ? -1 : 1;
if (by == this.options.sortBy) dir = -dir;
this.sortList(by, dir);
if (!supportsTouch) this.$pathinput.focus();
}, this));
this.$filebody = this.$filetable.find("tbody");
this.$filebody.on('click', 'tr', $.proxy(function(ev) {
this.data.listsel = $(ev.currentTarget).data("name");
this.setPath(this.data.listdir + "/" + this.data.listsel);
ev.preventDefault();
}, this)).on('dblclick', 'tr', $.proxy(this.onAction, this));
this.update(this.options);
},
update: function(options) {
$.extend(this.options, options);
this.$quicknav.empty();
for (var i = 0; i < this.options.quicknav.length; i++) {
this.$quicknav.append('<button type="button" class="btn btn-sm btn-default"><code>' + this.options.quicknav[i] + '</code></button>');
}
this.$quicknav.find("button").on('click', $.proxy(function(ev) {
this.setPath($(ev.delegateTarget).text());
}, this));
if (this.options.onUpdate)
this.options.onUpdate.call(this.$el, this.input);
this.sortList(this.options.sortBy, this.options.sortDir);
if (options.path !== undefined) {
this.setPath(options.path);
}
else if (options.filter !== undefined) {
this.loadDir();
}
},
getInput: function() {
return this.input;
},
setPath: function(newpath, reload) {
if (typeof newpath == "string") {
this.input.path = newpath;
this.$pathinput.val(newpath);
} else {
this.input.path = this.$pathinput.val();
}
this.input.dir = this.input.path.replace(/\/[^/]*$/, '');
this.input.file = this.input.path.substr(this.input.dir.length+1);
var fn = "";
if (this.input.dir == this.data.listdir)
fn = this.input.path.substr(this.data.listdir.length+1);
this.$filebody.children().each(function() {
if ($(this).data("name") === fn) $(this).addClass("active");
else $(this).removeClass("active");
});
if (reload)
this.data.lastdir = '';
if (this.input.path != this.data.lastpath) {
this.input.noload = false;
if (this.options.onPathChange)
this.options.onPathChange.call(this.$el, this.input);
this.data.lastpath = this.input.path;
}
if (this.input.dir != this.data.lastdir) {
if (this.input.noload) {
this.stopLoad();
this.data.lastdir = '';
this.$filebody.empty();
this.data.listdir = '';
this.data.listsel = '';
} else {
this.loadDir();
this.data.lastdir = this.input.dir;
}
}
if (!supportsTouch) this.$pathinput.focus();
},
loadDir: function() {
var self = this;
var fbuf = "";
this.stopLoad();
this.$filebody.empty();
this.data.listdir = this.input.dir;
this.data.listsel = '';
if (!this.data.listdir)
return;
this.$btnstop.prop('disabled', false);
this.xhr = loadcmd("vfs ls '" + this.input.dir + "'", this.$filebody, function(msg) {
if (msg.error) {
if (msg.error.startsWith("Request abort"))
return '<div class="bg-info">Stopped.</div>';
else
return '<div class="bg-danger">'+msg.error+'</div>';
}
fbuf += msg.text;
var lines = fbuf.split("\n");
if (!lines || lines.length < 2) return "";
fbuf = lines[lines.length-1];
var res = '', f = {};
for (var i = 0; i < lines.length-1; i++) {
if (!lines[i]) continue;
if (lines[i].startsWith("Error"))
return '<div class="bg-danger">' + lines[i] + '</div>';
f.size = lines[i].substring(0, 10).trim();
f.date = lines[i].substring(10, 29).trim();
f.name = lines[i].substring(29).trim();
if (!f.name) {
console.log("FileBrowser.loadDir: can't parse line: '" + lines[i] + "'");
} else {
f.path = self.data.listdir + '/' + f.name;
f.isdir = (f.size == '[DIR]');
f.bytes = self.sizeToBytes(f.size);
f.isodate = self.dateToISO(f.date);
f.class = "";
if (self.options.filter && (
(typeof self.options.filter == "string" && !f.isdir && !f.name.match(self.options.filter)) ||
(typeof self.options.filter == "function" && !self.options.filter(f)))) {
continue;
}
if (f.name == self.input.file) {
f.class += " active";
self.data.listsel = f.name;
} else {
f.class = "";
}
res += '<tr data-name="' + encode_html(f.name) + '" data-size="' + f.bytes +
'" data-date="' + f.isodate + '" class="' + f.class + '">' +
'<td class="col-xs-3 col-sm-2">' + f.size +
'</td><td class="hidden-xs col-sm-4">' + f.date +
'</td><td class="col-xs-9 col-sm-6">' + encode_html(f.name) + '</td></tr>';
}
}
if (!self.options.sortBy)
return res;
if (res) {
var scrollpos = self.$filebody.get(0).scrollTop;
self.$filebody.detach().append(res);
self.sortList();
self.$filebody.appendTo(self.$filetable).scrollTop(scrollpos);
self.$filetable.trigger('window-resize');
}
return null;
}).always($.proxy(function() {
this.$btnstop.prop('disabled', true);
}, this));
},
sizeToBytes: function(size) {
if (size == "[DIR]") return -1;
var unit = size[size.length-1], val = parseFloat(size);
if (unit == 'k') return val*1024;
else if (unit == 'M') return val*1048576;
else if (unit == 'G') return val*1073741824;
else return val;
},
dateToISO: function(date) {
var month = monthnames.indexOf(date.substr(3,3)) + 1;
return date.substr(7,4)+'-'+(month<10?'0':'')+month+'-'+date.substr(0,2)+' '+date.substr(12);
},
stopLoad: function() {
this.$btnstop.prop('disabled', true);
if (this.xhr)
this.xhr.abort();
if (!supportsTouch) this.$pathinput.focus();
},
sortList: function(by, dir) {
if (by == null) {
by = this.options.sortBy;
dir = this.options.sortDir;
} else {
dir = dir || 1;
this.options.sortBy = by;
this.options.sortDir = dir;
this.$filecols.removeClass('sort-up sort-down')
.filter('[data-key="'+by+'"]').addClass((dir<0) ? 'sort-down' : 'sort-up');
}
if (!by) return;
var rows = $.makeArray(this.$filebody.children());
rows.sort(function(a,b){
if (!a.dataset[by]) return 1; if (!b.dataset[by]) return -1;
var pri = a.dataset[by].localeCompare(b.dataset[by]);
var sec = (by!="name") ? a.dataset["name"].localeCompare(b.dataset["name"]) : 0;
return dir * (pri ? pri : sec);
});
this.$filebody.html(rows);
},
newDir: function() {
this.stopLoad();
var path = this.input.dir + "/";
var self = this;
promptdialog("text", "Create new directory", path + "… (empty = create this dir)", ["Cancel", "Create"], function(create, dirname) {
if (create) {
path = (path + dirname).replace(/\/+$/, "");
$.post("/api/execute", { "command": "vfs mkdir '" + path + "'" }, function(result) {
if (result.startsWith("Error"))
confirmdialog("Error", result, ["OK"]);
else
self.setPath(path + "/", path == self.input.dir);
}).fail(function(request, textStatus, errorThrown){
confirmdialog("Error", xhrErrorInfo(request, textStatus, errorThrown), ["OK"]);
});
}
});
},
onAction: function() {
if (this.options.onAction)
this.options.onAction.call(this.$el, this.input);
},
});
$.pluginMaker(ovms.FileBrowser);
// FileDialog:
ovms.FileDialog = function(el, options) {
if (el) this.init(el, options);
}
$.extend(ovms.FileDialog.prototype, ovms.Widget.prototype, {
cname: "filedialog",
options: {
title: 'Select file',
submit: 'Select',
onSubmit: null,
onCancel: null,
path: '',
quicknav: ['/sd/', '/store/'],
filter: null,
sortBy: null,
sortDir: 1,
select: 'f',
showNewDir: true,
backdrop: true,
keyboard: true,
transition: 'fade',
size: 'lg',
onUpdate: null,
show: false,
},
init: function(el, options) {
ovms.Widget.prototype.init.call(this, el, options);
this.input = {};
this.$fb = null;
this.$el.dialog({
input: this.input,
body: '<div class="filebrowser"/>',
onHide: $.proxy(this.onHide, this),
onHidden: $.proxy(this.onHidden, this),
});
this.$fb = this.$el.find('.filebrowser').filebrowser({
input: this.input,
onPathChange: $.proxy(this.onPathChange, this),
onAction: $.proxy(this.onAction, this),
});
this.update(this.options);
},
update: function(options) {
$.extend(this.options, options);
var newbtns = [];
if (this.options.showNewDir) newbtns.push(
{ label: "New dir", btnClass: "default pull-left", autoHide: false, action: $.proxy(this.newDir, this) });
newbtns.push(
{ label: "Cancel" },
{ label: this.options.submit, btnClass: "primary" });
this.$el.dialog({
title: this.options.title,
buttons: newbtns,
backdrop: this.options.backdrop,
keyboard: this.options.keyboard,
transition: this.options.transition,
size: this.options.size,
});
this.$btnsubmit = this.$el.find(".modal-footer .btn-primary");
this.$fb.filebrowser({
path: (options.path != null) ? options.path : this.input.path,
quicknav: this.options.quicknav,
filter: this.options.filter,
sortBy: this.options.sortBy,
sortDir: this.options.sortDir,
});
this.onPathChange();
if (this.options.onUpdate)
this.options.onUpdate.call(this.$el, this.input);
if (options.show != null) {
var showing = this.$el.dialog('isShowing');
if (options.show === true && !showing)
this.$el.dialog('show');
else if (options.show === false && showing)
this.$el.dialog('hide');
}
},
show: function(options) {
if (options) this.update(options);
this.$el.dialog('show');
},
hide: function() {
this.$el.dialog('hide');
},
setPath: function(newpath, reload) {
this.$fb.filebrowser('setPath', newpath, reload);
},
newDir: function() {
this.$fb.filebrowser('newDir');
},
onPathChange: function() {
var ok = (
(this.options.select == 'f' && this.input.file) ||
(this.options.select == 'd' && !this.input.file));
this.$btnsubmit.prop("disabled", !ok);
},
onAction: function() {
this.$el.dialog('triggerButton', this.options.submit);
},
onHide: function() {
this.$fb.filebrowser('stopLoad');
},
onHidden: function(input) {
if (input.button && input.button.label == this.options.submit) {
if (this.options.onSubmit)
this.options.onSubmit.call(this.$el, this.input);
}
else {
if (this.options.onCancel)
this.options.onCancel.call(this.$el, this.input);
}
},
});
$.pluginMaker(ovms.FileDialog);
// Template list editor:
$.fn.listEditor = function(op, data){
return this.each(function(){
if (op) {
$(this).trigger('list:'+op, data);
} else {
// init:
var $this = $(this);
var el_itemid = $this.find('.list-item-id');
var el_body = $this.find('.list-items');
var el_template = $this.find('template').html();
$this.on('list:addItem', function(evt, data) {
var id = Number(el_itemid.val()) + 1;
el_itemid.val(id);
data = $.extend({ MODE: "upd" }, data);
var txt = el_template.replace(/ITEM_ID/g, id).replace(/ITEM_MODE/g, data.MODE);
for (key in data)
txt = txt.replace(new RegExp('ITEM_'+key,'g'), encode_html(data[key]));
txt = txt.replace(/ITEM_\w+/g, '');
var $el = $(txt).appendTo(el_body);
$el.find("option[data-value]").each(function(){
$(this).prop("selected", $(this).val() == $(this).data("value"));
});
$el.find("input[data-value]").each(function(){
var sel = $(this).val() == $(this).data("value");
$(this).prop("checked", sel);
if (sel) $(this).parent().addClass("active");
else $(this).parent().removeClass("active");
});
var discl = (data.MODE == "add") ? "add-disabled" : "list-disabled";
$el.find("select."+discl).each(function(){
$(this).prop("disabled", true).append($('<input type="hidden">')
.attr("name", $(this).attr("name")).attr("value", $(this).val()));
});
$el.find("input."+discl).prop("readonly", true);
$el.find("button."+discl).prop("disabled", true);
$this.trigger('list:validate');
});
$this.on('change keyup', 'input,select,textarea', function(ev) {
$this.trigger('list:validate');
});
$this.on('click', '.list-item-add', function(evt) {
var data = $.extend({ MODE: "add" }, $(this).data('preset'));
$(this).trigger('list:addItem', data);
});
$this.on('click', '.list-item-del', function(evt) {
$(this).closest('.list-item').remove();
$this.trigger('list:validate');
});
}
});
};
/**
* Slider widget plugin
*/
$.fn.slider = function(options) {
return this.each(function() {
var $sld = $(this).closest('.slider'), data = $.extend({ checked: true }, $sld.data());
var opts = (typeof options == "object") ? options : data;
// init?
if ($sld.children().length == 0) {
var id = $sld.attr('id');
$sld.html('\
<div class="slider-control form-inline">\
<input class="slider-enable" type="checkbox" checked>\
<input class="form-control slider-value" type="number" id="input-ID" name="ID">\
<span class="slider-unit">UNIT</span>\
<input class="btn btn-default slider-down" type="button" value="">\
<input class="btn btn-default slider-set" type="button" value="◈">\
<input class="btn btn-default slider-up" type="button" value="">\
</div>\
<input class="slider-input" type="range">'
.replace(/ID/g, id).replace(/UNIT/g, opts.unit||''));
}
// update:
var $inp = $sld.find('.slider-value, .slider-input'), $cb = $sld.find('.slider-enable'),
$bt = $sld.find('input[type=button]'), $sb = $bt.filter('.slider-set'),
oldchk = data.checked, chk = (opts.checked != null) ? opts.checked : oldchk,
dis = (opts.disabled != null) ? opts.disabled : ($sld.prop('disabled')==true);
$.extend(data, opts);
if (opts.unit != null) $sld.find('.slider-unit').html(opts.unit);
if (opts.min != null) $inp.attr('min', opts.min);
if (opts.max != null) $inp.attr('max', opts.max);
if (opts.step != null) $inp.attr('step', opts.step);
if (opts.default != null) {
if ($sb.length == 1)
$sb.data('set', opts.default);
if (!chk)
$inp.val(opts.default);
data.default = Math.max(data.min, Math.min(data.max, 1*opts.default));
}
if (opts.value !== undefined) {
if (opts.value === null)
data.value = data.uservalue = data.default;
else
data.value = Math.max(data.min, Math.min(data.max, 1*opts.value));
if (chk)
$inp.attr('value', data.value).val(data.value);
if (chk || data.uservalue == null)
data.uservalue = data.value;
}
if (chk != oldchk) {
$cb.prop('checked', chk);
if (chk) {
if (opts.reset || data.reset)
data.value = data.default;
else
data.value = data.uservalue;
} else {
data.uservalue = data.value;
data.value = data.default;
}
$inp.val(data.value);
}
$cb.prop('disabled', dis);
$bt.prop('disabled', !chk || dis);
$inp.prop('disabled', !chk || dis).prop('checked', chk);
if (dis)
$sld.addClass('disabled').prop('disabled', true).attr('disabled', true);
else
$sld.removeClass('disabled').prop('disabled', false).attr('disabled', false);
$sld.data(data);
});
};
/**
* Highcharts
*/
var highchartsLoader;
$.fn.chart = function(options) {
if (this.length == 0)
return this;
var $this = this;
if (options === "destroy") {
// destroy:
var $cl = this.find(".has-chart").add(this.filter(".has-chart"));
$cl.each(function() {
var chart = $(this).data("chart");
if (typeof chart == "object") {
$(this).data("chart", null);
chart.destroy();
}
});
} else {
// init:
function init_charts() {
$this.each(function() {
var chart = Highcharts.chart(this, options, function() {
if (this.userOptions && this.userOptions.onUpdate)
this.userOptions.onUpdate.call(this, metrics);
});
$(this).data("chart", chart).addClass("has-chart get-window-resize").on("window-resize", function() {
$(this).data("chart").reflow();
});
$(this).closest(".metric.chart").data("chart", chart);
});
}
if (window.Highcharts) {
init_charts();
} else if (highchartsLoader) {
highchartsLoader.then(init_charts);
} else {
highchartsLoader = $.ajax({
url: (window.assets && window.assets["charts_js"]) || "/assets/charts.js?v=6.0.7",
dataType: "script",
cache: true,
success: function(){ init_charts(); }
});
}
}
return this;
};
/**
* DataTables
*/
var datatablesLoader;
$.fn.table = function(options) {
if (this.length == 0)
return this;
var $this = this;
if (options === "destroy") {
// destroy:
var $tl = this.find(".has-dataTable").add(this.filter(".has-dataTable"));
$tl.each(function() {
var table = $(this).data("dataTable");
if (typeof table == "object") {
$(this).data("dataTable", null);
table.destroy();
}
});
} else {
// init:
function init_tables() {
$this.each(function() {
$(this).one("init.dt", function(ev, settings) {
if (settings && settings.oInstance && settings.oInit && settings.oInit.onUpdate)
settings.oInit.onUpdate.call(settings.oInstance.api(), metrics);
});
var table = $(this).DataTable(options);
$(this).data("dataTable", table).addClass("has-dataTable");
$(this).closest(".metric.table").data("dataTable", table);
});
}
if ($.fn.DataTables) {
init_tables();
} else if (datatablesLoader) {
datatablesLoader.then(init_tables);
} else {
datatablesLoader = $.ajax({
url: (window.assets && window.assets["tables_js"]) || "/assets/tables.js?v=1.10.18",
dataType: "script",
cache: true,
success: function(){ init_tables(); }
});
}
}
return this;
};
/**
* Framework Init
*/
$(function(){
// Toggle night mode:
$('body').on('click', '.toggle-night', function(event){
var nm = $('body').toggleClass("night").hasClass("night");
page.params["nm"] = 0+nm;
updateLocation();
event.stopPropagation();
return false;
});
// Toggle fullscreen mode:
document.fullScreenMode = document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen;
$(document).on('mozfullscreenchange webkitfullscreenchange fullscreenchange', function() {
this.fullScreenMode = !this.fullScreenMode;
if (this.fullScreenMode) {
$('body').addClass("fullscreened");
} else {
$('body').removeClass("fullscreened");
}
$(window).trigger("resize");
});
$('body').on('click', '.toggle-fullscreen', function(evt) {
element = document.body;
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
}
return false;
});
// AJAX page links, forms & buttons:
$('body').on('click', 'a[target^="#"], form[target^="#"] .btn[type="submit"]', function(event){
var method = $(this).data("method") || "get";
var uri = $(this).attr("href");
var target = $(this).attr("target");
var data = {};
if (method.toLowerCase() == "post") {
var p = uri.split("?");
if (p.length == 2) {
uri = p[0];
data = p[1];
}
if (uri == "" || uri == "#")
uri = $("#main").data("uri");
}
if (!uri) {
var frm = $(this.form);
method = frm.attr("method") || "get";
uri = frm.attr("action");
target = frm.attr("target");
data = frm.serialize();
if (this.name)
data += (data?"&":"") + encodeURI(this.name+"="+(this.value||"1"));
}
if ($(this).data("dismiss") == "modal")
$(this).closest(".modal").removeClass("fade").modal("hide");
if (!loaduri(target, method, uri, data))
return true;
event.preventDefault();
event.stopPropagation();
return false;
});
$('body').on('submit', 'form[target^="#"]', function(event) {
var $frm = $(this);
var method = $frm.attr("method") || "get";
var uri = $frm.attr("action");
var target = $frm.attr("target");
var data = $frm.serialize();
var $btn = $frm.find('input[type="submit"], button[type="submit"]').first();
if ($btn.length && $btn.attr("name"))
data += (data?"&":"") + encodeURI($btn.attr("name") +"="+ ($btn.val()||"1"));
if ($frm.data("dismiss") == "modal" || $btn.data("dismiss") == "modal")
$frm.closest(".modal").removeClass("fade").modal("hide");
if (!loaduri(target, method, uri, data))
return true;
event.preventDefault();
event.stopPropagation();
return false;
});
// AJAX command links & buttons:
$('body').on('click', '.btn[data-cmd]', function(event){
var btn = $(this);
var cmd = btn.data("cmd");
var tgt = btn.data("target");
var updcnt = btn.data("watchcnt") || 0;
var updint = btn.data("watchint") || 2;
btn.prop("disabled", true);
$(tgt).data("updcnt", 0);
loadcmd(cmd, tgt).then(function(){
btn.prop("disabled", false);
$(tgt).data("updcnt", updcnt);
$(tgt).data("updint", updint);
$(tgt).data("updlast", now());
}, function(){
btn.prop("disabled", false);
});
event.stopPropagation();
return false;
});
$('body').on('click', '.btn[data-js]', function(event){
var btn = $(this);
var js = btn.data("js");
var tgt = btn.data("target");
var updcnt = btn.data("watchcnt") || 0;
var updint = btn.data("watchint") || 2;
btn.prop("disabled", true);
$(tgt).data("updcnt", 0);
loadjs(js, tgt).then(function(){
btn.prop("disabled", false);
$(tgt).data("updcnt", updcnt);
$(tgt).data("updint", updint);
$(tgt).data("updlast", now());
}, function(){
btn.prop("disabled", false);
});
event.stopPropagation();
return false;
});
// Long touch buttons:
var longtouchTimeout, $longtouchProgress;
$('body').on('touchstart', '.btn-longtouch .btn, .btn.btn-longtouch', function(ev) {
var $this = $(this);
if ($this.prop('disabled')) return;
var action = $this.attr("title") || $this.text();
if (navigator.vibrate) navigator.vibrate([100,400,100,400,100,400]);
$longtouchProgress = $('<div class="hover-progress longtouch"><div class="hover-progress-body"><div class="info">Hold touch for/to</div><div class="action">'+encode_html(action)+'</div><div class="progress"><div class="progress-bar progress-bar-info" style="width:0%"></div></div></div></div>').appendTo("body").find(".progress-bar");
window.getComputedStyle($longtouchProgress.get(0)).width;
$longtouchProgress.css("width", "100%");
longtouchTimeout = window.setTimeout(function() {
if (navigator.vibrate) navigator.vibrate(1000);
if ($longtouchProgress) $longtouchProgress.closest(".hover-progress").remove();
longtouchTimeout = null;
$longtouchProgress = null;
$this.trigger('click');
}, 1500);
ev.preventDefault();
}).on('touchcancel touchend', '.btn-longtouch .btn, .btn.btn-longtouch', function(ev) {
window.clearTimeout(longtouchTimeout);
if (navigator.vibrate) navigator.vibrate(0);
if ($longtouchProgress) $longtouchProgress.closest(".hover-progress").remove();
longtouchTimeout = null;
$longtouchProgress = null;
ev.preventDefault();
}).on('contextmenu', function(ev) {
if ($longtouchProgress) {
ev.preventDefault();
ev.stopPropagation();
ev.stopImmediatePropagation();
return false;
}
});
// Slider widget event handling:
$("body").on("change", ".slider-enable", function(evt) {
var $this = $(this), $sld = $this.closest(".slider"), $inp = $sld.find(".slider-input, .slider-value");
$this.slider({ checked: this.checked });
$inp.trigger("input", true).trigger("change", true);
});
$("body").on("input change", ".slider-value", function(evt, noprop) {
var $this = $(this), $sld = $this.closest(".slider"), $inp = $sld.find(".slider-input");
if (!noprop) {
$this.slider({ value: this.value });
$inp.trigger(evt.type, true);
}
});
$("body").on("input change", ".slider-input", function(evt, noprop) {
var $this = $(this), $sld = $this.closest(".slider"), $inp = $sld.find(".slider-value");
if (!noprop) {
$this.slider({ value: this.value });
$inp.trigger(evt.type, true);
}
});
$("body").on("click", ".slider-up", function(evt) {
$(this).closest(".slider").find(".slider-input").val(function() {
return Math.min(1*this.value + (1*this.step||1), this.max);
}).trigger("input").trigger("change");
});
$("body").on("click", ".slider-down", function(evt) {
$(this).closest(".slider").find(".slider-input").val(function() {
return Math.max(1*this.value - (1*this.step||1), this.min);
}).trigger("input").trigger("change");
});
$("body").on("click", ".slider-set", function(evt) {
var $inp = $(this).closest(".slider").find(".slider-input");
$inp.val($(this).data("set")).trigger("input").trigger("change");
});
// data-toggle="filefialog":
$("body").on('click', '.btn[data-toggle="filedialog"]', function(evt) {
var $this = $(this);
var $tgt = $($this.data("target"));
var $inp = $($this.data("input"));
if ($tgt.length && $inp.length) {
var val = $inp.val();
var opt = {
show: true,
onSubmit: function(input) { $inp.val(input.path); }
};
if (val) opt.path = val;
$tgt.filedialog(opt);
}
});
// Metrics displays:
$("body").on('msg:metrics', '.receiver', function(e, update) {
$(this).find(".metric").each(function() {
var $el = $(this), metric = $el.data("metric"), prec = $el.data("prec"), scale = $el.data("scale"), useUser = $el.data("user");
if (!metric) return;
// filter:
var keys = metric.split(","), val;
var metricName = "";
for (var i=0; i<keys.length; i++) {
metricName = keys[i];
if ((val = update[metricName]) != null) {
break;
}
}
if (val == null) return;
// process:
if ($el.hasClass("text")) {
var elt = $el.children(".value");
if (elt) elt.text(val);
elt = $el.children(".unit");
if (elt) elt.text(val);
} else if ($el.hasClass("number")) {
var vf = val;
if (scale != null)
vf = Number(vf) * scale;
else {
var mun = units.userUnitLabelFromMetric(metricName);
if (mun != "") {
// If there's a .unit.. then convert it.
item = $el.children(".unit");
if (item) {
item.text(mun);
useUser = true;
}
}
if (useUser)
vf = units.convertMetricToUserUnits(vf, metricName);
}
if (prec != null) vf = Number(vf).toFixed(prec);
$el.children(".value").text(vf);
} else if ($el.hasClass("progress")) {
var vf = val;
if (scale != null) vf = Number(vf) * scale;
if (prec != null) vf = Number(vf).toFixed(prec);
var $pb = $(this.firstElementChild), min = $pb.attr("aria-valuemin"), max = $pb.attr("aria-valuemax");
var vp = (val-min) / (max-min) * 100;
$pb.css("width", vp+"%").attr("aria-valuenow", vp).find(".value").text(vf);
var lw = 0; $pb.find("span").each(function(){ lw += $(this).width(); });
if (($pb.parent().width()*vp/100) < lw) $pb.addClass("value-low"); else $pb.removeClass("value-low");
} else if ($el.hasClass("chart")) {
var ch = $(this).data("chart");
if (ch && ch.userOptions && ch.userOptions.onUpdate)
ch.userOptions.onUpdate.call(ch, update);
} else if ($el.hasClass("table")) {
var dt = $(this).data("dataTable");
if (dt && dt.settings() && dt.settings()[0] && dt.settings()[0].oInit && dt.settings()[0].oInit.onUpdate)
dt.settings()[0].oInit.onUpdate.call(dt, update);
} else {
$el.text(val);
}
});
});
// Modal autoclear:
$("body").on("hidden.bs.modal", ".modal", function(evt) {
$(this).find(".modal-autoclear").empty();
});
// Proxy window resize:
$(window).on("resize", function(event){
$(".get-window-resize").trigger("window-resize");
});
$("body").on("window-resize", ".table-scrollable", function(evt) {
var bw = $(this).find('>tbody').get(0).scrollWidth;
if (bw) $(this).find('>thead').css('width', bw);
});
// Monitor timer:
if (!monitorTimer)
monitorTimer = window.setInterval(monitorUpdate, 1000);
// AJAX page init:
window.onpopstate = loadPage;
loadPage();
});