diff options
Diffstat (limited to 'web/share/js')
-rw-r--r-- | web/share/js/bb.js | 33 | ||||
-rw-r--r-- | web/share/js/index/main.js | 65 | ||||
-rw-r--r-- | web/share/js/kvm/atx.js | 43 | ||||
-rw-r--r-- | web/share/js/kvm/hid.js | 198 | ||||
-rw-r--r-- | web/share/js/kvm/keyboard.js | 190 | ||||
-rw-r--r-- | web/share/js/kvm/main.js | 16 | ||||
-rw-r--r-- | web/share/js/kvm/mouse.js | 160 | ||||
-rw-r--r-- | web/share/js/kvm/msd.js | 193 | ||||
-rw-r--r-- | web/share/js/kvm/session.js | 133 | ||||
-rw-r--r-- | web/share/js/kvm/stream.js | 221 | ||||
-rw-r--r-- | web/share/js/login/main.js | 36 | ||||
-rw-r--r-- | web/share/js/tools.js | 136 | ||||
-rw-r--r-- | web/share/js/wm.js | 387 |
13 files changed, 1811 insertions, 0 deletions
diff --git a/web/share/js/bb.js b/web/share/js/bb.js new file mode 100644 index 00000000..565a3ab4 --- /dev/null +++ b/web/share/js/bb.js @@ -0,0 +1,33 @@ +function checkBrowser() { + if ( + !window.navigator + || window.navigator.userAgent.indexOf("MSIE ") > 0 + || window.navigator.userAgent.indexOf("Trident/") > 0 + || window.navigator.userAgent.indexOf("Edge/") > 0 + ) { + var el_modal = document.createElement("div"); + el_modal.className = "modal"; + el_modal.style.visibility = "visible"; + el_modal.innerHTML = ` + <div class="modal-window"> + <div class="modal-content"> + Hello. You are using an incompatible or legacy browser.<br> + Please use one of the following browsers: + <hr> + <ul> + <li><a target="_blank" href="https://google.com/chrome">Google Chrome</a> <sup><i>recommended</i></sup></li> + <li><a target="_blank" href="https://chromium.org/Home">Chromium</a> <sup><i>recommended</i></sup></li> + <li><a target="_blank" href="https://mozilla.org/firefox">Mozilla Firefox</a></li> + <li><a target="_blank" href="https://apple.com/safari">Apple Safari</a></li> + <li><a target="_blank" href="https://opera.com">Opera</a></li> + <li><a target="_blank" href="https://vivaldi.com">Vivaldi</a></li> + </ul> + </div> + </div> + `; + document.body.appendChild(el_modal); + return false; + } else { + return true; + } +} diff --git a/web/share/js/index/main.js b/web/share/js/index/main.js new file mode 100644 index 00000000..8f5c74a6 --- /dev/null +++ b/web/share/js/index/main.js @@ -0,0 +1,65 @@ +function main() { + if (checkBrowser()) { + __setAppText(); + __loadKvmdInfo(); + } +} + +function __setAppText() { + $("app-text").innerHTML = ` + <span class="code-comment"># On Linux using Chromium/Chrome via any terminal:<br> + $</span> \`which chromium 2>/dev/null || which chrome 2>/dev/null\` --app="${window.location.href}"<br> + <br> + <span class="code-comment"># On MacOS using Terminal application:<br> + $</span> /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --app="${window.location.href}"<br> + <br> + <span class="code-comment"># On Windows via cmd.exe:<br> + C:\></span> start chrome --app="${window.location.href}" + `; +} + +function __loadKvmdInfo() { + var http = tools.makeRequest("GET", "/kvmd/info", function() { + if (http.readyState === 4) { + if (http.status === 200) { + var info = JSON.parse(http.responseText).result; + + var apps = Object.values(info.extras).sort(function(a, b) { + if (a["place"] < b["place"]) { + return -1; + } else if (a["place"] > b["place"]) { + return 1; + } else { + return 0; + } + }); + + $("apps-box").innerHTML = "<ul id=\"apps\"></ul>"; + apps.forEach(function(app) { + $("apps").innerHTML += ` + <li> + <div class="app"> + <a href="${app.path}"> + <div> + <img class="svg-gray" src="${app.icon}"> + ${app.name} + </div> + </a> + </div> + </li> + `; + }); + + if (info.meta && info.meta.server && info.meta.server.host) { + $("kvmd-meta-server-host").innerHTML = info.meta.server.host; + document.title = "Pi-KVM Index: " + info.meta.server.host; + } else { + $("kvmd-meta-server-host").innerHTML = ""; + document.title = "Pi-KVM Index"; + } + } else { + setTimeout(__loadKvmdInfo, 1000); + } + } + }); +} diff --git a/web/share/js/kvm/atx.js b/web/share/js/kvm/atx.js new file mode 100644 index 00000000..ed5f045e --- /dev/null +++ b/web/share/js/kvm/atx.js @@ -0,0 +1,43 @@ +function Atx() { + var self = this; + + /********************************************************************************/ + + var __init__ = function() { + $("atx-power-led").title = "Power Led"; + $("atx-hdd-led").title = "Disk Activity Led"; + + tools.setOnClick($("atx-power-button"), () => __clickButton("power", "Are you sure to click the power button?")); + tools.setOnClick($("atx-power-button-long"), () => __clickButton("power_long", "Are you sure to perform the long press of the power button?")); + tools.setOnClick($("atx-reset-button"), () => __clickButton("reset", "Are you sure to reboot the server?")); + }; + + /********************************************************************************/ + + self.setState = function(state) { + $("atx-power-led").className = ((state && state.leds.power) ? "led-green" : "led-gray"); + $("atx-hdd-led").className = ((state && state.leds.hdd) ? "led-red" : "led-gray"); + + wm.switchDisabled($("atx-power-button"), (!state || state.busy)); + wm.switchDisabled($("atx-power-button-long"), (!state || state.busy)); + wm.switchDisabled($("atx-reset-button"), (!state || state.busy)); + }; + + var __clickButton = function(button, confirm_msg) { + wm.confirm(confirm_msg).then(function(ok) { + if (ok) { + var http = tools.makeRequest("POST", "/kvmd/atx/click?button=" + button, function() { + if (http.readyState === 4) { + if (http.status === 409) { + wm.error("Performing another ATX operation for other client.<br>Please try again later"); + } else if (http.status !== 200) { + wm.error("Click error:<br>", http.responseText); + } + } + }); + } + }); + }; + + __init__(); +} diff --git a/web/share/js/kvm/hid.js b/web/share/js/kvm/hid.js new file mode 100644 index 00000000..49d659ab --- /dev/null +++ b/web/share/js/kvm/hid.js @@ -0,0 +1,198 @@ +function Hid() { + var self = this; + + /********************************************************************************/ + + var __ws = null; + + var __chars_to_codes = {}; + var __codes_delay = 50; + + var __keyboard = new Keyboard(); + var __mouse = new Mouse(); + + var __init__ = function() { + var __hidden_attr = null; + var __visibility_change_attr = null; + + if (typeof document.hidden !== "undefined") { + __hidden_attr = "hidden"; + __visibility_change_attr = "visibilitychange"; + } else if (typeof document.webkitHidden !== "undefined") { + __hidden_attr = "webkitHidden"; + __visibility_change_attr = "webkitvisibilitychange"; + } else if (typeof document.mozHidden !== "undefined") { + __hidden_attr = "mozHidden"; + __visibility_change_attr = "mozvisibilitychange"; + } + + if (__visibility_change_attr) { + document.addEventListener( + __visibility_change_attr, + function() { + if (document[__hidden_attr]) { + __releaseAll(); + } + }, + false + ); + } + + window.addEventListener("pagehide", __releaseAll); + window.addEventListener("blur", __releaseAll); + + __chars_to_codes = __buildCharsToCodes(); + + tools.setOnClick($("hid-pak-button"), __clickPasteAsKeysButton); + tools.setOnClick($("hid-reset-button"), __clickResetButton); + + Array.prototype.forEach.call(document.querySelectorAll("[data-shortcut]"), function(el_shortcut) { + tools.setOnClick(el_shortcut, () => __emitShortcut(el_shortcut.getAttribute("data-shortcut").split(" "))); + }); + }; + + /********************************************************************************/ + + self.setSocket = function(ws) { + wm.switchDisabled($("hid-pak-text"), !ws); + wm.switchDisabled($("hid-pak-button"), !ws); + wm.switchDisabled($("hid-reset-button"), !ws); + __ws = ws; + __keyboard.setSocket(ws); + __mouse.setSocket(ws); + }; + + var __releaseAll = function() { + __keyboard.releaseAll(); + }; + + var __emitShortcut = function(codes) { + return new Promise(function(resolve) { + tools.debug("HID: emitting keys:", codes); + + var raw_events = []; + [[codes, true], [codes.slice().reverse(), false]].forEach(function(op) { + var [op_codes, state] = op; + op_codes.forEach(function(code) { + raw_events.push({code: code, state: state}); + }); + }); + + var index = 0; + var iterate = () => setTimeout(function() { + __keyboard.fireEvent(raw_events[index].code, raw_events[index].state); + ++index; + if (index < raw_events.length) { + iterate(); + } else { + resolve(null); + } + }, __codes_delay); + iterate(); + }); + }; + + var __buildCharsToCodes = function() { + var chars_to_codes = { + "\n": ["Enter"], + "\t": ["Tab"], + " ": ["Space"], + "`": ["Backquote"], "~": ["ShiftLeft", "Backquote"], + "\\": ["Backslash"], "|": ["ShiftLeft", "Backslash"], + "[": ["BracketLeft"], "{": ["ShiftLeft", "BracketLeft"], + "]": ["BracketLeft"], "}": ["ShiftLeft", "BracketRight"], + ",": ["Comma"], "<": ["ShiftLeft", "Comma"], + ".": ["Period"], ">": ["ShiftLeft", "Period"], + "1": ["Digit1"], "!": ["ShiftLeft", "Digit1"], + "2": ["Digit2"], "@": ["ShiftLeft", "Digit2"], + "3": ["Digit3"], "#": ["ShiftLeft", "Digit3"], + "4": ["Digit4"], "$": ["ShiftLeft", "Digit4"], + "5": ["Digit5"], "%": ["ShiftLeft", "Digit5"], + "6": ["Digit6"], "^": ["ShiftLeft", "Digit6"], + "7": ["Digit7"], "&": ["ShiftLeft", "Digit7"], + "8": ["Digit8"], "*": ["ShiftLeft", "Digit8"], + "9": ["Digit9"], "(": ["ShiftLeft", "Digit9"], + "0": ["Digit0"], ")": ["ShiftLeft", "Digit0"], + "-": ["Minus"], "_": ["ShiftLeft", "Minus"], + "'": ["Quote"], "\"": ["ShiftLeft", "Quote"], + ";": ["Semicolon"], ":": ["ShiftLeft", "Semicolon"], + "/": ["Slash"], "?": ["ShiftLeft", "Slash"], + "=": ["Equal"], "+": ["ShiftLeft", "Equal"], + }; + + for (var ch = "a".charCodeAt(0); ch <= "z".charCodeAt(0); ++ch) { + var low = String.fromCharCode(ch); + var up = low.toUpperCase(); + var code = "Key" + up; + chars_to_codes[low] = [code]; + chars_to_codes[up] = ["ShiftLeft", code]; + } + + return chars_to_codes; + }; + + var __clickPasteAsKeysButton = function() { + var text = $("hid-pak-text").value.replace(/[^\x00-\x7F]/g, ""); // eslint-disable-line no-control-regex + if (text) { + var clipboard_codes = []; + var codes_count = 0; + [...text].forEach(function(ch) { + var codes = __chars_to_codes[ch]; + if (codes) { + codes_count += codes.length; + clipboard_codes.push(codes); + } + }); + var time = __codes_delay * codes_count * 2 / 1000; + + var confirm_msg = ` + You are going to automatically type ${codes_count} characters from the system clipboard. + It will take ${time} seconds.<br> + <br> + Are you sure you want to continue? + `; + + wm.confirm(confirm_msg).then(function(ok) { + if (ok) { + wm.switchDisabled($("hid-pak-text"), true); + wm.switchDisabled($("hid-pak-button"), true); + $("hid-pak-led").className = "led-yellow-rotating-fast"; + $("hid-pak-led").title = "Autotyping..."; + + tools.debug("HID: paste-as-keys:", text); + + var index = 0; + var iterate = function() { + __emitShortcut(clipboard_codes[index]).then(function() { + ++index; + if (index < clipboard_codes.length && __ws) { + iterate(); + } else { + $("hid-pak-text").value = ""; + wm.switchDisabled($("hid-pak-text"), false); + wm.switchDisabled($("hid-pak-button"), false); + $("hid-pak-led").className = "led-gray"; + $("hid-pak-led").title = ""; + } + }); + }; + iterate(); + } else { + $("hid-pak-text").value = ""; + } + }); + } + }; + + var __clickResetButton = function() { + var http = tools.makeRequest("POST", "/kvmd/hid/reset", function() { + if (http.readyState === 4) { + if (http.status !== 200) { + wm.error("HID reset error:<br>", http.responseText); + } + } + }); + }; + + __init__(); +} diff --git a/web/share/js/kvm/keyboard.js b/web/share/js/kvm/keyboard.js new file mode 100644 index 00000000..3c821406 --- /dev/null +++ b/web/share/js/kvm/keyboard.js @@ -0,0 +1,190 @@ +function Keyboard() { + var self = this; + + /********************************************************************************/ + + var __ws = null; + + var __keys = [].slice.call(document.querySelectorAll("div#keyboard-desktop div.keyboard-block div.keyboard-row div.key")); + var __modifiers = [].slice.call(document.querySelectorAll("div#keyboard-desktop div.keyboard-block div.keyboard-row div.modifier")); + + var __init__ = function() { + $("hid-keyboard-led").title = "Keyboard free"; + + $("keyboard-window").onkeydown = (event) => __keyboardHandler(event, true); + $("keyboard-window").onkeyup = (event) => __keyboardHandler(event, false); + $("keyboard-window").onfocus = __updateLeds; + $("keyboard-window").onblur = __updateLeds; + + $("stream-window").onkeydown = (event) => __keyboardHandler(event, true); + $("stream-window").onkeyup = (event) => __keyboardHandler(event, false); + $("stream-window").onfocus = __updateLeds; + $("stream-window").onblur = __updateLeds; + + window.addEventListener("focusin", __updateLeds); + window.addEventListener("focusout", __updateLeds); + + Array.prototype.forEach.call($$("key"), function(el_key) { + tools.setOnDown(el_key, () => __clickHandler(el_key, true)); + tools.setOnUp(el_key, () => __clickHandler(el_key, false)); + el_key.onmouseout = function() { + if (__isPressed(el_key)) { + __clickHandler(el_key, false); + } + }; + }); + + Array.prototype.forEach.call($$("modifier"), function(el_key) { + tools.setOnDown(el_key, () => __toggleModifierHandler(el_key)); + }); + + if (tools.browser.is_mac) { + tools.info("Keyboard: enabled Mac-CMD-Hook"); + } + }; + + /********************************************************************************/ + + self.setSocket = function(ws) { + if (ws !== __ws) { + self.releaseAll(); + __ws = ws; + } + __updateLeds(); + }; + + self.releaseAll = function() { + __keys.concat(__modifiers).forEach(function(el_key) { + if (__isActive(el_key)) { + self.fireEvent(el_key.getAttribute("data-key"), false); + } + }); + }; + + self.fireEvent = function(code, state) { + __keyboardHandler({code: code}, state); + }; + + var __updateLeds = function() { + tools.debug("Keyboard: update leds"); + if ( + __ws && ( + $("stream-window").classList.contains("window-active") + || $("keyboard-window").classList.contains("window-active") + ) + ) { + $("hid-keyboard-led").className = "led-green"; + $("hid-keyboard-led").title = "Keyboard captured"; + } else { + $("hid-keyboard-led").className = "led-gray"; + $("hid-keyboard-led").title = "Keyboard free"; + } + }; + + var __keyboardHandler = function(event, state) { + if (event.preventDefault) { + event.preventDefault(); + } + var el_key = document.querySelector(`[data-key='${event.code}']`); + if (el_key && !event.repeat) { + __commonHandler(el_key, state, "pressed"); + if (tools.browser.is_mac) { + // https://bugs.chromium.org/p/chromium/issues/detail?id=28089 + // https://bugzilla.mozilla.org/show_bug.cgi?id=1299553 + if ((event.code === "MetaLeft" || event.code === "MetaRight") && !state) { + __keys.forEach(function(el_key) { + if (__isActive(el_key)) { + self.fireEvent(el_key.getAttribute("data-key"), false); + } + }); + } + } + __unholdModifiers(); + } + }; + + var __clickHandler = function(el_key, state) { + __commonHandler(el_key, state, "pressed"); + __unholdModifiers(); + }; + + var __toggleModifierHandler = function(el_key) { + __commonHandler(el_key, !__isActive(el_key), "holded"); + }; + + var __unholdModifiers = function() { + __modifiers.forEach(function(el_key) { + if (__isHolded(el_key)) { + __deactivate(el_key); + __sendKey(el_key, false); + } + }); + }; + + var __commonHandler = function(el_key, state, cls) { + if (state && !__isActive(el_key)) { + __deactivate(el_key); + __activate(el_key, cls); + __sendKey(el_key, true); + } else { + __deactivate(el_key); + __sendKey(el_key, false); + } + }; + + var __isPressed = function(el_key) { + var is_pressed = false; + Array.prototype.forEach.call(__resolveKeys(el_key), function(el_key) { + is_pressed = (is_pressed || el_key.classList.contains("pressed")); + }); + return is_pressed; + }; + + var __isHolded = function(el_key) { + var is_holded = false; + Array.prototype.forEach.call(__resolveKeys(el_key), function(el_key) { + is_holded = (is_holded || el_key.classList.contains("holded")); + }); + return is_holded; + }; + + var __isActive = function(el_key) { + var is_active = false; + Array.prototype.forEach.call(__resolveKeys(el_key), function(el_key) { + is_active = (is_active || el_key.classList.contains("pressed") || el_key.classList.contains("holded")); + }); + return is_active; + }; + + var __activate = function(el_key, cls) { + Array.prototype.forEach.call(__resolveKeys(el_key), function(el_key) { + el_key.classList.add(cls); + }); + }; + + var __deactivate = function(el_key) { + Array.prototype.forEach.call(__resolveKeys(el_key), function(el_key) { + el_key.classList.remove("pressed"); + el_key.classList.remove("holded"); + }); + }; + + var __resolveKeys = function(el_key) { + var code = el_key.getAttribute("data-key"); + return document.querySelectorAll(`[data-key='${code}']`); + }; + + var __sendKey = function(el_key, state) { + var code = el_key.getAttribute("data-key"); + tools.debug("Keyboard: key", (state ? "pressed:" : "released:"), code); + if (__ws) { + __ws.send(JSON.stringify({ + event_type: "key", + key: code, + state: state, + })); + } + }; + + __init__(); +} diff --git a/web/share/js/kvm/main.js b/web/share/js/kvm/main.js new file mode 100644 index 00000000..5c6d775a --- /dev/null +++ b/web/share/js/kvm/main.js @@ -0,0 +1,16 @@ +var wm; + +function main() { + if (checkBrowser()) { + wm = new WindowManager(); + + tools.setOnClick($("show-about-button"), () => wm.showWindow($("about-window"))); + tools.setOnClick($("show-keyboard-button"), () => wm.showWindow($("keyboard-window"))); + tools.setOnClick($("show-stream-button"), () => wm.showWindow($("stream-window"))); + tools.setOnClick($("open-log-button"), () => window.open("/kvmd/log?seek=3600&follow=1", "_blank")); + + wm.showWindow($("stream-window")); + + new Session(); + } +} diff --git a/web/share/js/kvm/mouse.js b/web/share/js/kvm/mouse.js new file mode 100644 index 00000000..58e46d15 --- /dev/null +++ b/web/share/js/kvm/mouse.js @@ -0,0 +1,160 @@ +function Mouse() { + var self = this; + + /********************************************************************************/ + + var __ws = null; + + var __current_pos = {x: 0, y:0}; + var __sent_pos = {x: 0, y:0}; + var __wheel_delta = {x: 0, y: 0}; + + var __stream_hovered = false; + + var __init__ = function() { + $("hid-mouse-led").title = "Mouse free"; + + $("stream-box").onmouseenter = __hoverStream; + $("stream-box").onmouseleave = __leaveStream; + $("stream-box").onmousedown = (event) => __buttonHandler(event, true); + $("stream-box").onmouseup = (event) => __buttonHandler(event, false); + $("stream-box").oncontextmenu = (event) => event.preventDefault(); + $("stream-box").onmousemove = __moveHandler; + $("stream-box").onwheel = __wheelHandler; + $("stream-box").ontouchstart = (event) => __touchMoveHandler(event); + + Array.prototype.forEach.call(document.querySelectorAll("[data-mouse-button]"), function(el_button) { + var button = el_button.getAttribute("data-mouse-button"); + tools.setOnDown(el_button, () => __sendButton(button, true)); + tools.setOnUp(el_button, () => __sendButton(button, false)); + }); + + setInterval(__sendMove, 100); + }; + + /********************************************************************************/ + + self.setSocket = function(ws) { + __ws = ws; + if (ws) { + $("stream-box").classList.add("stream-box-mouse-enabled"); + } else { + $("stream-box").classList.remove("stream-box-mouse-enabled"); + } + __updateLeds(); + }; + + var __hoverStream = function() { + __stream_hovered = true; + __updateLeds(); + }; + + var __leaveStream = function() { + __stream_hovered = false; + __updateLeds(); + }; + + var __updateLeds = function() { + if (__ws && (__stream_hovered || tools.browser.is_ios)) { + // Mouse is always available on iOS via touchscreen + $("hid-mouse-led").className = "led-green"; + $("hid-mouse-led").title = "Mouse tracked"; + } else { + $("hid-mouse-led").className = "led-gray"; + $("hid-mouse-led").title = "Mouse free"; + } + }; + + var __buttonHandler = function(event, state) { + // https://www.w3schools.com/jsref/event_button.asp + event.preventDefault(); + switch (event.button) { + case 0: __sendButton("left", state); break; + case 2: __sendButton("right", state); break; + } + }; + + var __touchMoveHandler = function(event) { + event.preventDefault(); + if (event.touches[0].target && event.touches[0].target.getBoundingClientRect) { + var rect = event.touches[0].target.getBoundingClientRect(); + __current_pos = { + x: Math.round(event.touches[0].clientX - rect.left), + y: Math.round(event.touches[0].clientY - rect.top), + }; + __sendMove(); + } + }; + + var __moveHandler = function(event) { + var rect = event.target.getBoundingClientRect(); + __current_pos = { + x: Math.round(event.clientX - rect.left), + y: Math.round(event.clientY - rect.top), + }; + }; + + + var __sendButton = function(button, state) { + tools.debug("Mouse: button", (state ? "pressed:" : "released:"), button); + __sendMove(); + if (__ws) { + __ws.send(JSON.stringify({ + event_type: "mouse_button", + button: button, + state: state, + })); + } + }; + + var __sendMove = function() { + var pos = __current_pos; + if (pos.x !== __sent_pos.x || pos.y !== __sent_pos.y) { + var el_stream_image = $("stream-image"); + var to = { + x: __translate(pos.x, 0, el_stream_image.clientWidth, -32768, 32767), + y: __translate(pos.y, 0, el_stream_image.clientHeight, -32768, 32767), + }; + + tools.debug("Mouse: moved:", to); + if (__ws) { + __ws.send(JSON.stringify({ + event_type: "mouse_move", + to: to, + })); + } + __sent_pos = pos; + } + }; + + var __translate = function(x, a, b, c, d) { + return Math.round((x - a) / (b - a) * (d - c) + c); + }; + + var __wheelHandler = function(event) { + // https://learn.javascript.ru/mousewheel + if (event.preventDefault) { + event.preventDefault(); + } + + var delta = {x: 0, y: 0}; + + __wheel_delta.y += event.deltaY; + if (Math.abs(__wheel_delta.y) >= 100) { + delta.y = __wheel_delta.y / Math.abs(__wheel_delta.y) * (-5); + __wheel_delta.y = 0; + } + + if (delta.y) { + tools.debug("Mouse: scrolled:", delta); + if (__ws) { + __ws.send(JSON.stringify({ + event_type: "mouse_wheel", + delta: delta, + })); + } + } + }; + + __init__(); +} diff --git a/web/share/js/kvm/msd.js b/web/share/js/kvm/msd.js new file mode 100644 index 00000000..c23de99d --- /dev/null +++ b/web/share/js/kvm/msd.js @@ -0,0 +1,193 @@ +function Msd() { + var self = this; + + /********************************************************************************/ + + var __state = null; + var __upload_http = null; + var __image_file = null; + + var __init__ = function() { + $("msd-led").title = "Unknown state"; + + $("msd-select-new-image-file").onchange = __selectNewImageFile; + tools.setOnClick($("msd-select-new-image-button"), () => $("msd-select-new-image-file").click()); + + tools.setOnClick($("msd-upload-new-image-button"), __clickUploadNewImageButton); + tools.setOnClick($("msd-abort-uploading-button"), __clickAbortUploadingButton); + + tools.setOnClick($("msd-switch-to-kvm-button"), () => __clickSwitchButton("kvm")); + tools.setOnClick($("msd-switch-to-server-button"), () => __clickSwitchButton("server")); + + tools.setOnClick($("msd-reset-button"), __clickResetButton); + }; + + /********************************************************************************/ + + self.setState = function(state) { + __state = state; + __applyState(); + }; + + var __clickUploadNewImageButton = function() { + var form_data = new FormData(); + form_data.append("image_name", __image_file.name); + form_data.append("image_data", __image_file); + + __upload_http = new XMLHttpRequest(); + __upload_http.open("POST", "/kvmd/msd/write", true); + __upload_http.upload.timeout = 5000; + __upload_http.onreadystatechange = __uploadStateChange; + __upload_http.upload.onprogress = __uploadProgress; + __upload_http.send(form_data); + }; + + var __clickAbortUploadingButton = function() { + __upload_http.onreadystatechange = null; + __upload_http.upload.onprogress = null; + __upload_http.abort(); + __upload_http = null; + $("msd-progress").setAttribute("data-label", "Aborted"); + $("msd-progress-value").style.width = "0%"; + }; + + var __clickSwitchButton = function(to) { + var http = tools.makeRequest("POST", "/kvmd/msd/connect?to=" + to, function() { + if (http.readyState === 4) { + if (http.status !== 200) { + wm.error("Switch error:<br>", http.responseText); + } + } + __applyState(); + }); + __applyState(); + wm.switchDisabled($(`msd-switch-to-${to}-button`), true); + }; + + var __selectNewImageFile = function() { + var el_input = $("msd-select-new-image-file"); + var image_file = (el_input.files.length ? el_input.files[0] : null); + if (image_file && image_file.size > __state.info.size) { + wm.error("New image is too big for your Mass Storage Device.<br>Maximum:", __formatSize(__state.info.size)); + el_input.value = ""; + image_file = null; + } + __image_file = image_file; + __applyState(); + }; + + var __clickResetButton = function() { + var http = tools.makeRequest("POST", "/kvmd/msd/reset", function() { + if (http.readyState === 4) { + if (http.status !== 200) { + wm.error("MSD reset error:<br>", http.responseText); + } + } + __applyState(); + }); + __applyState(); + }; + + var __applyState = function() { + if (__state) { + if (__state.connected_to === "server") { + $("msd-another-another-user-uploads").style.display = "none"; + $("msd-led").className = "led-green"; + $("msd-status").innerHTML = $("msd-led").title = "Connected to Server"; + } else if (__state.busy) { + if (!__upload_http) { + $("msd-another-another-user-uploads").style.display = "block"; + } + $("msd-led").className = "led-yellow-rotating-fast"; + $("msd-status").innerHTML = $("msd-led").title = "Uploading new image"; + } else { + $("msd-another-another-user-uploads").style.display = "none"; + $("msd-led").className = "led-gray"; + if (__state.in_operate) { + $("msd-status").innerHTML = $("msd-led").title = "Connected to KVM"; + } else { + $("msd-status").innerHTML = $("msd-led").title = "Unavailable"; + } + } + + $("msd-not-in-operate").style.display = (__state.in_operate ? "none" : "block"); + $("msd-current-image-broken").style.display = ( + __state.in_operate && __state.info.image && + !__state.info.image.complete && !__state.busy ? "block" : "none" + ); + + $("msd-current-image-name").innerHTML = (__state.in_operate && __state.info.image ? __state.info.image.name : "None"); + $("msd-current-image-size").innerHTML = (__state.in_operate && __state.info.image ? __formatSize(__state.info.image.size) : "None"); + $("msd-storage-size").innerHTML = (__state.in_operate ? __formatSize(__state.info.size) : "Unavailable"); + + wm.switchDisabled($("msd-switch-to-kvm-button"), (!__state.in_operate || __state.connected_to === "kvm" || __state.busy)); + wm.switchDisabled($("msd-switch-to-server-button"), (!__state.in_operate || __state.connected_to === "server" || __state.busy)); + wm.switchDisabled($("msd-select-new-image-button"), (!__state.in_operate || __state.connected_to !== "kvm" || __state.busy || __upload_http)); + wm.switchDisabled($("msd-upload-new-image-button"), (!__state.in_operate || __state.connected_to !== "kvm" || __state.busy || !__image_file)); + wm.switchDisabled($("msd-abort-uploading-button"), (!__state.in_operate || !__upload_http)); + wm.switchDisabled($("msd-reset-button"), (!__state.in_operate || __upload_http)); + + $("msd-new-image").style.display = (__image_file ? "block" : "none"); + $("msd-progress").setAttribute("data-label", "Waiting for upload ..."); + $("msd-progress-value").style.width = "0%"; + $("msd-new-image-name").innerHTML = (__image_file ? __image_file.name : ""); + $("msd-new-image-size").innerHTML = (__image_file ? __formatSize(__image_file.size) : ""); + + } else { + $("msd-another-another-user-uploads").style.display = "none"; + $("msd-led").className = "led-gray"; + $("msd-status").innerHTML = ""; + $("msd-led").title = ""; + $("msd-not-in-operate").style.display = "none"; + $("msd-current-image-broken").style.display = "none"; + $("msd-current-image-name").innerHTML = ""; + $("msd-current-image-size").innerHTML = ""; + $("msd-storage-size").innerHTML = ""; + + wm.switchDisabled($("msd-switch-to-kvm-button"), true); + wm.switchDisabled($("msd-switch-to-server-button"), true); + wm.switchDisabled($("msd-select-new-image-button"), true); + wm.switchDisabled($("msd-upload-new-image-button"), true); + wm.switchDisabled($("msd-abort-uploading-button"), true); + wm.switchDisabled($("msd-reset-button"), true); + + $("msd-select-new-image-file").value = ""; + $("msd-new-image").style.display = "none"; + $("msd-progress").setAttribute("data-label", ""); + $("msd-progress-value").style.width = "0%"; + $("msd-new-image-name").innerHTML = ""; + $("msd-new-image-size").innerHTML = ""; + } + }; + + var __formatSize = function(size) { + if (size > 0) { + var index = Math.floor( Math.log(size) / Math.log(1024) ); + return (size / Math.pow(1024, index)).toFixed(2) * 1 + " " + ["B", "kB", "MB", "GB", "TB"][index]; + } else { + return 0; + } + }; + + var __uploadStateChange = function() { + if (__upload_http.readyState === 4) { + if (__upload_http.status !== 200) { + wm.error("Can't upload image to the Mass Storage Device:<br>", __upload_http.responseText); + } + $("msd-select-new-image-file").value = ""; + __image_file = null; + __upload_http = null; + __applyState(); + } + }; + + var __uploadProgress = function(event) { + if(event.lengthComputable) { + var percent = Math.round((event.loaded * 100) / event.total); + $("msd-progress").setAttribute("data-label", percent + "%"); + $("msd-progress-value").style.width = percent + "%"; + } + }; + + __init__(); +} diff --git a/web/share/js/kvm/session.js b/web/share/js/kvm/session.js new file mode 100644 index 00000000..5ba43511 --- /dev/null +++ b/web/share/js/kvm/session.js @@ -0,0 +1,133 @@ +function Session() { + // var self = this; + + /********************************************************************************/ + + var __ws = null; + + var __ping_timer = null; + var __missed_heartbeats = 0; + + var __hid = new Hid(); + var __atx = new Atx(); + var __msd = new Msd(); + var __streamer = new Streamer(); + + var __init__ = function() { + __startSession(); + }; + + /********************************************************************************/ + + var __setKvmdInfo = function(state) { + if (state.meta) { + var text = JSON.stringify(state.meta, undefined, 4).replace(/ /g, " ").replace(/\n/g, "<br>"); + $("about-meta").innerHTML = ` + <span class="code-comment">// The Pi-KVM metadata.<br> + // You can get this json using handle <a target="_blank" href="/kvmd/info">/kvmd/info</a>.<br> + // In the standard configuration this data<br> + // is specified in the file /etc/kvmd/meta.yaml.</span><br> + <br> + ${text} + `; + if (state.meta.server && state.meta.server.host) { + $("kvmd-meta-server-host").innerHTML = "Server: " + state.meta.server.host; + document.title = "Pi-KVM Session: " + state.meta.server.host; + } else { + $("kvmd-meta-server-host").innerHTML = ""; + document.title = "Pi-KVM Session"; + } + } + + $("about-version-kvmd").innerHTML = state.version.kvmd; + $("about-version-streamer").innerHTML = `${state.version.streamer} (${state.streamer})`; + }; + + var __startSession = function() { + $("link-led").className = "led-yellow"; + $("link-led").title = "Connecting..."; + var proto = (location.protocol === "https:" ? "wss" : "ws"); + __ws = new WebSocket(`${proto}://${location.host}/kvmd/ws`); + __ws.onopen = __wsOpenHandler; + __ws.onmessage = __wsMessageHandler; + __ws.onerror = __wsErrorHandler; + __ws.onclose = __wsCloseHandler; + }; + + var __wsOpenHandler = function(event) { + tools.debug("Session: socket opened:", event); + $("link-led").className = "led-green"; + $("link-led").title = "Connected"; + __hid.setSocket(__ws); + __missed_heartbeats = 0; + __ping_timer = setInterval(__pingServer, 1000); + }; + + var __wsMessageHandler = function(event) { + // tools.debug("Session: received socket data:", event.data); + event = JSON.parse(event.data); + if (event.msg_type === "pong") { + __missed_heartbeats = 0; + } else if (event.msg_type === "event") { + if (event.msg.event === "info_state") { + __setKvmdInfo(event.msg.event_attrs); + } else if (event.msg.event === "atx_state") { + __atx.setState(event.msg.event_attrs); + } else if (event.msg.event === "msd_state") { + __msd.setState(event.msg.event_attrs); + } else if (event.msg.event === "streamer_state") { + __streamer.setState(event.msg.event_attrs); + } + } + }; + + var __wsErrorHandler = function(event) { + tools.error("Session: socket error:", event); + if (__ws) { + __ws.onclose = null; + __ws.close(); + __wsCloseHandler(null); + } + }; + + var __wsCloseHandler = function(event) { + tools.debug("Session: socket closed:", event); + + $("link-led").className = "led-gray"; + + if (__ping_timer) { + clearInterval(__ping_timer); + __ping_timer = null; + } + + __hid.setSocket(null); + __atx.setState(null); + __msd.setState(null); + __streamer.setState(null); + __ws = null; + + setTimeout(function() { + $("link-led").className = "led-yellow"; + setTimeout(__startSession, 500); + }, 500); + }; + + var __pingServer = function() { + try { + __missed_heartbeats += 1; + if (__missed_heartbeats >= 5) { + throw new Error("Too many missed heartbeats"); + } + __ws.send(JSON.stringify({"event_type": "ping"})); + } catch (err) { + tools.error("Session: ping error:", err.message); + if (__ws) { + __ws.onclose = null; + __ws.close(); + __wsCloseHandler(null); + } + } + }; + + __init__(); +} diff --git a/web/share/js/kvm/stream.js b/web/share/js/kvm/stream.js new file mode 100644 index 00000000..01ded5bc --- /dev/null +++ b/web/share/js/kvm/stream.js @@ -0,0 +1,221 @@ +function Streamer() { + var self = this; + + /********************************************************************************/ + + var __resolution = {width: 640, height: 480}; + var __size_factor = 1; + var __client_key = tools.makeId(); + var __client_id = ""; + var __client_fps = -1; + var __prev = false; + + var __init__ = function() { + $("stream-led").title = "Stream inactive"; + + $("stream-quality-slider").min = 5; + $("stream-quality-slider").max = 100; + $("stream-quality-slider").step = 5; + $("stream-quality-slider").value = 80; + tools.setOnUpSlider($("stream-quality-slider"), 1000, __updateQualityValue, (value) => __sendParam("quality", value)); + + $("stream-desired-fps-slider").min = 0; + $("stream-desired-fps-slider").max = 30; + $("stream-desired-fps-slider").step = 1; + $("stream-desired-fps-slider").value = 0; + tools.setOnUpSlider($("stream-desired-fps-slider"), 1000, __updateDesiredFpsValue, (value) => __sendParam("desired_fps", value)); + + $("stream-size-slider").min = 20; + $("stream-size-slider").max = 200; + $("stream-size-slider").step = 5; + $("stream-size-slider").value = 100; + $("stream-size-slider").oninput = () => __resize(); + $("stream-size-slider").onchange = () => __resize(); + + tools.setOnClick($("stream-screenshot-button"), __clickScreenshotButton); + tools.setOnClick($("stream-reset-button"), __clickResetButton); + }; + + /********************************************************************************/ + + self.setState = function(state) { + if (state && state.state) { + var source = state.state.source; + var stream = state.state.stream; + + if (!__prev) { + $("stream-quality-slider").activated = false; + $("stream-desired-fps-slider").activated = false; + } + + if (!$("stream-quality-slider").activated) { + wm.switchDisabled($("stream-quality-slider"), false); + if ($("stream-quality-slider").value !== source.quality) { + $("stream-quality-slider").value = source.quality; + __updateQualityValue(source.quality); + } + } + + if (!$("stream-desired-fps-slider").activated) { + wm.switchDisabled($("stream-desired-fps-slider"), false); + if ($("stream-desired-fps-slider").value !== source.desired_fps) { + $("stream-desired-fps-slider").value = source.desired_fps; + __updateDesiredFpsValue(source.desired_fps); + } + } + + if (__resolution.width !== source.resolution.width || __resolution.height !== source.resolution.height) { + __resolution = source.resolution; + if ($("stream-auto-resize-checkbox").checked) { + __adjustSizeFactor(); + } else { + __applySizeFactor(); + } + } + + var stream_client = tools.getCookie("stream_client"); + if (!__client_id && stream_client && stream_client.startsWith(__client_key + "/")) { + tools.info("Stream: found acceptable stream_client cookie:", stream_client); + __client_id = stream_client.slice(stream_client.indexOf("/") + 1); + } + + if (stream.clients_stat.hasOwnProperty(__client_id)) { + __client_fps = stream.clients_stat[__client_id].fps; + } else { + __clearState(); + } + + if (!__prev) { + var path = "/streamer/stream?key=" + __client_key; + if (tools.browser.is_chrome || tools.browser.is_blink) { + // uStreamer fix for Blink https://bugs.chromium.org/p/chromium/issues/detail?id=527446 + tools.info("Stream: using advance_headers=1 to fix Blink MJPG bugs"); + path += "&advance_headers=1"; + } else if (tools.browser.is_safari || tools.browser.is_ios) { + // uStreamer fix for WebKit + tools.info("Stream: using dual_final_frames=1 to fix WebKit MJPG bugs"); + path += "&dual_final_frames=1"; + } + $("stream-image").src = path; + $("stream-image").className = "stream-image-active"; + $("stream-box").classList.remove("stream-box-inactive"); + $("stream-led").className = "led-green"; + $("stream-led").title = "Stream is active"; + wm.switchDisabled($("stream-screenshot-button"), false); + wm.switchDisabled($("stream-reset-button"), false); + tools.info("Stream: acquired"); + __prev = true; + } + + __updateStreamHeader(true); + + } else { + __clearState(); + } + }; + + var __clearState = function() { + tools.info("Stream: refreshing ..."); + + $("stream-image").className = "stream-image-inactive"; + $("stream-box").classList.add("stream-box-inactive"); + $("stream-led").className = "led-gray"; + $("stream-led").title = "Stream inactive"; + wm.switchDisabled($("stream-screenshot-button"), true); + wm.switchDisabled($("stream-reset-button"), true); + wm.switchDisabled($("stream-quality-slider"), true); + wm.switchDisabled($("stream-desired-fps-slider"), true); + + __client_key = tools.makeId(); + __client_id = ""; + __client_fps = -1; + __prev = false; + __updateStreamHeader(false); + }; + + var __updateQualityValue = function(value) { + $("stream-quality-value").innerHTML = value + "%"; + }; + + var __updateDesiredFpsValue = function(value) { + $("stream-desired-fps-value").innerHTML = (value === 0 ? "Unlimited" : value); + }; + + var __updateStreamHeader = function(online) { + var el_grab = document.querySelector("#stream-window-header .window-grab"); + var el_info = $("stream-info"); + if (online) { + var fps_suffix = (__client_fps >= 0 ? ` / ${__client_fps} fps` : ""); + el_grab.innerHTML = el_info.innerHTML = `Stream – ${__resolution.width}x${__resolution.height}${fps_suffix}`; + } else { + el_grab.innerHTML = el_info.innerHTML = "Stream – offline"; + } + }; + + var __clickScreenshotButton = function() { + var el_a = document.createElement("a"); + el_a.href = "/streamer/snapshot"; + el_a.target = "_blank"; + document.body.appendChild(el_a); + el_a.click(); + setTimeout(() => document.body.removeChild(el_a), 0); + }; + + var __clickResetButton = function() { + var http = tools.makeRequest("POST", "/kvmd/streamer/reset", function() { + if (http.readyState === 4) { + if (http.status !== 200) { + wm.error("Can't reset stream:<br>", http.responseText); + } + } + }); + }; + + var __sendParam = function(name, value) { + var http = tools.makeRequest("POST", `/kvmd/streamer/set_params?${name}=${value}`, function() { + if (http.readyState === 4) { + if (http.status !== 200) { + wm.error("Can't configure stream:<br>", http.responseText); + } + } + }); + }; + + var __resize = function(center=false) { + var size = $("stream-size-slider").value; + $("stream-size-value").innerHTML = size + "%"; + __size_factor = size / 100; + __applySizeFactor(center); + }; + + var __adjustSizeFactor = function() { + var el_window = $("stream-window"); + var el_slider = $("stream-size-slider"); + var view = wm.getViewGeometry(); + + for (var size = 100; size >= el_slider.min; size -= el_slider.step) { + tools.info("Stream: adjusting size:", size); + $("stream-size-slider").value = size; + __resize(true); + + var rect = el_window.getBoundingClientRect(); + if ( + rect.bottom <= view.bottom + && rect.top >= view.top + && rect.left >= view.left + && rect.right <= view.right + ) { + break; + } + } + }; + + var __applySizeFactor = function(center=false) { + var el_stream_image = $("stream-image"); + el_stream_image.style.width = __resolution.width * __size_factor + "px"; + el_stream_image.style.height = __resolution.height * __size_factor + "px"; + wm.showWindow($("stream-window"), false, center); + }; + + __init__(); +} diff --git a/web/share/js/login/main.js b/web/share/js/login/main.js new file mode 100644 index 00000000..dc45d03f --- /dev/null +++ b/web/share/js/login/main.js @@ -0,0 +1,36 @@ +function main() { + if (checkBrowser()) { + tools.setOnClick($("login-button"), __login); + document.onkeyup = function(event) { + if (event.code == "Enter") { + event.preventDefault(); + __login(); + } + }; + $("user-input").focus(); + } +} + +function __login() { + var user = $("user-input").value; + var passwd = $("passwd-input").value; + var body = `user=${encodeURIComponent(user)}&passwd=${encodeURIComponent(passwd)}`; + var http = tools.makeRequest("POST", "/kvmd/auth/login", function() { + if (http.readyState === 4) { + if (http.status === 200) { + document.location.href = "/"; + } + __setDisabled(false); + $("passwd-input").focus(); + $("passwd-input").select(); + } + }, body, "application/x-www-form-urlencoded"); + http.send(); + __setDisabled(true); +} + +function __setDisabled(disabled) { + $("user-input").disabled = disabled; + $("passwd-input").disabled = disabled; + $("login-button").disabled = disabled; +} diff --git a/web/share/js/tools.js b/web/share/js/tools.js new file mode 100644 index 00000000..80b074ee --- /dev/null +++ b/web/share/js/tools.js @@ -0,0 +1,136 @@ +var tools = new function() { + var __debug = (new URL(window.location.href)).searchParams.get("debug"); + + this.makeRequest = function(method, url, callback, body=null, content_type=null) { + var http = new XMLHttpRequest(); + http.open(method, url, true); + if (content_type) { + http.setRequestHeader("Content-Type", content_type); + } + http.onreadystatechange = callback; + http.timeout = 5000; + http.send(body); + return http; + }; + + this.makeId = function() { + var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + var id = ""; + for (var count = 0; count < 16; ++count) { + id += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return id; + }; + + this.getCookie = function(name) { + var matches = document.cookie.match(new RegExp( + "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, "\\$1") + "=([^;]*)" // eslint-disable-line no-useless-escape + )); + return (matches ? decodeURIComponent(matches[1]) : ""); + }; + + this.setOnClick = function(el, callback) { + el.onclick = el.ontouchend = function(event) { + event.preventDefault(); + callback(); + }; + }; + this.setOnDown = function(el, callback) { + el.onmousedown = el.ontouchstart = function(event) { + event.preventDefault(); + callback(); + }; + }; + this.setOnUp = function(el, callback) { + el.onmouseup = el.ontouchend = function(event) { + event.preventDefault(); + callback(); + }; + }; + + this.setOnUpSlider = function(el, delay, display_callback, execute_callback) { + el.execution_timer = null; + el.activated = false; + + var clear_timer = function() { + if (el.execution_timer) { + clearTimeout(el.execution_timer); + el.execution_timer = null; + } + }; + + el.oninput = el.onchange = () => display_callback(el.value); + + el.onmousedown = el.ontouchstart = function() { + clear_timer(); + el.activated = true; + }; + + el.onmouseup = el.ontouchend = function(event) { + event.preventDefault(); + clear_timer(); + el.execution_timer = setTimeout(function() { + execute_callback(el.value); + }, delay); + }; + }; + + this.debug = function(...args) { + if (__debug) { + console.log("LOG/DEBUG", ...args); // eslint-disable-line no-console + } + }; + this.info = (...args) => console.log("LOG/INFO", ...args); // eslint-disable-line no-console + this.error = (...args) => console.error("LOG/ERROR", ...args); // eslint-disable-line no-console + + this.browser = new function() { + // https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser/9851769 + + // Opera 8.0+ + var is_opera = ( + (!!window.opr && !!opr.addons) // eslint-disable-line no-undef + || !!window.opera + || (navigator.userAgent.indexOf(" OPR/") >= 0) + ); + + // Firefox 1.0+ + var is_firefox = (typeof InstallTrigger !== "undefined"); + + // Safari 3.0+ "[object HTMLElementConstructor]" + var is_safari = (/constructor/i.test(window.HTMLElement) || (function (p) { + return p.toString() === "[object SafariRemoteNotification]"; + })(!window["safari"] || (typeof safari !== "undefined" && safari.pushNotification))); // eslint-disable-line no-undef + + // Chrome 1+ + var is_chrome = (!!window.chrome && !!window.chrome.webstore); + + // Blink engine detection + var is_blink = ((is_chrome || is_opera) && !!window.CSS); + + // iOS browsers + // https://stackoverflow.com/questions/9038625/detect-if-device-is-ios + var is_ios = (!!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform)); + + // Any browser on Mac + var is_mac = (( + window.navigator.oscpu + || window.navigator.platform + || window.navigator.appVersion + || "Unknown" + ).indexOf("Mac") !== -1); + + return { + "is_opera": is_opera, + "is_firefox": is_firefox, + "is_safari": is_safari, + "is_chrome": is_chrome, + "is_blink": is_blink, + "is_ios": is_ios, + "is_mac": is_mac, + }; + }; + this.info("Browser:", this.browser); +}; + +var $ = (id) => document.getElementById(id); +var $$ = (cls) => document.getElementsByClassName(cls); diff --git a/web/share/js/wm.js b/web/share/js/wm.js new file mode 100644 index 00000000..c292ece7 --- /dev/null +++ b/web/share/js/wm.js @@ -0,0 +1,387 @@ +function WindowManager() { + var self = this; + + /********************************************************************************/ + + var __top_z_index = 0; + var __windows = []; + var __menu_items = []; + + var __init__ = function() { + Array.prototype.forEach.call(document.querySelectorAll("button"), function(el_button) { + // XXX: Workaround for iOS Safari: + // https://stackoverflow.com/questions/3885018/active-pseudo-class-doesnt-work-in-mobile-safari + el_button.ontouchstart = function() {}; + }); + + Array.prototype.forEach.call($$("menu-item"), function(el_item) { + el_item.parentElement.querySelector(".menu-item-content").setAttribute("tabindex", "-1"); + tools.setOnDown(el_item, () => __toggleMenu(el_item)); + __menu_items.push(el_item); + }); + + Array.prototype.forEach.call($$("window"), function(el_window) { + el_window.setAttribute("tabindex", "-1"); + __makeWindowMovable(el_window); + __windows.push(el_window); + + var el_button = el_window.querySelector(".window-header .window-button-close"); + if (el_button) { + tools.setOnClick(el_button, function() { + el_window.style.visibility = "hidden"; + __activateLastWindow(el_window); + }); + } + }); + + window.onmouseup = __globalMouseButtonHandler; + window.ontouchend = __globalMouseButtonHandler; + + window.addEventListener("focusin", __focusIn); + window.addEventListener("focusout", __focusOut); + + window.addEventListener("resize", () => __organizeWindowsOnResize(false)); + window.addEventListener("orientationchange", () => __organizeWindowsOnResize(true)); + }; + + /********************************************************************************/ + + self.error = (...args) => __modalDialog("Error", args.join(" "), true, false); + self.confirm = (...args) => __modalDialog("Question", args.join(" "), true, true); + + var __modalDialog = function(header, text, ok, cancel) { + var el_modal = document.createElement("div"); + el_modal.className = "modal"; + el_modal.style.visibility = "visible"; + + var el_window = document.createElement("div"); + el_window.className = "modal-window"; + el_window.setAttribute("tabindex", "-1"); + el_modal.appendChild(el_window); + + var el_header = document.createElement("div"); + el_header.className = "modal-header"; + el_header.innerHTML = header; + el_window.appendChild(el_header); + + var el_content = document.createElement("div"); + el_content.className = "modal-content"; + el_content.innerHTML = text; + el_window.appendChild(el_content); + + var promise = null; + if (ok || cancel) { + promise = new Promise(function(resolve) { + var el_buttons = document.createElement("div"); + el_buttons.className = "modal-buttons"; + el_window.appendChild(el_buttons); + + function close(retval) { + el_window.style.visibility = "hidden"; + el_modal.outerHTML = ""; + var index = __windows.indexOf(el_modal); + if (index !== -1) { + __windows.splice(index, 1); + } + __activateLastWindow(el_modal); + resolve(retval); + } + + if (cancel) { + var el_cancel_button = document.createElement("button"); + el_cancel_button.innerHTML = "Cancel"; + tools.setOnClick(el_cancel_button, () => close(false)); + el_buttons.appendChild(el_cancel_button); + } + if (ok) { + var el_ok_button = document.createElement("button"); + el_ok_button.innerHTML = "OK"; + tools.setOnClick(el_ok_button, () => close(true)); + el_buttons.appendChild(el_ok_button); + } + if (ok && cancel) { + el_ok_button.className = "row50"; + el_cancel_button.className = "row50"; + } + + el_window.onkeyup = function(event) { + event.preventDefault(); + if (ok && event.code === "Enter") { + el_ok_button.click(); + } else if (cancel && event.code === "Escape") { + el_cancel_button.click(); + } + }; + }); + } + + __windows.push(el_modal); + document.body.appendChild(el_modal); + __activateWindow(el_modal); + + return promise; + }; + + self.switchDisabled = function(el, disabled) { + if (disabled && document.activeElement === el) { + var el_to_focus = ( + el.closest(".modal-window") + || el.closest(".window") + || el.closest(".menu-item-content") + ); + if (el_to_focus) { + el_to_focus.focus(); + } + } + el.disabled = disabled; + }; + + self.showWindow = function(el_window, activate=true, center=false) { + if (!__isWindowOnPage(el_window) || el_window.hasAttribute("data-centered") || center) { + var view = self.getViewGeometry(); + var rect = el_window.getBoundingClientRect(); + + el_window.style.top = Math.max($("menu").clientHeight, Math.round((view.bottom - rect.height) / 2)) + "px"; + el_window.style.left = Math.round((view.right - rect.width) / 2) + "px"; + el_window.setAttribute("data-centered", ""); + } + + el_window.style.visibility = "visible"; + if (activate) { + __activateWindow(el_window); + } + }; + + self.getViewGeometry = function() { + return { + top: $("menu").clientHeight, + bottom: Math.max(document.documentElement.clientHeight, window.innerHeight || 0), + left: 0, + right: Math.max(document.documentElement.clientWidth, window.innerWidth || 0), + }; + }; + + var __isWindowOnPage = function(el_window) { + var view = self.getViewGeometry(); + var rect = el_window.getBoundingClientRect(); + + return ( + (rect.bottom - el_window.clientHeight / 1.5) <= view.bottom + && rect.top >= view.top + && (rect.left + el_window.clientWidth / 1.5) >= view.left + && (rect.right - el_window.clientWidth / 1.5) <= view.right + ); + }; + + var __toggleMenu = function(el_a) { + var all_hidden = true; + + __menu_items.forEach(function(el_item) { + var el_menu = el_item.parentElement.querySelector(".menu-item-content"); + if (el_item === el_a && window.getComputedStyle(el_menu, null).visibility === "hidden") { + el_item.classList.add("menu-item-selected"); + el_menu.style.visibility = "visible"; + el_menu.focus(); + all_hidden &= false; + } else { + el_item.classList.remove("menu-item-selected"); + el_menu.style.visibility = "hidden"; + } + }); + + if (all_hidden) { + document.onkeyup = null; + __activateLastWindow(); + } else { + document.onkeyup = function(event) { + if (event.code === "Escape") { + event.preventDefault(); + __closeAllMenues(); + __activateLastWindow(); + } + }; + } + }; + + var __closeAllMenues = function() { + document.onkeyup = null; + __menu_items.forEach(function(el_item) { + var el_menu = el_item.parentElement.querySelector(".menu-item-content"); + el_item.classList.remove("menu-item-selected"); + el_menu.style.visibility = "hidden"; + }); + }; + + var __focusIn = function(event) { + var el_parent; + if ((el_parent = event.target.closest(".modal-window")) !== null) { + el_parent.classList.add("window-active"); + } else if ((el_parent = event.target.closest(".window")) !== null) { + el_parent.classList.add("window-active"); + } else if ((el_parent = event.target.closest(".menu-item-content")) !== null) { + el_parent.classList.add("menu-item-content-active"); + } + tools.debug("Focus in:", el_parent); + }; + + var __focusOut = function(event) { + var el_parent; + if ((el_parent = event.target.closest(".modal-window")) !== null) { + el_parent.classList.remove("window-active"); + } else if ((el_parent = event.target.closest(".window")) !== null) { + el_parent.classList.remove("window-active"); + } else if ((el_parent = event.target.closest(".menu-item-content")) !== null) { + el_parent.classList.remove("menu-item-content-active"); + } + tools.debug("Focus out:", el_parent); + }; + + var __globalMouseButtonHandler = function(event) { + if (!event.target.matches(".menu-item")) { + for (var el_item = event.target; el_item && el_item !== document; el_item = el_item.parentNode) { + if (el_item.hasAttribute("data-force-hide-menu")) { + break; + } else if (el_item.hasAttribute("data-dont-hide-menu")) { + return; + } + } + __closeAllMenues(); + __activateLastWindow(); + } + }; + + var __organizeWindowsOnResize = function(orientation) { + var view = self.getViewGeometry(); + + Array.prototype.forEach.call($$("window"), function(el_window) { + if (el_window.style.visibility === "visible" && (orientation || el_window.hasAttribute("data-centered"))) { + var rect = el_window.getBoundingClientRect(); + + el_window.style.top = Math.max($("menu").clientHeight, Math.round((view.bottom - rect.height) / 2)) + "px"; + el_window.style.left = Math.round((view.right - rect.width) / 2) + "px"; + el_window.setAttribute("data-centered", ""); + } + }); + }; + + var __activateLastWindow = function(el_except_window=null) { + var el_last_window = null; + + if (document.activeElement) { + el_last_window = (document.activeElement.closest(".modal-window") || document.activeElement.closest(".window")); + } + + if (!el_last_window || el_last_window === el_except_window) { + var max_z_index = 0; + + __windows.forEach(function(el_window) { + var z_index = parseInt(window.getComputedStyle(el_window, null).zIndex) || 0; + var visibility = window.getComputedStyle(el_window, null).visibility; + + if (max_z_index < z_index && visibility !== "hidden" && el_window !== el_except_window) { + el_last_window = el_window; + max_z_index = z_index; + } + }); + } + + if (el_last_window) { + tools.debug("Activating last window:", el_last_window); + __activateWindow(el_last_window); + } + }; + + var __activateWindow = function(el_window) { + if (window.getComputedStyle(el_window, null).visibility !== "hidden") { + var el_to_focus; + var el_window_contains_focus; + + if (el_window.className === "modal") { + el_to_focus = el_window.querySelector(".modal-window"); + el_window_contains_focus = (document.activeElement && document.activeElement.closest(".modal-window")); + } else { // .window + el_to_focus = el_window; + el_window_contains_focus = (document.activeElement && document.activeElement.closest(".window")); + } + + if (el_window.className !== "modal" && parseInt(el_window.style.zIndex) !== __top_z_index) { + __top_z_index += 1; + el_window.style.zIndex = __top_z_index; + tools.debug("UI: activated window:", el_window); + } + + if (el_window !== el_window_contains_focus) { + el_to_focus.focus(); + tools.debug("UI: focused window:", el_window); + } + } + }; + + var __makeWindowMovable = function(el_window) { + var el_header = el_window.querySelector(".window-header"); + var el_grab = el_window.querySelector(".window-header .window-grab"); + + var prev_pos = {x: 0, y: 0}; + + function startMoving(event) { + __closeAllMenues(); + __activateWindow(el_window); + event = (event || window.event); + event.preventDefault(); + + if (!event.touches || event.touches.length === 1) { + el_header.classList.add("window-header-grabbed"); + + prev_pos = getEventPosition(event); + + document.onmousemove = doMoving; + document.onmouseup = stopMoving; + + document.ontouchmove = doMoving; + document.ontouchend = stopMoving; + } + } + + function doMoving(event) { + el_window.removeAttribute("data-centered"); + + event = (event || window.event); + event.preventDefault(); + + var event_pos = getEventPosition(event); + var x = prev_pos.x - event_pos.x; + var y = prev_pos.y - event_pos.y; + + el_window.style.top = (el_window.offsetTop - y) + "px"; + el_window.style.left = (el_window.offsetLeft - x) + "px"; + + prev_pos = event_pos; + } + + function stopMoving() { + el_header.classList.remove("window-header-grabbed"); + + document.onmousemove = null; + document.onmouseup = null; + + document.ontouchmove = null; + document.ontouchend = null; + } + + function getEventPosition(event) { + if (event.touches) { + return {x: event.touches[0].clientX, y: event.touches[0].clientY}; + } else { + return {x: event.clientX, y: event.clientY}; + } + } + + el_window.setAttribute("data-centered", ""); + el_window.onclick = el_window.ontouchend = () => __activateWindow(el_window); + + el_grab.onmousedown = startMoving; + el_grab.ontouchstart = startMoving; + }; + + __init__(); +} |