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

1999 lines
65 KiB
JavaScript
Raw Normal View History

/* 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 = {};
2023-01-24 20:50:05 +01:00
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();
2023-01-24 20:50:05 +01:00
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);
}
2023-01-24 20:50:05 +01:00
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);
}
2023-01-24 20:50:05 +01:00
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])) {
2023-01-24 20:50:05 +01:00
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() {
2023-01-24 20:50:05 +01:00
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;
2023-01-24 20:50:05 +01:00
var metricName = "";
for (var i=0; i<keys.length; i++) {
2023-01-24 20:50:05 +01:00
metricName = keys[i];
if ((val = update[metricName]) != null) {
break;
}
}
if (val == null) return;
2023-01-24 20:50:05 +01:00
// process:
if ($el.hasClass("text")) {
2023-01-24 20:50:05 +01:00
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;
2023-01-24 20:50:05 +01:00
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();
});