mirror of
https://github.com/ae-utbm/sith.git
synced 2025-06-08 04:05:22 +00:00
Merge pull request #1094 from ae-utbm/tooltips
Create a js tooltip library
This commit is contained in:
commit
af613c4cca
@ -319,14 +319,14 @@ export class IcsCalendar extends inheritHtmlElement("div") {
|
|||||||
click: async (event: Event) => {
|
click: async (event: Event) => {
|
||||||
const button = event.target as HTMLButtonElement;
|
const button = event.target as HTMLButtonElement;
|
||||||
button.classList.add("text-copy");
|
button.classList.add("text-copy");
|
||||||
if (!button.hasAttribute("position")) {
|
button.setAttribute("tooltip-class", "calendar-copy-tooltip");
|
||||||
button.setAttribute("tooltip", gettext("Link copied"));
|
if (!button.hasAttribute("tooltip-position")) {
|
||||||
button.setAttribute("position", "top");
|
button.setAttribute("tooltip-position", "top");
|
||||||
button.setAttribute("no-hover", "");
|
|
||||||
}
|
}
|
||||||
if (button.classList.contains("text-copied")) {
|
if (button.classList.contains("text-copied")) {
|
||||||
button.classList.remove("text-copied");
|
button.classList.remove("text-copied");
|
||||||
}
|
}
|
||||||
|
button.setAttribute("tooltip", gettext("Link copied"));
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(
|
||||||
new URL(
|
new URL(
|
||||||
await makeUrl(calendarCalendarInternal),
|
await makeUrl(calendarCalendarInternal),
|
||||||
@ -334,6 +334,7 @@ export class IcsCalendar extends inheritHtmlElement("div") {
|
|||||||
).toString(),
|
).toString(),
|
||||||
);
|
);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
button.setAttribute("tooltip-class", "calendar-copy-tooltip text-copied");
|
||||||
button.classList.remove("text-copied");
|
button.classList.remove("text-copied");
|
||||||
button.classList.add("text-copied");
|
button.classList.add("text-copied");
|
||||||
button.classList.remove("text-copy");
|
button.classList.remove("text-copy");
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
@import "core/static/core/colors";
|
@import "core/static/core/colors";
|
||||||
|
@import "core/static/core/tooltips";
|
||||||
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@ -114,12 +115,10 @@ ics-calendar {
|
|||||||
button.text-copied:focus,
|
button.text-copied:focus,
|
||||||
button.text-copied:hover {
|
button.text-copied:hover {
|
||||||
transition: 500ms ease-out;
|
transition: 500ms ease-out;
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button.text-copied[tooltip]::before {
|
.fc .fc-getCalendarLink-button {
|
||||||
opacity: 0;
|
margin-right: 0.5rem;
|
||||||
transition: opacity 500ms ease-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc .fc-helpButton-button {
|
.fc .fc-helpButton-button {
|
||||||
@ -138,3 +137,13 @@ ics-calendar {
|
|||||||
background-color: rgba(20, 20, 20, 0.6);
|
background-color: rgba(20, 20, 20, 0.6);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltip.calendar-copy-tooltip {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 500ms ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip.calendar-copy-tooltip.text-copied {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 500ms ease-out;
|
||||||
|
}
|
174
core/static/bundled/core/tooltips-index.ts
Normal file
174
core/static/bundled/core/tooltips-index.ts
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
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 `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 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
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```html
|
||||||
|
* <p tooltip="tooltip text"></p>
|
||||||
|
* <p tooltip="tooltip left" tooltip-position="left"></p>
|
||||||
|
* <div tooltip="tooltip custom class" tooltip-class="custom custom-class"></div>
|
||||||
|
* ```
|
||||||
|
**/
|
||||||
|
|
||||||
|
type Status = "open" | "close";
|
||||||
|
|
||||||
|
const tooltips: Map<HTMLElement, HTMLElement> = new Map();
|
||||||
|
|
||||||
|
function getPosition(element: HTMLElement): Placement | "auto" {
|
||||||
|
const position = element.getAttribute("tooltip-position");
|
||||||
|
if (position) {
|
||||||
|
return position as Placement | "auto";
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
const tooltip = document.createElement("div");
|
||||||
|
document.body.append(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: [] },
|
||||||
|
]) {
|
||||||
|
const populated = attributes.default;
|
||||||
|
if (element.hasAttribute(attributes.src)) {
|
||||||
|
populated.push(...element.getAttribute(attributes.src).split(" "));
|
||||||
|
}
|
||||||
|
tooltip.setAttribute(attributes.dst, populated.join(" "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTooltip(element: HTMLElement) {
|
||||||
|
const tooltip = tooltips.get(element);
|
||||||
|
if (tooltip === undefined) {
|
||||||
|
return createTooltip(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tooltip;
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
computePosition(target, tooltip, {
|
||||||
|
...getPlacement(target),
|
||||||
|
...getMiddleware(target),
|
||||||
|
}).then(({ x, y }) => {
|
||||||
|
Object.assign(tooltip.style, {
|
||||||
|
left: `${x}px`,
|
||||||
|
top: `${y}px`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
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
|
||||||
|
tooltips.get(target).remove();
|
||||||
|
tooltips.delete(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).observe(document.body, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["tooltip", "tooltip-class", "toolitp-position", "tooltip-id"],
|
||||||
|
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,
|
||||||
|
});
|
@ -45,63 +45,6 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[tooltip] {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
[tooltip]::before {
|
|
||||||
@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%);
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
position: absolute;
|
|
||||||
white-space: nowrap;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 500ms ease-out;
|
|
||||||
top: 120%; // Put the tooltip under the element
|
|
||||||
}
|
|
||||||
|
|
||||||
[tooltip]:hover::before {
|
|
||||||
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 {
|
.ib {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 1px;
|
padding: 1px;
|
||||||
|
26
core/static/core/tooltips.scss
Normal file
26
core/static/core/tooltips.scss
Normal file
@ -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;
|
||||||
|
|
||||||
|
white-space: normal;
|
||||||
|
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip[tooltip-status=open] {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 500ms ease-in;
|
||||||
|
}
|
@ -7,6 +7,7 @@
|
|||||||
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
|
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
|
||||||
<link rel="stylesheet" href="{{ static('core/base.css') }}">
|
<link rel="stylesheet" href="{{ static('core/base.css') }}">
|
||||||
<link rel="stylesheet" href="{{ static('core/style.scss') }}">
|
<link rel="stylesheet" href="{{ static('core/style.scss') }}">
|
||||||
|
<link rel="stylesheet" href="{{ static('core/tooltips.scss') }}">
|
||||||
<link rel="stylesheet" href="{{ static('core/markdown.scss') }}">
|
<link rel="stylesheet" href="{{ static('core/markdown.scss') }}">
|
||||||
<link rel="stylesheet" href="{{ static('core/header.scss') }}">
|
<link rel="stylesheet" href="{{ static('core/header.scss') }}">
|
||||||
<link rel="stylesheet" href="{{ static('core/navbar.scss') }}">
|
<link rel="stylesheet" href="{{ static('core/navbar.scss') }}">
|
||||||
@ -24,6 +25,7 @@
|
|||||||
<script type="module" src="{{ static('bundled/alpine-index.js') }}"></script>
|
<script type="module" src="{{ static('bundled/alpine-index.js') }}"></script>
|
||||||
<script type="module" src="{{ static('bundled/htmx-index.js') }}"></script>
|
<script type="module" src="{{ static('bundled/htmx-index.js') }}"></script>
|
||||||
<script type="module" src="{{ static('bundled/country-flags-index.ts') }}"></script>
|
<script type="module" src="{{ static('bundled/country-flags-index.ts') }}"></script>
|
||||||
|
<script type="module" src="{{ static('bundled/core/tooltips-index.ts') }}"></script>
|
||||||
|
|
||||||
<!-- Jquery declared here to be accessible in every django widgets -->
|
<!-- Jquery declared here to be accessible in every django widgets -->
|
||||||
<script src="{{ static('bundled/vendored/jquery.min.js') }}"></script>
|
<script src="{{ static('bundled/vendored/jquery.min.js') }}"></script>
|
||||||
|
26
package-lock.json
generated
26
package-lock.json
generated
@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alpinejs/sort": "^3.14.7",
|
"@alpinejs/sort": "^3.14.7",
|
||||||
"@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0",
|
"@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0",
|
||||||
|
"@floating-ui/dom": "^1.6.13",
|
||||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||||
"@fullcalendar/core": "^6.1.15",
|
"@fullcalendar/core": "^6.1.15",
|
||||||
"@fullcalendar/daygrid": "^6.1.15",
|
"@fullcalendar/daygrid": "^6.1.15",
|
||||||
@ -2162,6 +2163,31 @@
|
|||||||
"node": ">=18"
|
"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": {
|
"node_modules/@fortawesome/fontawesome-free": {
|
||||||
"version": "6.7.2",
|
"version": "6.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz",
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alpinejs/sort": "^3.14.7",
|
"@alpinejs/sort": "^3.14.7",
|
||||||
"@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0",
|
"@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0",
|
||||||
|
"@floating-ui/dom": "^1.6.13",
|
||||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||||
"@fullcalendar/core": "^6.1.15",
|
"@fullcalendar/core": "^6.1.15",
|
||||||
"@fullcalendar/daygrid": "^6.1.15",
|
"@fullcalendar/daygrid": "^6.1.15",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user