Add tooltip
This commit is contained in:
parent
e63695e17f
commit
6454b63f6b
|
@ -39,10 +39,34 @@
|
||||||
|
|
||||||
export default class DOMTools {
|
export default class DOMTools {
|
||||||
|
|
||||||
|
/** Document/window width */
|
||||||
|
static get screenWidth() {return Math.max(document.documentElement.clientWidth, window.innerWidth || 0);}
|
||||||
|
|
||||||
|
/** Document/window height */
|
||||||
|
static get screenHeight() {return Math.max(document.documentElement.clientHeight, window.innerHeight || 0);}
|
||||||
|
|
||||||
static escapeID(id) {
|
static escapeID(id) {
|
||||||
return id.replace(/^[^a-z]+|[^\w-]+/gi, "-");
|
return id.replace(/^[^a-z]+|[^\w-]+/gi, "-");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://javascript.info/js-animation
|
||||||
|
static animate({timing = _ => _, update, duration}) {
|
||||||
|
const start = performance.now();
|
||||||
|
|
||||||
|
requestAnimationFrame(function animate(time) {
|
||||||
|
// timeFraction goes from 0 to 1
|
||||||
|
let timeFraction = (time - start) / duration;
|
||||||
|
if (timeFraction > 1) timeFraction = 1;
|
||||||
|
|
||||||
|
// calculate the current animation state
|
||||||
|
const progress = timing(timeFraction);
|
||||||
|
|
||||||
|
update(progress); // draw it
|
||||||
|
|
||||||
|
if (timeFraction < 1) requestAnimationFrame(animate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a style to the document.
|
* Adds a style to the document.
|
||||||
* @param {string} id - identifier to use as the element id
|
* @param {string} id - identifier to use as the element id
|
||||||
|
@ -87,27 +111,6 @@ export default class DOMTools {
|
||||||
const element = document.getElementById(id);
|
const element = document.getElementById(id);
|
||||||
if (element) element.remove();
|
if (element) element.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://javascript.info/js-animation
|
|
||||||
static animate({timing = _ => _, update, duration}) {
|
|
||||||
const start = performance.now();
|
|
||||||
|
|
||||||
requestAnimationFrame(function animate(time) {
|
|
||||||
// timeFraction goes from 0 to 1
|
|
||||||
let timeFraction = (time - start) / duration;
|
|
||||||
if (timeFraction > 1) timeFraction = 1;
|
|
||||||
|
|
||||||
// calculate the current animation state
|
|
||||||
const progress = timing(timeFraction);
|
|
||||||
|
|
||||||
update(progress); // draw it
|
|
||||||
|
|
||||||
if (timeFraction < 1) {
|
|
||||||
requestAnimationFrame(animate);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is my shit version of not having to use `$` from jQuery. Meaning
|
* This is my shit version of not having to use `$` from jQuery. Meaning
|
||||||
|
|
|
@ -14,6 +14,7 @@ import Logger from "common/logger";
|
||||||
import Patcher from "./patcher";
|
import Patcher from "./patcher";
|
||||||
import Emotes from "../builtins/emotes/emotes";
|
import Emotes from "../builtins/emotes/emotes";
|
||||||
import ipc from "./ipc";
|
import ipc from "./ipc";
|
||||||
|
import Tooltip from "../ui/tooltip";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `BdApi` is a globally (`window.BdApi`) accessible object for use by plugins and developers to make their lives easier.
|
* `BdApi` is a globally (`window.BdApi`) accessible object for use by plugins and developers to make their lives easier.
|
||||||
|
@ -162,6 +163,22 @@ BdApi.showToast = function(content, options = {}) {
|
||||||
return Notices.show(content, options);
|
return Notices.show(content, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a tooltip to automatically show on hover.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} node - DOM node to monitor and show the tooltip on
|
||||||
|
* @param {string|HTMLElement} content - string to show in the tooltip
|
||||||
|
* @param {object} options - additional options for the tooltip
|
||||||
|
* @param {"primary"|"info"|"success"|"warn"|"danger"} [options.style="primary"] - correlates to the discord styling/colors
|
||||||
|
* @param {"top"|"right"|"bottom"|"left"} [options.side="top"] - can be any of top, right, bottom, left
|
||||||
|
* @param {boolean} [options.preventFlip=false] - prevents moving the tooltip to the opposite side if it is too big or goes offscreen
|
||||||
|
* @param {boolean} [options.disabled=false] - whether the tooltip should be disabled from showing on hover
|
||||||
|
* @returns new Tooltip
|
||||||
|
*/
|
||||||
|
BdApi.createTooltip = function(node, content, options = {}) {
|
||||||
|
return Tooltip.create(node, content, options);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds a webpack module using a filter.
|
* Finds a webpack module using a filter.
|
||||||
*
|
*
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
.bd-layer {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-tooltip {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 16px;
|
||||||
|
max-width: 190px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
word-wrap: break-word;
|
||||||
|
z-index: 1002;
|
||||||
|
will-change: opacity, transform;
|
||||||
|
box-shadow: var(--elevation-high);
|
||||||
|
color: var(--header-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-tooltip-content {
|
||||||
|
padding: 8px 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-tooltip-pointer {
|
||||||
|
pointer-events: none;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border: 5px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-tooltip-primary {
|
||||||
|
background-color: var(--background-floating);
|
||||||
|
color: var(--text-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-tooltip-primary .bd-tooltip-pointer {
|
||||||
|
border-top-color: var(--background-floating);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-tooltip-info {
|
||||||
|
background-color: #4A90E2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-tooltip-info .bd-tooltip-pointer {
|
||||||
|
border-top-color: #4A90E2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-tooltip-success {
|
||||||
|
background-color: #43B581;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-tooltip-success .bd-tooltip-pointer {
|
||||||
|
border-top-color: #43B581;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-tooltip-danger {
|
||||||
|
background-color: #F04747;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-tooltip-danger .bd-tooltip-pointer {
|
||||||
|
border-top-color: #F04747;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-tooltip-warn {
|
||||||
|
background-color: #FFA600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-tooltip-warn .bd-tooltip-pointer {
|
||||||
|
border-top-color: #FFA600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-tooltip-top .bd-tooltip-pointer {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-tooltip-bottom .bd-tooltip-pointer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -5px;
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-tooltip-right .bd-tooltip-pointer {
|
||||||
|
position: absolute;
|
||||||
|
right: 100%;
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -5px;
|
||||||
|
border-left-width: 5px;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-tooltip-left .bd-tooltip-pointer {
|
||||||
|
position: absolute;
|
||||||
|
left: 100%;
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -5px;
|
||||||
|
border-left-width: 5px;
|
||||||
|
transform: rotate(270deg);
|
||||||
|
}
|
|
@ -0,0 +1,164 @@
|
||||||
|
import Logger from "common/logger";
|
||||||
|
import {DOM} from "modules";
|
||||||
|
|
||||||
|
|
||||||
|
const toPx = function(value) {
|
||||||
|
return `${value}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = ["primary", "info", "success", "warn", "danger"];
|
||||||
|
const sides = ["top", "right", "bottom", "left"];
|
||||||
|
|
||||||
|
export default class Tooltip {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param {HTMLElement} node - DOM node to monitor and show the tooltip on
|
||||||
|
* @param {string|HTMLElement} tip - string to show in the tooltip
|
||||||
|
* @param {object} options - additional options for the tooltip
|
||||||
|
* @param {"primary"|"info"|"success"|"warn"|"danger"} [options.style="primary"] - correlates to the discord styling/colors
|
||||||
|
* @param {"top"|"right"|"bottom"|"left"} [options.side="top"] - can be any of top, right, bottom, left
|
||||||
|
* @param {boolean} [options.preventFlip=false] - prevents moving the tooltip to the opposite side if it is too big or goes offscreen
|
||||||
|
* @param {boolean} [options.disabled=false] - whether the tooltip should be disabled from showing on hover
|
||||||
|
*/
|
||||||
|
constructor(node, text, options = {}) {
|
||||||
|
const {style = "primary", side = "top", preventFlip = false, disabled = false} = options;
|
||||||
|
this.node = node;
|
||||||
|
this.label = text;
|
||||||
|
this.style = style.toLowerCase();
|
||||||
|
this.side = side.toLowerCase();
|
||||||
|
this.preventFlip = preventFlip;
|
||||||
|
this.disabled = disabled;
|
||||||
|
this.active = false;
|
||||||
|
|
||||||
|
if (!sides.includes(this.side)) return Logger.err("Tooltip", `Side ${this.side} does not exist.`);
|
||||||
|
if (!styles.includes(this.style)) return Logger.err("Tooltip", `Style ${this.style} does not exist.`);
|
||||||
|
|
||||||
|
this.element = DOM.createElement(`<div class="bd-layer">`);
|
||||||
|
this.tooltipElement = DOM.createElement(`<div class="bd-tooltip"><div class="bd-tooltip-pointer"></div><div class="bd-tooltip-content"></div></div>`);
|
||||||
|
this.tooltipElement.classList.add(`bd-tooltip-${this.style}`);
|
||||||
|
|
||||||
|
this.labelElement = this.tooltipElement.childNodes[1];
|
||||||
|
if (text instanceof HTMLElement) this.labelElement.append(text);
|
||||||
|
else this.labelElement.textContent = text;
|
||||||
|
|
||||||
|
this.element.append(this.tooltipElement);
|
||||||
|
|
||||||
|
this.node.addEventListener("mouseenter", () => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.node.addEventListener("mouseleave", () => {
|
||||||
|
this.hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Alias for the constructor */
|
||||||
|
static create(node, text, options = {}) {return new Tooltip(node, text, options);}
|
||||||
|
|
||||||
|
/** Container where the tooltip will be appended. */
|
||||||
|
get container() {return document.querySelector(`#app-mount`);}
|
||||||
|
/** Boolean representing if the tooltip will fit on screen above the element */
|
||||||
|
get canShowAbove() {return this.node.getBoundingClientRect().top - this.element.offsetHeight >= 0;}
|
||||||
|
/** Boolean representing if the tooltip will fit on screen below the element */
|
||||||
|
get canShowBelow() {return this.node.getBoundingClientRect().top + this.node.offsetHeight + this.element.offsetHeight <= DOM.screenHeight;}
|
||||||
|
/** Boolean representing if the tooltip will fit on screen to the left of the element */
|
||||||
|
get canShowLeft() {return this.node.getBoundingClientRect().left - this.element.offsetWidth >= 0;}
|
||||||
|
/** Boolean representing if the tooltip will fit on screen to the right of the element */
|
||||||
|
get canShowRight() {return this.node.getBoundingClientRect().left + this.node.offsetWidth + this.element.offsetWidth <= DOM.screenWidth;}
|
||||||
|
|
||||||
|
/** Hides the tooltip. Automatically called on mouseleave. */
|
||||||
|
hide() {
|
||||||
|
/** Don't rehide if already inactive */
|
||||||
|
if (!this.active) return;
|
||||||
|
this.active = false;
|
||||||
|
this.element.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shows the tooltip. Automatically called on mouseenter. Will attempt to flip if position was wrong. */
|
||||||
|
show() {
|
||||||
|
/** Don't reshow if already active */
|
||||||
|
if (this.active) return;
|
||||||
|
this.active = true;
|
||||||
|
this.labelElement.textContent = this.label;
|
||||||
|
this.container.append(this.element);
|
||||||
|
|
||||||
|
if (this.side == "top") {
|
||||||
|
if (this.canShowAbove || (!this.canShowAbove && this.preventFlip)) this.showAbove();
|
||||||
|
else this.showBelow();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.side == "bottom") {
|
||||||
|
if (this.canShowBelow || (!this.canShowBelow && this.preventFlip)) this.showBelow();
|
||||||
|
else this.showAbove();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.side == "left") {
|
||||||
|
if (this.canShowLeft || (!this.canShowLeft && this.preventFlip)) this.showLeft();
|
||||||
|
else this.showRight();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.side == "right") {
|
||||||
|
if (this.canShowRight || (!this.canShowRight && this.preventFlip)) this.showRight();
|
||||||
|
else this.showLeft();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Do not create a new observer each time if one already exists! */
|
||||||
|
if (this.observer) return;
|
||||||
|
/** Use an observer in show otherwise you'll cause unclosable tooltips */
|
||||||
|
this.observer = new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
const nodes = Array.from(mutation.removedNodes);
|
||||||
|
const directMatch = nodes.indexOf(this.node) > -1;
|
||||||
|
const parentMatch = nodes.some(parent => parent.contains(this.node));
|
||||||
|
if (directMatch || parentMatch) {
|
||||||
|
this.hide();
|
||||||
|
this.observer.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.observer.observe(document.body, {subtree: true, childList: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Force showing the tooltip above the node. */
|
||||||
|
showAbove() {
|
||||||
|
this.tooltipElement.classList.add("bd-tooltip-top");
|
||||||
|
this.element.style.setProperty("top", toPx(this.node.getBoundingClientRect().top - this.element.offsetHeight - 10));
|
||||||
|
this.centerHorizontally();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Force showing the tooltip below the node. */
|
||||||
|
showBelow() {
|
||||||
|
this.tooltipElement.classList.add("bd-tooltip-bottom");
|
||||||
|
this.element.style.setProperty("top", toPx(this.node.getBoundingClientRect().top + this.node.offsetHeight + 10));
|
||||||
|
this.centerHorizontally();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Force showing the tooltip to the left of the node. */
|
||||||
|
showLeft() {
|
||||||
|
this.tooltipElement.classList.add("bd-tooltip-left");
|
||||||
|
this.element.style.setProperty("left", toPx(this.node.getBoundingClientRect().left - this.element.offsetWidth - 10));
|
||||||
|
this.centerVertically();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Force showing the tooltip to the right of the node. */
|
||||||
|
showRight() {
|
||||||
|
this.tooltipElement.classList.add("bd-tooltip-right");
|
||||||
|
this.element.style.setProperty("left", toPx(this.node.getBoundingClientRect().left + this.node.offsetWidth + 10));
|
||||||
|
this.centerVertically();
|
||||||
|
}
|
||||||
|
|
||||||
|
centerHorizontally() {
|
||||||
|
const nodecenter = this.node.getBoundingClientRect().left + (this.node.offsetWidth / 2);
|
||||||
|
this.element.style.setProperty("left", toPx(nodecenter - (this.element.offsetWidth / 2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
centerVertically() {
|
||||||
|
const nodecenter = this.node.getBoundingClientRect().top + (this.node.offsetHeight / 2);
|
||||||
|
this.element.style.setProperty("top", toPx(nodecenter - (this.element.offsetHeight / 2)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Tooltip = Tooltip;
|
Loading…
Reference in New Issue