Improved tooltips
This commit is contained in:
parent
79d04cf55a
commit
c281f87335
@ -14,6 +14,7 @@ import ServerEvents from "./ServerEvents"
|
|||||||
import { displayAiringDate, displayDate, displayTime } from "./DateView"
|
import { displayAiringDate, displayDate, displayTime } from "./DateView"
|
||||||
import { findAll, supportsWebP, requestIdleCallback, swapElements, delay, findAllInside } from "./Utils"
|
import { findAll, supportsWebP, requestIdleCallback, swapElements, delay, findAllInside } from "./Utils"
|
||||||
import * as actions from "./Actions"
|
import * as actions from "./Actions"
|
||||||
|
import ToolTip from "./Elements/tool-tip/tool-tip"
|
||||||
|
|
||||||
export default class AnimeNotifier {
|
export default class AnimeNotifier {
|
||||||
app: Application
|
app: Application
|
||||||
@ -39,6 +40,7 @@ export default class AnimeNotifier {
|
|||||||
lastReloadContentPath: string
|
lastReloadContentPath: string
|
||||||
currentMediaId: string
|
currentMediaId: string
|
||||||
serverEvents: ServerEvents
|
serverEvents: ServerEvents
|
||||||
|
tip: ToolTip
|
||||||
|
|
||||||
constructor(app: Application) {
|
constructor(app: Application) {
|
||||||
this.app = app
|
this.app = app
|
||||||
@ -53,31 +55,6 @@ export default class AnimeNotifier {
|
|||||||
|
|
||||||
// Never remove src property on diffs
|
// Never remove src property on diffs
|
||||||
Diff.persistentAttributes.add("src")
|
Diff.persistentAttributes.add("src")
|
||||||
|
|
||||||
// Is intersection observer supported?
|
|
||||||
if("IntersectionObserver" in window) {
|
|
||||||
// Enable lazy load
|
|
||||||
this.visibilityObserver = new IntersectionObserver(
|
|
||||||
entries => {
|
|
||||||
for(let entry of entries) {
|
|
||||||
if(entry.isIntersecting) {
|
|
||||||
entry.target["became visible"]()
|
|
||||||
this.visibilityObserver.unobserve(entry.target)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Disable lazy load feature
|
|
||||||
this.visibilityObserver = {
|
|
||||||
disconnect: () => {},
|
|
||||||
observe: (elem: HTMLElement) => {
|
|
||||||
elem["became visible"]()
|
|
||||||
},
|
|
||||||
unobserve: (_: HTMLElement) => {}
|
|
||||||
} as IntersectionObserver
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@ -124,6 +101,39 @@ export default class AnimeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Web components
|
||||||
|
this.registerWebComponents()
|
||||||
|
|
||||||
|
// Tooltip
|
||||||
|
this.tip = new ToolTip()
|
||||||
|
document.body.appendChild(this.tip)
|
||||||
|
document.addEventListener("linkclicked", () => this.tip.classList.add("fade-out"))
|
||||||
|
|
||||||
|
// Intersection observer
|
||||||
|
if("IntersectionObserver" in window) {
|
||||||
|
// Enable lazy load
|
||||||
|
this.visibilityObserver = new IntersectionObserver(
|
||||||
|
entries => {
|
||||||
|
for(let entry of entries) {
|
||||||
|
if(entry.isIntersecting) {
|
||||||
|
entry.target["became visible"]()
|
||||||
|
this.visibilityObserver.unobserve(entry.target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Disable lazy load feature
|
||||||
|
this.visibilityObserver = {
|
||||||
|
disconnect: () => {},
|
||||||
|
observe: (elem: HTMLElement) => {
|
||||||
|
elem["became visible"]()
|
||||||
|
},
|
||||||
|
unobserve: (_: HTMLElement) => {}
|
||||||
|
} as IntersectionObserver
|
||||||
|
}
|
||||||
|
|
||||||
// Status message
|
// Status message
|
||||||
this.statusMessage = new StatusMessage(
|
this.statusMessage = new StatusMessage(
|
||||||
document.getElementById("status-message") as HTMLElement,
|
document.getElementById("status-message") as HTMLElement,
|
||||||
@ -183,7 +193,7 @@ export default class AnimeNotifier {
|
|||||||
Promise.resolve().then(() => this.dragAndDrop()),
|
Promise.resolve().then(() => this.dragAndDrop()),
|
||||||
Promise.resolve().then(() => this.colorBoxes()),
|
Promise.resolve().then(() => this.colorBoxes()),
|
||||||
Promise.resolve().then(() => this.loadCharacterRanking()),
|
Promise.resolve().then(() => this.loadCharacterRanking()),
|
||||||
Promise.resolve().then(() => this.assignTooltipOffsets()),
|
Promise.resolve().then(() => this.prepareTooltips()),
|
||||||
Promise.resolve().then(() => this.countUp())
|
Promise.resolve().then(() => this.countUp())
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -200,6 +210,23 @@ export default class AnimeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerWebComponents() {
|
||||||
|
if(!("customElements" in window)) {
|
||||||
|
console.warn("Web components not supported in your current browser")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom element names must have a dash in their name
|
||||||
|
const elements = new Map<string, Function>([
|
||||||
|
["tool-tip", ToolTip]
|
||||||
|
])
|
||||||
|
|
||||||
|
// Register all custom elements
|
||||||
|
for(const [tag, definition] of elements.entries()) {
|
||||||
|
window.customElements.define(tag, definition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
applyPageTitle() {
|
applyPageTitle() {
|
||||||
let headers = document.getElementsByTagName("h1")
|
let headers = document.getElementsByTagName("h1")
|
||||||
|
|
||||||
@ -313,80 +340,24 @@ export default class AnimeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assignTooltipOffsets(elements?: IterableIterator<HTMLElement>) {
|
prepareTooltips(elements?: IterableIterator<HTMLElement>) {
|
||||||
requestIdleCallback(() => {
|
if(!elements) {
|
||||||
const distanceToBorder = 5
|
elements = findAll("tip")
|
||||||
let contentRect: ClientRect
|
}
|
||||||
|
|
||||||
if(!elements) {
|
this.tip.setAttribute("active", "false")
|
||||||
elements = findAll("tip")
|
|
||||||
|
// Assign mouse enter event handler
|
||||||
|
for(let element of elements) {
|
||||||
|
element.onmouseenter = () => {
|
||||||
|
this.tip.classList.remove("fade-out")
|
||||||
|
this.tip.show(element)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign mouse enter event handler
|
element.onmouseleave = () => {
|
||||||
for(let element of elements) {
|
this.tip.hide()
|
||||||
Diff.mutations.queue(() => {
|
|
||||||
element.classList.add("tip-active")
|
|
||||||
})
|
|
||||||
|
|
||||||
element.onmouseenter = () => {
|
|
||||||
Diff.mutations.queue(() => {
|
|
||||||
if(!contentRect) {
|
|
||||||
contentRect = this.app.content.getBoundingClientRect()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamic label assignment to prevent label texts overflowing
|
|
||||||
// and taking horizontal space at page load.
|
|
||||||
let label = element.getAttribute("aria-label")
|
|
||||||
|
|
||||||
if(!label) {
|
|
||||||
console.error("Tooltip without a label:", element)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
element.dataset.label = label
|
|
||||||
|
|
||||||
// This is the most expensive call in this whole function,
|
|
||||||
// it consumes about 2-4 ms every time you call it.
|
|
||||||
let rect = element.getBoundingClientRect()
|
|
||||||
|
|
||||||
// Calculate offsets
|
|
||||||
let tipStyle = window.getComputedStyle(element, ":before")
|
|
||||||
|
|
||||||
if(!tipStyle.width || !tipStyle.paddingLeft) {
|
|
||||||
console.error("Tooltip with incorrect computed style:", element)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let tipWidth = parseInt(tipStyle.width) + parseInt(tipStyle.paddingLeft) * 2
|
|
||||||
let tipStartX = rect.left + rect.width / 2 - tipWidth / 2 - contentRect.left
|
|
||||||
let tipEndX = tipStartX + tipWidth
|
|
||||||
let leftOffset = 0
|
|
||||||
|
|
||||||
if(tipStartX < distanceToBorder) {
|
|
||||||
leftOffset = -tipStartX + distanceToBorder
|
|
||||||
} else if(tipEndX > contentRect.width - distanceToBorder) {
|
|
||||||
leftOffset = -(tipEndX - contentRect.width + distanceToBorder)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(leftOffset !== 0) {
|
|
||||||
element.classList.remove("tip-active")
|
|
||||||
element.classList.add("tip-offset-root")
|
|
||||||
|
|
||||||
let tipChild = document.createElement("div")
|
|
||||||
tipChild.classList.add("tip-offset-child")
|
|
||||||
tipChild.setAttribute("data-label", element.dataset.label)
|
|
||||||
tipChild.style.left = Math.round(leftOffset) + "px"
|
|
||||||
tipChild.style.width = rect.width + "px"
|
|
||||||
tipChild.style.height = rect.height + "px"
|
|
||||||
element.appendChild(tipChild)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unassign event listener
|
|
||||||
element.onmouseenter = null
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dragAndDrop() {
|
dragAndDrop() {
|
||||||
@ -1272,7 +1243,7 @@ export default class AnimeNotifier {
|
|||||||
this.app.ajaxify(element.getElementsByTagName("a"))
|
this.app.ajaxify(element.getElementsByTagName("a"))
|
||||||
this.lazyLoad(findAllInside("lazy", element))
|
this.lazyLoad(findAllInside("lazy", element))
|
||||||
this.mountMountables(findAllInside("mountable", element))
|
this.mountMountables(findAllInside("mountable", element))
|
||||||
this.assignTooltipOffsets(findAllInside("tip", element))
|
this.prepareTooltips(findAllInside("tip", element))
|
||||||
this.textAreaFocus()
|
this.textAreaFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,6 +237,7 @@ export default class Application {
|
|||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
|
// Prevent loading the same page
|
||||||
let url = (this as HTMLAnchorElement).getAttribute("href")
|
let url = (this as HTMLAnchorElement).getAttribute("href")
|
||||||
|
|
||||||
if(!url || url === self.currentPath) {
|
if(!url || url === self.currentPath) {
|
||||||
@ -245,6 +246,7 @@ export default class Application {
|
|||||||
|
|
||||||
// Load requested page
|
// Load requested page
|
||||||
self.load(url)
|
self.load(url)
|
||||||
|
self.emit("linkclicked")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
38
scripts/Elements/tool-tip/tool-tip.scarlet
Normal file
38
scripts/Elements/tool-tip/tool-tip.scarlet
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
const tip-opacity = 1
|
||||||
|
const tip-transform-hidden = translateY(20%)
|
||||||
|
const tip-transform-visible = translateY(0)
|
||||||
|
const tip-offset = -0.6rem
|
||||||
|
|
||||||
|
tool-tip
|
||||||
|
position absolute
|
||||||
|
z-index 100000
|
||||||
|
opacity 0
|
||||||
|
transform tip-transform-hidden
|
||||||
|
transition opacity transition-speed ease, transform transition-speed ease
|
||||||
|
pointer-events none
|
||||||
|
margin-top tip-offset
|
||||||
|
|
||||||
|
[active="true"]
|
||||||
|
opacity tip-opacity
|
||||||
|
transform tip-transform-visible
|
||||||
|
|
||||||
|
.box
|
||||||
|
color text-color
|
||||||
|
text-shadow none
|
||||||
|
padding 0.6rem 0.8rem
|
||||||
|
background tip-bg-color
|
||||||
|
font-size 0.92rem
|
||||||
|
border 1px solid ui-border-color
|
||||||
|
border-radius ui-element-border-radius
|
||||||
|
box-shadow shadow-light
|
||||||
|
white-space nowrap
|
||||||
|
|
||||||
|
.arrow
|
||||||
|
position relative
|
||||||
|
width 6px
|
||||||
|
height 6px
|
||||||
|
margin-top -1px
|
||||||
|
transform translateX(-50%)
|
||||||
|
border-style solid
|
||||||
|
border-width 6px 6px 0 6px
|
||||||
|
border-color tip-bg-color transparent transparent transparent
|
60
scripts/Elements/tool-tip/tool-tip.ts
Normal file
60
scripts/Elements/tool-tip/tool-tip.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import Diff from "scripts/Diff"
|
||||||
|
|
||||||
|
export default class ToolTip extends HTMLElement {
|
||||||
|
anchor: HTMLElement
|
||||||
|
box: HTMLElement
|
||||||
|
arrow: HTMLElement
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.box = document.createElement("div")
|
||||||
|
this.box.classList.add("box")
|
||||||
|
this.appendChild(this.box)
|
||||||
|
|
||||||
|
this.arrow = document.createElement("div")
|
||||||
|
this.arrow.classList.add("arrow")
|
||||||
|
this.appendChild(this.arrow)
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.setAttribute("active", "false")
|
||||||
|
}
|
||||||
|
|
||||||
|
show(anchor: HTMLElement) {
|
||||||
|
const distanceToBorder = 5
|
||||||
|
|
||||||
|
this.anchor = anchor
|
||||||
|
this.box.textContent = this.anchor.getAttribute("aria-label")
|
||||||
|
|
||||||
|
let anchorRect = this.anchor.getBoundingClientRect()
|
||||||
|
let boxRect = this.box.getBoundingClientRect()
|
||||||
|
|
||||||
|
let finalX = anchorRect.left + anchorRect.width / 2 - boxRect.width / 2
|
||||||
|
let finalY = anchorRect.top - boxRect.height
|
||||||
|
|
||||||
|
let contentRect = {
|
||||||
|
left: distanceToBorder,
|
||||||
|
top: distanceToBorder,
|
||||||
|
right: document.body.clientWidth - distanceToBorder,
|
||||||
|
bottom: document.body.clientHeight - distanceToBorder
|
||||||
|
}
|
||||||
|
|
||||||
|
let offsetX = 0
|
||||||
|
|
||||||
|
if(finalX < contentRect.left) {
|
||||||
|
offsetX = contentRect.left - finalX
|
||||||
|
finalX += offsetX
|
||||||
|
} else if(finalX + boxRect.width > contentRect.right) {
|
||||||
|
offsetX = contentRect.right - (finalX + boxRect.width)
|
||||||
|
finalX += offsetX
|
||||||
|
}
|
||||||
|
|
||||||
|
let arrowX = boxRect.width / 2 - offsetX
|
||||||
|
|
||||||
|
Diff.mutations.queue(() => {
|
||||||
|
this.style.left = finalX + "px"
|
||||||
|
this.style.top = finalY + "px"
|
||||||
|
this.arrow.style.left = arrowX + "px"
|
||||||
|
this.setAttribute("active", "true")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,90 +0,0 @@
|
|||||||
const tip-opacity = 0.97
|
|
||||||
const tip-transform-hidden = translateX(-50%) translateY(-80%)
|
|
||||||
const tip-transform-visible = translateX(-50%) translateY(-100%)
|
|
||||||
|
|
||||||
mixin tip-before
|
|
||||||
content attr(data-label)
|
|
||||||
position absolute
|
|
||||||
top -10px
|
|
||||||
left 50%
|
|
||||||
z-index 100000
|
|
||||||
pointer-events none
|
|
||||||
opacity 0
|
|
||||||
transform tip-transform-hidden
|
|
||||||
font-size 0.92rem
|
|
||||||
color text-color
|
|
||||||
text-shadow none
|
|
||||||
padding 0.2rem 0.7rem
|
|
||||||
background tip-bg-color
|
|
||||||
border 1px solid ui-border-color
|
|
||||||
border-radius ui-element-border-radius
|
|
||||||
box-shadow shadow-light
|
|
||||||
white-space nowrap
|
|
||||||
default-transition
|
|
||||||
|
|
||||||
mixin tip-after
|
|
||||||
content ""
|
|
||||||
position absolute
|
|
||||||
top -4px
|
|
||||||
left 50%
|
|
||||||
z-index 100001
|
|
||||||
pointer-events none
|
|
||||||
opacity 0
|
|
||||||
width 0
|
|
||||||
height 0
|
|
||||||
border-style solid
|
|
||||||
border-width 8px 8px 0 8px
|
|
||||||
border-color tip-bg-color transparent transparent transparent
|
|
||||||
transform tip-transform-hidden
|
|
||||||
default-transition
|
|
||||||
|
|
||||||
.tip-offset-child
|
|
||||||
position absolute !important
|
|
||||||
left 0
|
|
||||||
top 0
|
|
||||||
pointer-events none
|
|
||||||
|
|
||||||
// Tooltips are not activated by default.
|
|
||||||
// This helps loading times as the browser
|
|
||||||
// doesn't need to create pseudo elements on page load.
|
|
||||||
.tip
|
|
||||||
// ...
|
|
||||||
|
|
||||||
// When the browser is idle, we give all tips the "tip-active" class.
|
|
||||||
.tip-active
|
|
||||||
position relative
|
|
||||||
|
|
||||||
:before
|
|
||||||
tip-before
|
|
||||||
|
|
||||||
:after
|
|
||||||
tip-after
|
|
||||||
|
|
||||||
:hover
|
|
||||||
:before
|
|
||||||
opacity tip-opacity
|
|
||||||
transform tip-transform-visible
|
|
||||||
|
|
||||||
:after
|
|
||||||
opacity tip-opacity
|
|
||||||
transform tip-transform-visible
|
|
||||||
|
|
||||||
.tip-offset-root
|
|
||||||
position relative
|
|
||||||
|
|
||||||
:after
|
|
||||||
tip-after
|
|
||||||
|
|
||||||
.tip-offset-child
|
|
||||||
:before
|
|
||||||
tip-before
|
|
||||||
|
|
||||||
:hover
|
|
||||||
:after
|
|
||||||
opacity tip-opacity
|
|
||||||
transform tip-transform-visible
|
|
||||||
|
|
||||||
.tip-offset-child
|
|
||||||
:before
|
|
||||||
opacity tip-opacity
|
|
||||||
transform tip-transform-visible
|
|
Loading…
Reference in New Issue
Block a user