From f4879721ac1d4b1347d58445fd00d4c90a4f2e1a Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Sat, 3 Feb 2024 10:16:03 -0800 Subject: [PATCH 01/14] Cleanup JS just a bit - Remove ability to connect to arbitrary hosts and ports via query params. We already required websockify anyway, so this never worked. Plus, we only care about using the viewer for our own purposes, not as a general purpose novnc thing - Get rid of the ctrlAltDelete function. It's not particularly useful in this context. I will work on adding more UI here, but this location is a good place for other buttons --- js/index.js | 79 +------------------ .../static/index.html | 2 - 2 files changed, 4 insertions(+), 77 deletions(-) diff --git a/js/index.js b/js/index.js index 3a3df481..42911b5c 100644 --- a/js/index.js +++ b/js/index.js @@ -24,100 +24,29 @@ function disconnectedFromServer(e) { } } -// When this function is called, the server requires -// credentials to authenticate -function credentialsAreRequired(e) { - const password = prompt("Password Required:"); - rfb.sendCredentials({ password: password }); -} - // When this function is called we have received // a desktop name from the server function updateDesktopName(e) { desktopName = e.detail.name; } -// Since most operating systems will catch Ctrl+Alt+Del -// before they get a chance to be intercepted by the browser, -// we provide a way to emulate this key sequence. -function sendCtrlAltDel() { - rfb.sendCtrlAltDel(); - return false; -} - // Show a status text in the top bar function status(text) { document.getElementById("status").textContent = text; } -// This function extracts the value of one variable from the -// query string. If the variable isn't defined in the URL -// it returns the default value instead. -function readQueryVariable(name, defaultValue) { - // A URL with a query parameter can look like this: - // https://www.example.com?myqueryparam=myvalue - // - // Note that we use location.href instead of location.search - // because Firefox < 53 has a bug w.r.t location.search - const re = new RegExp(".*[?&]" + name + "=([^&#]*)"), - match = document.location.href.match(re); - - if (match) { - // We have to decode the URL since want the cleartext value - return decodeURIComponent(match[1]); - } - - return defaultValue; -} - -document.getElementById("sendCtrlAltDelButton").onclick = sendCtrlAltDel; - -// Read parameters specified in the URL query string -// By default, use the host and port of server that served this file -const host = readQueryVariable("host", window.location.hostname); -let port = readQueryVariable("port", window.location.port); -const password = readQueryVariable("password"); - -const path = readQueryVariable( - "path", - window.location.pathname.replace(/[^/]*$/, "").substring(1) + "websockify", -); - -// | | | | | | -// | | | Connect | | | -// v v v v v v - -status("Connecting"); - -// Build the websocket URL used to connect -let url; -if (window.location.protocol === "https:") { - url = "wss"; -} else { - url = "ws"; -} -url += "://" + host; -if (port) { - url += ":" + port; -} -url += "/" + path; +// Construct the websockify websocket URL we want to connect to +let websockifyUrl = new URL("websockify", window.location); +websockifyUrl.protocol = window.location.protocol === "https" ? "wss" : "ws"; // Creating a new RFB object will start a new connection -rfb = new RFB(document.getElementById("screen"), url, { - credentials: { password: password }, -}); +rfb = new RFB(document.getElementById("screen"), websockifyUrl.toString(), {}); // Add listeners to important events from the RFB module rfb.addEventListener("connect", connectedToServer); rfb.addEventListener("disconnect", disconnectedFromServer); -rfb.addEventListener("credentialsrequired", credentialsAreRequired); rfb.addEventListener("desktopname", updateDesktopName); -// Set parameters that can be changed on an active connection -rfb.viewOnly = readQueryVariable("view_only", false); - -rfb.scaleViewport = readQueryVariable("scale", true); - // Clipboard function toggleClipboardPanel() { document diff --git a/jupyter_remote_desktop_proxy/static/index.html b/jupyter_remote_desktop_proxy/static/index.html index 376f261e..d8d5021d 100644 --- a/jupyter_remote_desktop_proxy/static/index.html +++ b/jupyter_remote_desktop_proxy/static/index.html @@ -30,8 +30,6 @@ - -
Send CtrlAltDel
From 21efc204cd48416b4699516a353576a2d42dd707 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Sat, 3 Feb 2024 10:21:24 -0800 Subject: [PATCH 02/14] Add eslint & fix the one issue it found --- .pre-commit-config.yaml | 9 +++++++++ js/index.js | 2 +- package.json | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9a0c50bd..cf374582 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -68,6 +68,15 @@ repos: hooks: - id: flake8 + # Lint: JS code + - repo: https://github.com/pre-commit/mirrors-eslint + rev: "v8.56.0" # Use the sha / tag you want to point at + hooks: + - id: eslint + files: \.jsx?$ + types: [file] + exclude: jupyter_remote_desktop_proxy/static/dist + # Content here is mostly copied from other locations, so lets not make # formatting changes in it. exclude: share diff --git a/js/index.js b/js/index.js index 42911b5c..ff35c679 100644 --- a/js/index.js +++ b/js/index.js @@ -11,7 +11,7 @@ let desktopName; // When this function is called we have // successfully connected to a server -function connectedToServer(e) { +function connectedToServer() { status("Connected to " + desktopName); } diff --git a/package.json b/package.json index 11c795b8..115ff4e3 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "devDependencies": { "babel-loader": "^9.1.3", + "eslint": "^8.56.0", "webpack": "^5.90.1", "webpack-cli": "^5.1.4" } From 389f265b4336ad8c2a506abdc52ce5a542ab316a Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Sat, 3 Feb 2024 10:22:19 -0800 Subject: [PATCH 03/14] Add missing eslintrc file --- .eslintrc.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .eslintrc.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..fdb4321a --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,30 @@ +module.exports = { + env: { + browser: true, + es2021: true, + }, + extends: ["eslint:recommended"], + overrides: [ + { + env: { + node: true, + }, + files: [".eslintrc.{js,cjs}"], + parserOptions: { + sourceType: "script", + }, + }, + ], + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + }, + plugins: [], + rules: { + "no-unused-vars": ["error", { args: "after-used" }], + }, + ignorePatterns: [ + "jupyter_remote_desktop_proxy/static/dist/**", + "webpack.config.js", + ], +}; From 7674f14870606c15615ad5c1c46f29d10a6b5d26 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Sat, 3 Feb 2024 10:30:47 -0800 Subject: [PATCH 04/14] Stop showing the desktop name This is almost always a random sequence of strings and is not useful. --- js/index.js | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/js/index.js b/js/index.js index ff35c679..4cbd5a20 100644 --- a/js/index.js +++ b/js/index.js @@ -6,13 +6,9 @@ // RFB holds the API to connect and communicate with a VNC server import RFB from "@novnc/novnc/core/rfb"; -let rfb; -let desktopName; - -// When this function is called we have -// successfully connected to a server +// When this function is called we have successfully connected to a server function connectedToServer() { - status("Connected to " + desktopName); + status("Connected!"); } // This function is called when we are disconnected @@ -24,12 +20,6 @@ function disconnectedFromServer(e) { } } -// When this function is called we have received -// a desktop name from the server -function updateDesktopName(e) { - desktopName = e.detail.name; -} - // Show a status text in the top bar function status(text) { document.getElementById("status").textContent = text; @@ -40,12 +30,15 @@ let websockifyUrl = new URL("websockify", window.location); websockifyUrl.protocol = window.location.protocol === "https" ? "wss" : "ws"; // Creating a new RFB object will start a new connection -rfb = new RFB(document.getElementById("screen"), websockifyUrl.toString(), {}); +const rfb = new RFB( + document.getElementById("screen"), + websockifyUrl.toString(), + {}, +); // Add listeners to important events from the RFB module rfb.addEventListener("connect", connectedToServer); rfb.addEventListener("disconnect", disconnectedFromServer); -rfb.addEventListener("desktopname", updateDesktopName); // Clipboard function toggleClipboardPanel() { From b411bf2e7c638e836f163e2e0ff9b2a67dbd60a9 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Sat, 3 Feb 2024 12:19:00 -0800 Subject: [PATCH 05/14] Set scaleViewPort to true Was missed when I undid all the queryparam stuff --- js/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/index.js b/js/index.js index 4cbd5a20..bf262251 100644 --- a/js/index.js +++ b/js/index.js @@ -40,6 +40,9 @@ const rfb = new RFB( rfb.addEventListener("connect", connectedToServer); rfb.addEventListener("disconnect", disconnectedFromServer); +// Scale our viewport so the user doesn't have to scroll +rfb.scaleViewport = true; + // Clipboard function toggleClipboardPanel() { document From f932075b52ace07f0b0e117284d02ededcdc8c5f Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Sat, 3 Feb 2024 13:36:04 -0800 Subject: [PATCH 06/14] Cleanup styles - Include a CSS reset - Use Jupyter Brand Colors - Add a helpful bit of info for clipboard popout --- js/index.css | 86 +++++++++++++++++++ js/index.js | 15 ++-- jupyter_remote_desktop_proxy/static/index.css | 79 ----------------- .../static/index.html | 28 +++--- package.json | 6 +- webpack.config.js | 8 +- 6 files changed, 121 insertions(+), 101 deletions(-) create mode 100644 js/index.css delete mode 100644 jupyter_remote_desktop_proxy/static/index.css diff --git a/js/index.css b/js/index.css new file mode 100644 index 00000000..a0e10f78 --- /dev/null +++ b/js/index.css @@ -0,0 +1,86 @@ +/** +* Derived from https://github.com/novnc/noVNC/blob/v1.4.0/vnc_lite.html, which was licensed +* under the 2-clause BSD license +*/ + +html { + /** + Colors from https://github.com/jupyter/design/blob/main/brandguide/brand_guide.pdf + **/ + --jupyter-main-brand-color: #f37626; + --jupyter-dark-grey: #4d4d4d; + --jupyter-medium-dark-grey: #616161; + --jupyter-medium-grey: #757575; + --jupyter-grey: #9e9e9e; + + --topbar-height: 32px; +} + +body { + height: 100vh; + display: flex; + flex-direction: column; + background-color: var(--jupyter-medium-dark-grey); +} + +#top-bar { + background-color: var(--jupyter-main-brand-color); + color: white; + font: bold 12px Helvetica; + border-bottom: 1px white; + display: flex; + align-items: center; + height: var(--topbar-height); +} + +#status { + text-align: center; + flex-grow: 1; +} + +#screen { + flex: 1; + /* fill remaining space */ + overflow: hidden; +} + +.hidden { + display: none !important; +} + +/* Clipboard */ +#clipboard-area { + position: fixed; + top: var(--topbar-height); + left: 0px; + padding: 4px; +} + +#clipboard-button { + border: 1px outset; + cursor: pointer; +} + +#clipboard-button img { + height: 24px; + vertical-align: middle; +} + +#clipboard-button .label { + padding: 5px 5px 4px 0px; +} + +#clipboard-area { + /* Full screen, minus padding and left and right margins */ + display: flex; + flex-direction: column; + background-color: white; + border-bottom-right-radius: 4px; + border: 1px solid var(--jupyter-medium-grey); +} + +#clipboard-text { + min-width: 500px; + max-width: 100%; + border: 1px solid var(--jupyter-medium-grey); +} diff --git a/js/index.js b/js/index.js index bf262251..185645a9 100644 --- a/js/index.js +++ b/js/index.js @@ -3,6 +3,9 @@ * under the 2-clause BSD license */ +import "reset-css"; +import "./index.css"; + // RFB holds the API to connect and communicate with a VNC server import RFB from "@novnc/novnc/core/rfb"; @@ -45,23 +48,21 @@ rfb.scaleViewport = true; // Clipboard function toggleClipboardPanel() { - document - .getElementById("noVNC_clipboard_area") - .classList.toggle("noVNC_clipboard_closed"); + document.getElementById("clipboard-area").classList.toggle("hidden"); } document - .getElementById("noVNC_clipboard_button") + .getElementById("clipboard-button") .addEventListener("click", toggleClipboardPanel); function clipboardReceive(e) { - document.getElementById("noVNC_clipboard_text").value = e.detail.text; + document.getElementById("clipboard-text").value = e.detail.text; } rfb.addEventListener("clipboard", clipboardReceive); function clipboardSend() { - const text = document.getElementById("noVNC_clipboard_text").value; + const text = document.getElementById("clipboard-text").value; rfb.clipboardPasteFrom(text); } document - .getElementById("noVNC_clipboard_text") + .getElementById("clipboard-text") .addEventListener("change", clipboardSend); diff --git a/jupyter_remote_desktop_proxy/static/index.css b/jupyter_remote_desktop_proxy/static/index.css deleted file mode 100644 index 226934b8..00000000 --- a/jupyter_remote_desktop_proxy/static/index.css +++ /dev/null @@ -1,79 +0,0 @@ -/** -* Derived from https://github.com/novnc/noVNC/blob/v1.4.0/vnc_lite.html, which was licensed -* under the 2-clause BSD license -*/ - -body { - margin: 0; - background-color: dimgrey; - height: 100%; - display: flex; - flex-direction: column; -} - -html { - height: 100%; -} - -#top_bar { - background-color: #6e84a3; - color: white; - font: bold 12px Helvetica; - padding: 6px 5px 4px 5px; - border-bottom: 1px outset; -} - -#status { - text-align: center; -} - -#sendCtrlAltDelButton { - position: fixed; - top: 0px; - right: 0px; - border: 1px outset; - padding: 5px 5px 4px 5px; - cursor: pointer; -} - -#screen { - flex: 1; - /* fill remaining space */ - overflow: hidden; -} - -/* Clipboard */ -#noVNC_clipboard_area { - position: fixed; - top: 0px; - left: 0px; -} - -#noVNC_clipboard_button { - border: 1px outset; - cursor: pointer; -} - -#noVNC_clipboard_button img { - height: 24px; - vertical-align: middle; -} - -#noVNC_clipboard_button .label { - padding: 5px 5px 4px 0px; -} - -#noVNC_clipboard { - /* Full screen, minus padding and left and right margins */ - max-width: calc(100vw - 2 * 15px - 75px - 25px); - background-color: #6e84a3; -} - -.noVNC_clipboard_closed #noVNC_clipboard { - display: none; -} - -#noVNC_clipboard_text { - width: 500px; - max-width: 100%; -} diff --git a/jupyter_remote_desktop_proxy/static/index.html b/jupyter_remote_desktop_proxy/static/index.html index d8d5021d..e0c49625 100644 --- a/jupyter_remote_desktop_proxy/static/index.html +++ b/jupyter_remote_desktop_proxy/static/index.html @@ -13,28 +13,30 @@ Chrome Frame. --> - + -
-
Loading
- - -
-
- - Clipboard -
-
- -
+
+
+ + Clipboard
+
Loading...
+ + + diff --git a/package.json b/package.json index 115ff4e3..4551affb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "dependencies": { - "@novnc/novnc": "^1.4.0" + "@novnc/novnc": "^1.4.0", + "reset-css": "^5.0.2" }, "scripts": { "webpack": "webpack", @@ -8,7 +9,10 @@ }, "devDependencies": { "babel-loader": "^9.1.3", + "css-loader": "^6.10.0", "eslint": "^8.56.0", + "mini-css-extract-plugin": "^2.8.0", + "style-loader": "^3.3.4", "webpack": "^5.90.1", "webpack-cli": "^5.1.4" } diff --git a/webpack.config.js b/webpack.config.js index e86067f6..6ec22bc8 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,8 +1,14 @@ const webpack = require("webpack"); +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const path = require("path"); module.exports = { entry: path.resolve(__dirname, "js/index.js"), + plugins: [ + new MiniCssExtractPlugin({ + filename: "index.css", + }), + ], devtool: "source-map", mode: "development", module: { @@ -14,7 +20,7 @@ module.exports = { }, { test: /\.(css)/, - use: ["style-loader", "css-loader"], + use: [MiniCssExtractPlugin.loader, "css-loader"], }, ], }, From 3d47e0ab54986670f2387dd02cdfc28fb2818701 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Sat, 3 Feb 2024 13:54:50 -0800 Subject: [PATCH 07/14] Add a Jupyter Logo that takes you back home --- js/index.css | 21 ++++- js/index.js | 3 + .../static/index.html | 14 +-- .../static/jupyter-logo.svg | 88 +++++++++++++++++++ 4 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 jupyter_remote_desktop_proxy/static/jupyter-logo.svg diff --git a/js/index.css b/js/index.css index a0e10f78..366eee7c 100644 --- a/js/index.css +++ b/js/index.css @@ -24,15 +24,31 @@ body { } #top-bar { - background-color: var(--jupyter-main-brand-color); + background-color: var(--jupyter-dark-grey); color: white; - font: bold 12px Helvetica; + font: 12px Helvetica; border-bottom: 1px white; display: flex; align-items: center; height: var(--topbar-height); } +#logo { + padding: 0 32px; +} + +#logo img { + height: 24px; +} + +#menu { + display: flex; + font-weight: bold; +} + +#menu li { +} + #status { text-align: center; flex-grow: 1; @@ -57,7 +73,6 @@ body { } #clipboard-button { - border: 1px outset; cursor: pointer; } diff --git a/js/index.js b/js/index.js index 185645a9..cc2e6728 100644 --- a/js/index.js +++ b/js/index.js @@ -46,6 +46,9 @@ rfb.addEventListener("disconnect", disconnectedFromServer); // Scale our viewport so the user doesn't have to scroll rfb.scaleViewport = true; +// Use a CSS variable to set background color +rfb.background = "var(--jupyter-medium-dark-grey)"; + // Clipboard function toggleClipboardPanel() { document.getElementById("clipboard-area").classList.toggle("hidden"); diff --git a/jupyter_remote_desktop_proxy/static/index.html b/jupyter_remote_desktop_proxy/static/index.html index e0c49625..aa3fe8ee 100644 --- a/jupyter_remote_desktop_proxy/static/index.html +++ b/jupyter_remote_desktop_proxy/static/index.html @@ -18,11 +18,15 @@
-
- - Clipboard -
-
Loading...
+ + Connecting... +
diff --git a/jupyter_remote_desktop_proxy/static/jupyter-logo.svg b/jupyter_remote_desktop_proxy/static/jupyter-logo.svg new file mode 100644 index 00000000..fde0d6e3 --- /dev/null +++ b/jupyter_remote_desktop_proxy/static/jupyter-logo.svg @@ -0,0 +1,88 @@ + +logo-1.svg +Created using Figma 0.90 + + + + + + + + + + + + + + + + + + From e867a7d778be759ec363fbe11b779282621f3cdf Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Sat, 3 Feb 2024 15:10:29 -0800 Subject: [PATCH 08/14] Style everything to be nicer - Uses floating-ui for a proper popover for the clipboard - Sort out the menu to look much better --- js/index.css | 75 ++++++++++++------- js/index.js | 15 ++-- js/setupTooltip.js | 63 ++++++++++++++++ .../static/index.html | 22 ++++-- package.json | 1 + 5 files changed, 133 insertions(+), 43 deletions(-) create mode 100644 js/setupTooltip.js diff --git a/js/index.css b/js/index.css index 366eee7c..cf43fbe0 100644 --- a/js/index.css +++ b/js/index.css @@ -14,6 +14,9 @@ html { --jupyter-grey: #9e9e9e; --topbar-height: 32px; + + /* Use Jupyter Brand fonts */ + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; } body { @@ -26,11 +29,10 @@ body { #top-bar { background-color: var(--jupyter-dark-grey); color: white; - font: 12px Helvetica; border-bottom: 1px white; display: flex; align-items: center; - height: var(--topbar-height); + padding: 4px 0px; } #logo { @@ -44,9 +46,26 @@ body { #menu { display: flex; font-weight: bold; + margin-left: auto; + font-size: 12px; } #menu li { + border-right: 1px white solid; + padding: 8px 8px; +} + +#menu li:last-child { + border-right: 0; +} + +#menu a { + color: white; + text-decoration: none; +} + +#status-label { + font-weight: normal; } #status { @@ -65,37 +84,37 @@ body { } /* Clipboard */ -#clipboard-area { - position: fixed; - top: var(--topbar-height); - left: 0px; - padding: 4px; -} - -#clipboard-button { - cursor: pointer; -} - -#clipboard-button img { - height: 24px; - vertical-align: middle; -} - -#clipboard-button .label { - padding: 5px 5px 4px 0px; -} - -#clipboard-area { - /* Full screen, minus padding and left and right margins */ +#clipboard-content { display: flex; flex-direction: column; - background-color: white; - border-bottom-right-radius: 4px; - border: 1px solid var(--jupyter-medium-grey); + padding: 4px; + gap: 4px; } #clipboard-text { min-width: 500px; max-width: 100%; - border: 1px solid var(--jupyter-medium-grey); +} + +.tooltip-container { + display: none; + overflow: visible; /* Needed for the arrow to show up */ + width: max-content; + position: absolute; + top: 0; + left: 0; + background: white; + color: var(--jupyter-dark-grey); + padding: 6px; + border-radius: 4px; + font-size: 90%; + box-shadow: 4px 4px 4px 0px var(--jupyter-grey); +} + +.arrow { + position: absolute; + background: white; + width: 8px; + height: 8px; + transform: rotate(45deg); } diff --git a/js/index.js b/js/index.js index cc2e6728..72aac3b5 100644 --- a/js/index.js +++ b/js/index.js @@ -9,9 +9,11 @@ import "./index.css"; // RFB holds the API to connect and communicate with a VNC server import RFB from "@novnc/novnc/core/rfb"; +import { setupTooltip } from "./setupTooltip"; + // When this function is called we have successfully connected to a server function connectedToServer() { - status("Connected!"); + status("Connected"); } // This function is called when we are disconnected @@ -50,12 +52,6 @@ rfb.scaleViewport = true; rfb.background = "var(--jupyter-medium-dark-grey)"; // Clipboard -function toggleClipboardPanel() { - document.getElementById("clipboard-area").classList.toggle("hidden"); -} -document - .getElementById("clipboard-button") - .addEventListener("click", toggleClipboardPanel); function clipboardReceive(e) { document.getElementById("clipboard-text").value = e.detail.text; @@ -69,3 +65,8 @@ function clipboardSend() { document .getElementById("clipboard-text") .addEventListener("change", clipboardSend); + +setupTooltip( + document.getElementById("clipboard-button"), + document.getElementById("clipboard-container"), +); diff --git a/js/setupTooltip.js b/js/setupTooltip.js new file mode 100644 index 00000000..677af467 --- /dev/null +++ b/js/setupTooltip.js @@ -0,0 +1,63 @@ +import { computePosition, flip, shift, offset, arrow } from "@floating-ui/dom"; + +/** + * + * @param {Element} button + * @param {Element} tooltip + */ +export function setupTooltip(button, tooltip) { + const arrowElement = document.querySelector(".arrow"); + function updatePosition() { + computePosition(button, tooltip, { + placement: "bottom", + middleware: [ + offset(6), + flip(), + shift({ padding: 5 }), + arrow({ element: arrowElement }), + ], + }).then(({ x, y, placement, middlewareData }) => { + Object.assign(tooltip.style, { + left: `${x}px`, + top: `${y}px`, + }); + + // Accessing the data + const { x: arrowX, y: arrowY } = middlewareData.arrow; + + const staticSide = { + top: "bottom", + right: "left", + bottom: "top", + left: "right", + }[placement.split("-")[0]]; + + Object.assign(arrowElement.style, { + left: arrowX != null ? `${arrowX}px` : "", + top: arrowY != null ? `${arrowY}px` : "", + right: "", + bottom: "", + [staticSide]: "-4px", + }); + }); + } + function toggleTooltip() { + if (tooltip.style.display === "block") { + tooltip.style.display = "none"; + } else { + tooltip.style.display = "block"; + } + updatePosition(); + } + + function hideTooltip() { + tooltip.style.display = ""; + } + + [ + ["click", toggleTooltip], + ["blur", hideTooltip], + ].forEach(([event, listener]) => { + button.addEventListener(event, listener); + }); +} diff --git a/jupyter_remote_desktop_proxy/static/index.html b/jupyter_remote_desktop_proxy/static/index.html index aa3fe8ee..24a0cf52 100644 --- a/jupyter_remote_desktop_proxy/static/index.html +++ b/jupyter_remote_desktop_proxy/static/index.html @@ -21,10 +21,13 @@ - Connecting...
@@ -33,12 +36,15 @@
- @@ -36,7 +36,7 @@
-
+