/* 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, '>'); } 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; iLogin"; 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 = '
' + '×' + '' + text + '' + '
'; } 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 '
'+msg.error+'
'; else return $('
').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 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(); }; 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 == "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 = ' ' + msg.subtype + ' Info'; opts.timeout = 60; } else if (msg.type == "alert") { opts.title = ' ' + msg.subtype + ' Alert'; } else if (msg.type == "error") { opts.title = ' ' + msg.subtype + ' Error'; } else return; opts.body = '
' + msg.value + '
'; confirmdialog(opts.title, opts.body, ["OK"], opts.timeout); } $.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])) { try { console.log("subscribe " + tops[i]); if (ws) ws.send("subscribe " + tops[i]); subs.push(tops[i]); } catch (e) { console.log(e); } } } $(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 : '

Rebooting now…

') + '

The window will automatically reload when the browser reconnects to the module.

' + '

'); }; /** * 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('
'); 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] = $('') .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($('
'), arguments); }; $.fn.promptdialog = function(_type, _title, _prompt, _buttons, _action) { var options = { show: true, title: _title, buttons: [] }; var uid = ++ovms.uid; options.body = ''; 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($('
'), 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('
SizeName
'.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(''); } 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 '
Stopped.
'; else return '
'+msg.error+'
'; } 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 '
' + lines[i] + '
'; 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 += '' + '' + f.size + '' + f.date + '' + encode_html(f.name) + ''; } } 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: '
', 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($('') .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('\
\ \ \ UNIT\ \ \ \
\ ' .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 = $('
Hold touch for/to
'+encode_html(action)+'
').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"); if (!metric) return; // filter: var keys = metric.split(","), val; for (var i=0; itbody').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(); });