From 35e96fb8757ad1b9588b12cbab0665846de875a1 Mon Sep 17 00:00:00 2001 From: Sli Date: Fri, 25 Apr 2025 15:35:39 +0200 Subject: [PATCH 01/11] Create basic tooltip library --- core/static/bundled/core/tooltips-index.ts | 62 ++++++++++++++++++++++ core/static/core/style.scss | 49 ++++------------- core/templates/core/base.jinja | 1 + package-lock.json | 26 +++++++++ package.json | 1 + 5 files changed, 99 insertions(+), 40 deletions(-) create mode 100644 core/static/bundled/core/tooltips-index.ts diff --git a/core/static/bundled/core/tooltips-index.ts b/core/static/bundled/core/tooltips-index.ts new file mode 100644 index 00000000..60908431 --- /dev/null +++ b/core/static/bundled/core/tooltips-index.ts @@ -0,0 +1,62 @@ +import { type Placement, computePosition } from "@floating-ui/dom"; + +/** + * Library usage: + * Add a `tooltip` attribute to any html element with it's tooltip text + * You can control the position of the tooltp with the `position` attribute + * Allowed placements are `top`, `right`, `bottom`, `left` + * You can add `-start` and `-end` to all allowed placement values + **/ + +function getPlacement(element: HTMLElement): Placement { + const position = element.getAttribute("position"); + if (position) { + return position as Placement; + } + return "bottom"; +} + +function getTooltip(element: HTMLElement) { + for (const tooltip of document.body.getElementsByClassName("tooltip")) { + if (tooltip.textContent === element.getAttribute("tooltip")) { + return tooltip as HTMLElement; + } + } + + const tooltip = document.createElement("div"); + document.body.append(tooltip); + tooltip.classList.add("tooltip"); + tooltip.innerText = element.getAttribute("tooltip"); + + return tooltip; +} + +addEventListener("mouseover", (event: MouseEvent) => { + const target = event.target as HTMLElement; + if (!target.hasAttribute("tooltip")) { + return; + } + + const tooltip = getTooltip(target); + tooltip.setAttribute("tooltip-status", "open"); + + computePosition(target, tooltip, { + placement: getPlacement(target), + }).then(({ x, y }) => { + Object.assign(tooltip.style, { + left: `${x}px`, + top: `${y}px`, + }); + }); + + document.body.append(tooltip); +}); + +addEventListener("mouseout", (event: MouseEvent) => { + const target = event.target as HTMLElement; + if (!target.hasAttribute("tooltip")) { + return; + } + + getTooltip(target).setAttribute("tooltip-status", "close"); +}); diff --git a/core/static/core/style.scss b/core/static/core/style.scss index 58b188fc..ec962a2a 100644 --- a/core/static/core/style.scss +++ b/core/static/core/style.scss @@ -45,17 +45,10 @@ body { } } -[tooltip] { - position: relative; -} - -[tooltip]::before { +.tooltip { @include shadow; z-index: 1; pointer-events: none; - content: attr(tooltip); - left: 50%; - transform: translateX(-50%); background-color: #333; color: #fff; border: 0.5px solid hsl(0, 0%, 50%); @@ -64,44 +57,20 @@ body { position: absolute; white-space: nowrap; opacity: 0; - transition: opacity 500ms ease-out; - top: 120%; // Put the tooltip under the element + transition: opacity 500ms ease-in; + + position: absolute; + width: max-content; + + left: 0; + top: 0; } -[tooltip]:hover::before { +.tooltip[tooltip-status=open] { opacity: 1; transition: opacity 500ms ease-in; } -[no-hover][tooltip]::before { - opacity: 1; - transition: opacity 500ms ease-in; -} - -[position="top"][tooltip]::before { - top: initial; - bottom: 120%; -} - -[position="bottom"][tooltip]::before { - top: 120%; - bottom: initial; -} - -[position="left"][tooltip]::before { - top: initial; - bottom: 0%; - left: initial; - right: 65%; -} - -[position="right"][tooltip]::before { - top: initial; - bottom: 0%; - left: 150%; - right: initial; -} - .ib { display: inline-block; padding: 1px; diff --git a/core/templates/core/base.jinja b/core/templates/core/base.jinja index 41b13398..832bc1b8 100644 --- a/core/templates/core/base.jinja +++ b/core/templates/core/base.jinja @@ -24,6 +24,7 @@ + diff --git a/package-lock.json b/package-lock.json index 541a492d..87d4bc5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@alpinejs/sort": "^3.14.7", "@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0", + "@floating-ui/dom": "^1.6.13", "@fortawesome/fontawesome-free": "^6.6.0", "@fullcalendar/core": "^6.1.15", "@fullcalendar/daygrid": "^6.1.15", @@ -2162,6 +2163,31 @@ "node": ">=18" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "license": "MIT" + }, "node_modules/@fortawesome/fontawesome-free": { "version": "6.7.2", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz", diff --git a/package.json b/package.json index 94fa21fc..22b1d2c1 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "dependencies": { "@alpinejs/sort": "^3.14.7", "@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0", + "@floating-ui/dom": "^1.6.13", "@fortawesome/fontawesome-free": "^6.6.0", "@fullcalendar/core": "^6.1.15", "@fullcalendar/daygrid": "^6.1.15", From 1872e4abe5a187208b41af7921d9c46132bbeaeb Mon Sep 17 00:00:00 2001 From: Sli Date: Fri, 25 Apr 2025 16:08:15 +0200 Subject: [PATCH 02/11] Adapt calendar to new tooltip library --- .../com/components/ics-calendar-index.ts | 6 +++-- com/static/com/components/ics-calendar.scss | 8 ++---- core/static/core/style.scss | 5 +++- core/static/core/tooltips.scss | 26 +++++++++++++++++++ core/templates/core/base.jinja | 1 + 5 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 core/static/core/tooltips.scss diff --git a/com/static/bundled/com/components/ics-calendar-index.ts b/com/static/bundled/com/components/ics-calendar-index.ts index 33044996..c339c2d5 100644 --- a/com/static/bundled/com/components/ics-calendar-index.ts +++ b/com/static/bundled/com/components/ics-calendar-index.ts @@ -320,13 +320,13 @@ export class IcsCalendar extends inheritHtmlElement("div") { const button = event.target as HTMLButtonElement; button.classList.add("text-copy"); if (!button.hasAttribute("position")) { - button.setAttribute("tooltip", gettext("Link copied")); button.setAttribute("position", "top"); - button.setAttribute("no-hover", ""); } if (button.classList.contains("text-copied")) { button.classList.remove("text-copied"); } + button.setAttribute("tooltip", gettext("Link copied")); + button.dispatchEvent(new Event("mouseover", { bubbles: true })); navigator.clipboard.writeText( new URL( await makeUrl(calendarCalendarInternal), @@ -337,6 +337,8 @@ export class IcsCalendar extends inheritHtmlElement("div") { button.classList.remove("text-copied"); button.classList.add("text-copied"); button.classList.remove("text-copy"); + button.dispatchEvent(new Event("mouseout", { bubbles: true })); + button.removeAttribute("tooltip"); }, 1500); }, }, diff --git a/com/static/com/components/ics-calendar.scss b/com/static/com/components/ics-calendar.scss index d0c0f92c..7eb75c5c 100644 --- a/com/static/com/components/ics-calendar.scss +++ b/com/static/com/components/ics-calendar.scss @@ -1,4 +1,5 @@ @import "core/static/core/colors"; +@import "core/static/core/tooltips"; :root { @@ -117,11 +118,6 @@ ics-calendar { margin-right: 0.5rem; } - button.text-copied[tooltip]::before { - opacity: 0; - transition: opacity 500ms ease-out; - } - .fc .fc-helpButton-button { border-radius: 70%; padding-left: 0.5rem; @@ -137,4 +133,4 @@ ics-calendar { .fc .fc-helpButton-button:hover { background-color: rgba(20, 20, 20, 0.6); } -} \ No newline at end of file +} diff --git a/core/static/core/style.scss b/core/static/core/style.scss index ec962a2a..0481c280 100644 --- a/core/static/core/style.scss +++ b/core/static/core/style.scss @@ -45,7 +45,7 @@ body { } } -.tooltip { +@mixin tooltip { @include shadow; z-index: 1; pointer-events: none; @@ -58,7 +58,10 @@ body { white-space: nowrap; opacity: 0; transition: opacity 500ms ease-in; +} +.tooltip { + @include tooltip; position: absolute; width: max-content; diff --git a/core/static/core/tooltips.scss b/core/static/core/tooltips.scss new file mode 100644 index 00000000..57bcb7a3 --- /dev/null +++ b/core/static/core/tooltips.scss @@ -0,0 +1,26 @@ +@import "colors"; + +.tooltip { + @include shadow; + z-index: 1; + pointer-events: none; + background-color: #333; + color: #fff; + border: 0.5px solid hsl(0, 0%, 50%); + border-radius: 5px; + padding: 5px 10px; + position: absolute; + white-space: nowrap; + opacity: 0; + transition: opacity 500ms ease-out; + position: absolute; + width: max-content; + + left: 0; + top: 0; +} + +.tooltip[tooltip-status=open] { + opacity: 1; + transition: opacity 500ms ease-in; +} \ No newline at end of file diff --git a/core/templates/core/base.jinja b/core/templates/core/base.jinja index 832bc1b8..6479e833 100644 --- a/core/templates/core/base.jinja +++ b/core/templates/core/base.jinja @@ -7,6 +7,7 @@ + From 48bf72f62350f1ba1e54b406ac2c8765d8f944ae Mon Sep 17 00:00:00 2001 From: Sli Date: Mon, 28 Apr 2025 09:53:20 +0200 Subject: [PATCH 03/11] Improve tooltips performance --- core/static/bundled/core/tooltips-index.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/core/static/bundled/core/tooltips-index.ts b/core/static/bundled/core/tooltips-index.ts index 60908431..7581a929 100644 --- a/core/static/bundled/core/tooltips-index.ts +++ b/core/static/bundled/core/tooltips-index.ts @@ -8,6 +8,8 @@ import { type Placement, computePosition } from "@floating-ui/dom"; * You can add `-start` and `-end` to all allowed placement values **/ +const tooltips = new Map(); + function getPlacement(element: HTMLElement): Placement { const position = element.getAttribute("position"); if (position) { @@ -16,17 +18,20 @@ function getPlacement(element: HTMLElement): Placement { return "bottom"; } -function getTooltip(element: HTMLElement) { - for (const tooltip of document.body.getElementsByClassName("tooltip")) { - if (tooltip.textContent === element.getAttribute("tooltip")) { - return tooltip as HTMLElement; - } - } - +function createTooltip(element: HTMLElement) { const tooltip = document.createElement("div"); document.body.append(tooltip); tooltip.classList.add("tooltip"); tooltip.innerText = element.getAttribute("tooltip"); + tooltips.set(element, tooltip); + return tooltip; +} + +function getTooltip(element: HTMLElement) { + const tooltip = tooltips.get(element); + if (tooltip === undefined) { + return createTooltip(element); + } return tooltip; } From 68b1a962706b2a8313abb6351e766918ad6e2838 Mon Sep 17 00:00:00 2001 From: Sli Date: Mon, 28 Apr 2025 14:19:04 +0200 Subject: [PATCH 04/11] Allow popup customization --- .../com/components/ics-calendar-index.ts | 6 ++-- com/static/com/components/ics-calendar.scss | 5 +++ core/static/bundled/core/tooltips-index.ts | 33 +++++++++++++++---- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/com/static/bundled/com/components/ics-calendar-index.ts b/com/static/bundled/com/components/ics-calendar-index.ts index c339c2d5..6816bace 100644 --- a/com/static/bundled/com/components/ics-calendar-index.ts +++ b/com/static/bundled/com/components/ics-calendar-index.ts @@ -319,8 +319,9 @@ export class IcsCalendar extends inheritHtmlElement("div") { click: async (event: Event) => { const button = event.target as HTMLButtonElement; button.classList.add("text-copy"); - if (!button.hasAttribute("position")) { - button.setAttribute("position", "top"); + button.setAttribute("tooltip-class", "text-copy"); + if (!button.hasAttribute("tooltip-position")) { + button.setAttribute("tooltip-position", "top"); } if (button.classList.contains("text-copied")) { button.classList.remove("text-copied"); @@ -334,6 +335,7 @@ export class IcsCalendar extends inheritHtmlElement("div") { ).toString(), ); setTimeout(() => { + button.setAttribute("tooltip-class", "text-copied"); button.classList.remove("text-copied"); button.classList.add("text-copied"); button.classList.remove("text-copy"); diff --git a/com/static/com/components/ics-calendar.scss b/com/static/com/components/ics-calendar.scss index 7eb75c5c..4aa002b4 100644 --- a/com/static/com/components/ics-calendar.scss +++ b/com/static/com/components/ics-calendar.scss @@ -117,6 +117,7 @@ ics-calendar { transition: 500ms ease-out; margin-right: 0.5rem; } +} .fc .fc-helpButton-button { border-radius: 70%; @@ -134,3 +135,7 @@ ics-calendar { background-color: rgba(20, 20, 20, 0.6); } } + +.tooltip.text-copy { + opacity: 1; +} diff --git a/core/static/bundled/core/tooltips-index.ts b/core/static/bundled/core/tooltips-index.ts index 7581a929..42a1cc82 100644 --- a/core/static/bundled/core/tooltips-index.ts +++ b/core/static/bundled/core/tooltips-index.ts @@ -3,15 +3,21 @@ import { type Placement, computePosition } from "@floating-ui/dom"; /** * Library usage: * Add a `tooltip` attribute to any html element with it's tooltip text - * You can control the position of the tooltp with the `position` attribute + * You can control the position of the tooltp with the `tooltip-position` attribute * Allowed placements are `top`, `right`, `bottom`, `left` * You can add `-start` and `-end` to all allowed placement values + * + * You can customize your tooltip by passing additionnal classes or ids to it + * You can use `tooltip-class` and `tooltip-id` to add additional elements to the + * `class` and `id` attribute of the generated tooltip **/ +type Status = "open" | "close"; + const tooltips = new Map(); function getPlacement(element: HTMLElement): Placement { - const position = element.getAttribute("position"); + const position = element.getAttribute("tooltip-position"); if (position) { return position as Placement; } @@ -21,12 +27,27 @@ function getPlacement(element: HTMLElement): Placement { function createTooltip(element: HTMLElement) { const tooltip = document.createElement("div"); document.body.append(tooltip); - tooltip.classList.add("tooltip"); - tooltip.innerText = element.getAttribute("tooltip"); tooltips.set(element, tooltip); return tooltip; } +function updateTooltip(element: HTMLElement, tooltip: HTMLElement, status: Status) { + // Update tooltip status and set it's attributes and content + tooltip.setAttribute("tooltip-status", status); + tooltip.innerText = element.getAttribute("tooltip"); + + for (const attributes of [ + { src: "tooltip-class", dst: "class", default: ["tooltip"] }, + { src: "tooltip-id", dst: "id", default: [] }, + ]) { + let populated = attributes.default; + if (element.hasAttribute(attributes.src)) { + populated = populated.concat(element.getAttribute(attributes.src).split(" ")); + } + tooltip.setAttribute(attributes.dst, populated.join(" ")); + } +} + function getTooltip(element: HTMLElement) { const tooltip = tooltips.get(element); if (tooltip === undefined) { @@ -43,7 +64,7 @@ addEventListener("mouseover", (event: MouseEvent) => { } const tooltip = getTooltip(target); - tooltip.setAttribute("tooltip-status", "open"); + updateTooltip(target, tooltip, "open"); computePosition(target, tooltip, { placement: getPlacement(target), @@ -63,5 +84,5 @@ addEventListener("mouseout", (event: MouseEvent) => { return; } - getTooltip(target).setAttribute("tooltip-status", "close"); + updateTooltip(target, getTooltip(target), "close"); }); From 9bd3c618a40df4973e41f1a059c3c422fb589d6c Mon Sep 17 00:00:00 2001 From: Sli Date: Mon, 28 Apr 2025 14:31:56 +0200 Subject: [PATCH 05/11] Add doc, tooltip offset and css cleanup --- core/static/bundled/core/tooltips-index.ts | 11 +++++++- core/static/core/style.scss | 29 ---------------------- 2 files changed, 10 insertions(+), 30 deletions(-) diff --git a/core/static/bundled/core/tooltips-index.ts b/core/static/bundled/core/tooltips-index.ts index 42a1cc82..47df492d 100644 --- a/core/static/bundled/core/tooltips-index.ts +++ b/core/static/bundled/core/tooltips-index.ts @@ -1,7 +1,8 @@ -import { type Placement, computePosition } from "@floating-ui/dom"; +import { type Placement, computePosition, offset } from "@floating-ui/dom"; /** * Library usage: + * * Add a `tooltip` attribute to any html element with it's tooltip text * You can control the position of the tooltp with the `tooltip-position` attribute * Allowed placements are `top`, `right`, `bottom`, `left` @@ -10,6 +11,13 @@ import { type Placement, computePosition } from "@floating-ui/dom"; * You can customize your tooltip by passing additionnal classes or ids to it * You can use `tooltip-class` and `tooltip-id` to add additional elements to the * `class` and `id` attribute of the generated tooltip + * + * @example + * ```html + *

+ *

+ *
+ * ``` **/ type Status = "open" | "close"; @@ -68,6 +76,7 @@ addEventListener("mouseover", (event: MouseEvent) => { computePosition(target, tooltip, { placement: getPlacement(target), + middleware: [offset(6)], }).then(({ x, y }) => { Object.assign(tooltip.style, { left: `${x}px`, diff --git a/core/static/core/style.scss b/core/static/core/style.scss index 0481c280..544903a2 100644 --- a/core/static/core/style.scss +++ b/core/static/core/style.scss @@ -45,35 +45,6 @@ body { } } -@mixin tooltip { - @include shadow; - z-index: 1; - pointer-events: none; - background-color: #333; - color: #fff; - border: 0.5px solid hsl(0, 0%, 50%); - border-radius: 5px; - padding: 5px 10px; - position: absolute; - white-space: nowrap; - opacity: 0; - transition: opacity 500ms ease-in; -} - -.tooltip { - @include tooltip; - position: absolute; - width: max-content; - - left: 0; - top: 0; -} - -.tooltip[tooltip-status=open] { - opacity: 1; - transition: opacity 500ms ease-in; -} - .ib { display: inline-block; padding: 1px; From 19aac8f30207f6ddb1349359d8a9c715095ad8c0 Mon Sep 17 00:00:00 2001 From: Sli Date: Mon, 28 Apr 2025 15:25:57 +0200 Subject: [PATCH 06/11] Fix tooltip size --- core/static/bundled/core/tooltips-index.ts | 48 +++++++++++++++++----- core/static/core/tooltips.scss | 4 +- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/core/static/bundled/core/tooltips-index.ts b/core/static/bundled/core/tooltips-index.ts index 47df492d..f9721ca4 100644 --- a/core/static/bundled/core/tooltips-index.ts +++ b/core/static/bundled/core/tooltips-index.ts @@ -1,12 +1,22 @@ -import { type Placement, computePosition, offset } from "@floating-ui/dom"; +import { + type Placement, + autoPlacement, + computePosition, + flip, + offset, + size, +} from "@floating-ui/dom"; /** * Library usage: * * Add a `tooltip` attribute to any html element with it's tooltip text * You can control the position of the tooltp with the `tooltip-position` attribute - * Allowed placements are `top`, `right`, `bottom`, `left` - * You can add `-start` and `-end` to all allowed placement values + * Allowed placements are `auto`, `top`, `right`, `bottom`, `left` + * You can add `-start` and `-end` to all allowed placement values except for `auto` + * Default placement is `auto` + * Note: placement are suggestions and the position could change if the popup gets + * outside of the screen. * * You can customize your tooltip by passing additionnal classes or ids to it * You can use `tooltip-class` and `tooltip-id` to add additional elements to the @@ -24,12 +34,30 @@ type Status = "open" | "close"; const tooltips = new Map(); -function getPlacement(element: HTMLElement): Placement { +function getPosition(element: HTMLElement): Placement | "auto" { const position = element.getAttribute("tooltip-position"); if (position) { - return position as Placement; + return position as Placement | "auto"; } - return "bottom"; + return "auto"; +} + +function getMiddleware(element: HTMLElement) { + const middleware = [offset(6), size()]; + if (getPosition(element) === "auto") { + middleware.push(autoPlacement()); + } else { + middleware.push(flip()); + } + return { middleware: middleware }; +} + +function getPlacement(element: HTMLElement) { + const position = getPosition(element); + if (position !== "auto") { + return { placement: position }; + } + return {}; } function createTooltip(element: HTMLElement) { @@ -48,9 +76,9 @@ function updateTooltip(element: HTMLElement, tooltip: HTMLElement, status: Statu { src: "tooltip-class", dst: "class", default: ["tooltip"] }, { src: "tooltip-id", dst: "id", default: [] }, ]) { - let populated = attributes.default; + const populated = attributes.default; if (element.hasAttribute(attributes.src)) { - populated = populated.concat(element.getAttribute(attributes.src).split(" ")); + populated.push(...element.getAttribute(attributes.src).split(" ")); } tooltip.setAttribute(attributes.dst, populated.join(" ")); } @@ -75,8 +103,8 @@ addEventListener("mouseover", (event: MouseEvent) => { updateTooltip(target, tooltip, "open"); computePosition(target, tooltip, { - placement: getPlacement(target), - middleware: [offset(6)], + ...getPlacement(target), + ...getMiddleware(target), }).then(({ x, y }) => { Object.assign(tooltip.style, { left: `${x}px`, diff --git a/core/static/core/tooltips.scss b/core/static/core/tooltips.scss index 57bcb7a3..3efce0ce 100644 --- a/core/static/core/tooltips.scss +++ b/core/static/core/tooltips.scss @@ -13,8 +13,8 @@ white-space: nowrap; opacity: 0; transition: opacity 500ms ease-out; - position: absolute; - width: max-content; + + white-space: normal; left: 0; top: 0; From d1e5c93a08e956b26281518a6630f45c7fb0e567 Mon Sep 17 00:00:00 2001 From: Sli Date: Wed, 14 May 2025 12:38:13 +0200 Subject: [PATCH 07/11] Improve tooltips by using mutation observers --- core/static/bundled/core/tooltips-index.ts | 72 +++++++++++++++++----- 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/core/static/bundled/core/tooltips-index.ts b/core/static/bundled/core/tooltips-index.ts index f9721ca4..eb0a201a 100644 --- a/core/static/bundled/core/tooltips-index.ts +++ b/core/static/bundled/core/tooltips-index.ts @@ -18,7 +18,7 @@ import { * Note: placement are suggestions and the position could change if the popup gets * outside of the screen. * - * You can customize your tooltip by passing additionnal classes or ids to it + * You can customize your tooltip by passing additional classes or ids to it * You can use `tooltip-class` and `tooltip-id` to add additional elements to the * `class` and `id` attribute of the generated tooltip * @@ -32,7 +32,7 @@ import { type Status = "open" | "close"; -const tooltips = new Map(); +const tooltips: Map = new Map(); function getPosition(element: HTMLElement): Placement | "auto" { const position = element.getAttribute("tooltip-position"); @@ -93,12 +93,10 @@ function getTooltip(element: HTMLElement) { return tooltip; } -addEventListener("mouseover", (event: MouseEvent) => { - const target = event.target as HTMLElement; - if (!target.hasAttribute("tooltip")) { - return; - } - +function tooltipMouseover(event: MouseEvent) { + // We get the closest tooltip to have a consistent behavior + // when hovering over a child element of a tooltip marked element + const target = (event.target as HTMLElement).closest("[tooltip]") as HTMLElement; const tooltip = getTooltip(target); updateTooltip(target, tooltip, "open"); @@ -113,13 +111,57 @@ addEventListener("mouseover", (event: MouseEvent) => { }); document.body.append(tooltip); -}); - -addEventListener("mouseout", (event: MouseEvent) => { - const target = event.target as HTMLElement; - if (!target.hasAttribute("tooltip")) { - return; - } +} +function tooltipMouseout(event: MouseEvent) { + // We get the closest tooltip to have a consistent behavior + // when hovering over a child element of a tooltip marked element + const target = (event.target as HTMLElement).closest("[tooltip]") as HTMLElement; updateTooltip(target, getTooltip(target), "close"); +} + +window.addEventListener("DOMContentLoaded", () => { + for (const el of document.querySelectorAll("[tooltip]")) { + el.addEventListener("mouseover", tooltipMouseover); + el.addEventListener("mouseout", tooltipMouseout); + } +}); + +// Add / remove callback when tooltip attribute is added / removed +new MutationObserver((mutations: MutationRecord[]) => { + for (const mutation of mutations) { + const target = mutation.target as HTMLElement; + target.removeEventListener("mouseover", tooltipMouseover); + target.removeEventListener("mouseout", tooltipMouseout); + if (target.hasAttribute("tooltip")) { + target.addEventListener("mouseover", tooltipMouseover); + target.addEventListener("mouseout", tooltipMouseout); + } + } +}).observe(document.body, { + attributes: true, + attributeFilter: ["tooltip"], + subtree: true, +}); + +// Remove orphan tooltips +new MutationObserver((mutations: MutationRecord[]) => { + for (const mutation of mutations) { + for (const node of mutation.removedNodes) { + if (node.nodeType !== node.ELEMENT_NODE) { + continue; + } + const target = node as HTMLElement; + if (!target.hasAttribute("tooltip")) { + continue; + } + if (tooltips.has(target)) { + tooltips.get(target).remove(); + tooltips.delete(target); + } + } + } +}).observe(document.body, { + subtree: true, + childList: true, }); From 3a5bff8810e8aa723b4a9b1de3cfb8393a453022 Mon Sep 17 00:00:00 2001 From: Sli Date: Wed, 14 May 2025 13:37:36 +0200 Subject: [PATCH 08/11] Fix tooltip not appearing/disapearing when attribute is removed --- com/static/bundled/com/components/ics-calendar-index.ts | 2 -- core/static/bundled/core/tooltips-index.ts | 7 +++++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/com/static/bundled/com/components/ics-calendar-index.ts b/com/static/bundled/com/components/ics-calendar-index.ts index 6816bace..6347cc22 100644 --- a/com/static/bundled/com/components/ics-calendar-index.ts +++ b/com/static/bundled/com/components/ics-calendar-index.ts @@ -327,7 +327,6 @@ export class IcsCalendar extends inheritHtmlElement("div") { button.classList.remove("text-copied"); } button.setAttribute("tooltip", gettext("Link copied")); - button.dispatchEvent(new Event("mouseover", { bubbles: true })); navigator.clipboard.writeText( new URL( await makeUrl(calendarCalendarInternal), @@ -339,7 +338,6 @@ export class IcsCalendar extends inheritHtmlElement("div") { button.classList.remove("text-copied"); button.classList.add("text-copied"); button.classList.remove("text-copy"); - button.dispatchEvent(new Event("mouseout", { bubbles: true })); button.removeAttribute("tooltip"); }, 1500); }, diff --git a/core/static/bundled/core/tooltips-index.ts b/core/static/bundled/core/tooltips-index.ts index eb0a201a..0b68a385 100644 --- a/core/static/bundled/core/tooltips-index.ts +++ b/core/static/bundled/core/tooltips-index.ts @@ -136,6 +136,13 @@ new MutationObserver((mutations: MutationRecord[]) => { if (target.hasAttribute("tooltip")) { target.addEventListener("mouseover", tooltipMouseover); target.addEventListener("mouseout", tooltipMouseout); + if (target.matches(":hover")) { + target.dispatchEvent(new Event("mouseover", { bubbles: true })); + } + } else if (tooltips.has(target)) { + // Remove corresponding tooltip + tooltips.get(target).remove(); + tooltips.delete(target); } } }).observe(document.body, { From 6bb6be011c95f1830865550763d3e40486e1a6f6 Mon Sep 17 00:00:00 2001 From: Sli Date: Wed, 14 May 2025 15:02:46 +0200 Subject: [PATCH 09/11] Fix tooltip fading transitions and synchronize additional attributes --- com/static/bundled/com/components/ics-calendar-index.ts | 5 ++--- com/static/com/components/ics-calendar.scss | 9 +++++++-- core/static/bundled/core/tooltips-index.ts | 8 ++++++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/com/static/bundled/com/components/ics-calendar-index.ts b/com/static/bundled/com/components/ics-calendar-index.ts index 6347cc22..ad85280a 100644 --- a/com/static/bundled/com/components/ics-calendar-index.ts +++ b/com/static/bundled/com/components/ics-calendar-index.ts @@ -319,7 +319,7 @@ export class IcsCalendar extends inheritHtmlElement("div") { click: async (event: Event) => { const button = event.target as HTMLButtonElement; button.classList.add("text-copy"); - button.setAttribute("tooltip-class", "text-copy"); + button.setAttribute("tooltip-class", "calendar-copy-tooltip"); if (!button.hasAttribute("tooltip-position")) { button.setAttribute("tooltip-position", "top"); } @@ -334,11 +334,10 @@ export class IcsCalendar extends inheritHtmlElement("div") { ).toString(), ); setTimeout(() => { - button.setAttribute("tooltip-class", "text-copied"); + button.setAttribute("tooltip-class", "calendar-copy-tooltip text-copied"); button.classList.remove("text-copied"); button.classList.add("text-copied"); button.classList.remove("text-copy"); - button.removeAttribute("tooltip"); }, 1500); }, }, diff --git a/com/static/com/components/ics-calendar.scss b/com/static/com/components/ics-calendar.scss index 4aa002b4..74d04053 100644 --- a/com/static/com/components/ics-calendar.scss +++ b/com/static/com/components/ics-calendar.scss @@ -117,7 +117,6 @@ ics-calendar { transition: 500ms ease-out; margin-right: 0.5rem; } -} .fc .fc-helpButton-button { border-radius: 70%; @@ -136,6 +135,12 @@ ics-calendar { } } -.tooltip.text-copy { +.tooltip.calendar-copy-tooltip { opacity: 1; + transition: opacity 500ms ease-in; } + +.tooltip.calendar-copy-tooltip.text-copied { + opacity: 0; + transition: opacity 500ms ease-out; +} \ No newline at end of file diff --git a/core/static/bundled/core/tooltips-index.ts b/core/static/bundled/core/tooltips-index.ts index 0b68a385..e315a7b2 100644 --- a/core/static/bundled/core/tooltips-index.ts +++ b/core/static/bundled/core/tooltips-index.ts @@ -110,7 +110,9 @@ function tooltipMouseover(event: MouseEvent) { }); }); - document.body.append(tooltip); + if (!tooltip.isConnected) { + document.body.append(tooltip); + } } function tooltipMouseout(event: MouseEvent) { @@ -138,6 +140,8 @@ new MutationObserver((mutations: MutationRecord[]) => { target.addEventListener("mouseout", tooltipMouseout); if (target.matches(":hover")) { target.dispatchEvent(new Event("mouseover", { bubbles: true })); + } else { + target.dispatchEvent(new Event("mouseout", { bubbles: true })); } } else if (tooltips.has(target)) { // Remove corresponding tooltip @@ -147,7 +151,7 @@ new MutationObserver((mutations: MutationRecord[]) => { } }).observe(document.body, { attributes: true, - attributeFilter: ["tooltip"], + attributeFilter: ["tooltip", "tooltip-class", "toolitp-position"], subtree: true, }); From e63a09ee7e5e67f6d9e57dde28ad9694a239bfce Mon Sep 17 00:00:00 2001 From: Sli Date: Wed, 14 May 2025 14:20:54 +0200 Subject: [PATCH 10/11] Synchronize tooltip-id and don't append tooltip to body twice --- core/static/bundled/core/tooltips-index.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/core/static/bundled/core/tooltips-index.ts b/core/static/bundled/core/tooltips-index.ts index e315a7b2..629c9767 100644 --- a/core/static/bundled/core/tooltips-index.ts +++ b/core/static/bundled/core/tooltips-index.ts @@ -109,10 +109,6 @@ function tooltipMouseover(event: MouseEvent) { top: `${y}px`, }); }); - - if (!tooltip.isConnected) { - document.body.append(tooltip); - } } function tooltipMouseout(event: MouseEvent) { @@ -151,7 +147,7 @@ new MutationObserver((mutations: MutationRecord[]) => { } }).observe(document.body, { attributes: true, - attributeFilter: ["tooltip", "tooltip-class", "toolitp-position"], + attributeFilter: ["tooltip", "tooltip-class", "toolitp-position", "tooltip-id"], subtree: true, }); From 13f8b5db614f4e29b0c522fd93bb7215fa1bb160 Mon Sep 17 00:00:00 2001 From: Sli Date: Wed, 14 May 2025 15:04:20 +0200 Subject: [PATCH 11/11] Fix bug where help button moves as if possessed --- com/static/com/components/ics-calendar.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/com/static/com/components/ics-calendar.scss b/com/static/com/components/ics-calendar.scss index 74d04053..1c0a15bd 100644 --- a/com/static/com/components/ics-calendar.scss +++ b/com/static/com/components/ics-calendar.scss @@ -115,6 +115,9 @@ ics-calendar { button.text-copied:focus, button.text-copied:hover { transition: 500ms ease-out; + } + + .fc .fc-getCalendarLink-button { margin-right: 0.5rem; }