From 4e11adf60b74db6a69fcebbf07dcd7c8e8b00a20 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 29 Mar 2024 14:24:45 -0300 Subject: content, client: linkContribution: platform info in tooltips --- src/static/client3.js | 207 +++++++++++++++++++++++++++++++++++++++++++++++++- src/static/site6.css | 42 ++++++++++ 2 files changed, 248 insertions(+), 1 deletion(-) (limited to 'src/static') diff --git a/src/static/client3.js b/src/static/client3.js index 5738b46..f86fd84 100644 --- a/src/static/client3.js +++ b/src/static/client3.js @@ -5,7 +5,8 @@ // that cannot 8e done at static-site compile time, 8y its fundamentally // ephemeral nature. -import {empty, filterMultipleArrays, stitchArrays} from '../util/sugar.js'; +import {accumulateSum, empty, filterMultipleArrays, stitchArrays} + from '../util/sugar.js'; const clientInfo = window.hsmusicClientInfo = Object.create(null); @@ -3036,6 +3037,210 @@ function addDatestampTooltipPageListeners() { clientSteps.getPageReferences.push(getDatestampTooltipReferences); clientSteps.addPageListeners.push(addDatestampTooltipPageListeners); +// Artist external link tooltips -------------------------- + +// These don't need to have tooltip events specially added as +// they're implemented with "text with tooltip" components. + +const artistExternalLinkTooltipInfo = initInfo('artistExternalLinkTooltipInfo', { + tooltips: null, + tooltipRows: null, + + settings: { + // This is the maximum distance, in CSS pixels, that the mouse + // can appear to be moving per second while still considered + // "idle". A greater value means higher tolerance for small + // movements. + maximumIdleSpeed: 40, + + // Leaving the mouse idle for this amount of time, over a single + // row of the tooltip, will cause a column of supplemental info + // to display. + mouseIdleShowInfoDelay: 1000, + + // If none of these tooltips are visible for this amount of time, + // the supplemental info column is hidden. It'll never disappear + // while a tooltip is actually visible. + hideInfoAfterTooltipHiddenDelay: 2250, + }, + + state: { + // This is shared by all tooltips. + showingTooltipInfo: false, + + mouseIdleTimeout: null, + hideInfoTimeout: null, + + mouseMovementPositions: [], + mouseMovementTimestamps: [], + }, +}); + +function getArtistExternalLinkTooltipPageReferences() { + const info = artistExternalLinkTooltipInfo; + + info.tooltips = + Array.from(document.getElementsByClassName('icons-tooltip')); + + info.tooltipRows = + info.tooltips.map(tooltip => + Array.from(tooltip.getElementsByClassName('icon'))); +} + +function addArtistExternalLinkTooltipInternalListeners() { + const info = artistExternalLinkTooltipInfo; + + hoverableTooltipInfo.event.whenTooltipShows.push(({tooltip}) => { + const {state} = info; + + if (info.tooltips.includes(tooltip)) { + clearTimeout(state.hideInfoTimeout); + state.hideInfoTimeout = null; + } + }); + + hoverableTooltipInfo.event.whenTooltipHides.push(() => { + const {settings, state} = info; + + if (state.showingTooltipInfo) { + state.hideInfoTimeout = + setTimeout(() => { + state.hideInfoTimeout = null; + hideArtistExternalLinkTooltipInfo(); + }, settings.hideInfoAfterTooltipHiddenDelay); + } else { + clearTimeout(state.mouseIdleTimeout); + state.mouseIdleTimeout = null; + } + }); +} + +function addArtistExternalLinkTooltipPageListeners() { + const info = artistExternalLinkTooltipInfo; + + for (const tooltip of info.tooltips) { + tooltip.addEventListener('mousemove', domEvent => { + handleArtistExternalLinkTooltipMouseMoved(domEvent); + }); + + tooltip.addEventListener('mouseout', () => { + const {state} = info; + + clearTimeout(state.mouseIdleTimeout); + state.mouseIdleTimeout = null; + }); + } + + for (const tooltipRow of info.tooltipRows.flat()) { + tooltipRow.addEventListener('mouseover', () => { + const {state} = info; + + clearTimeout(state.mouseIdleTimeout); + state.mouseIdleTimeout = null; + }); + } +} + +function handleArtistExternalLinkTooltipMouseMoved(domEvent) { + const info = artistExternalLinkTooltipInfo; + const {settings, state} = info; + + if (state.showingTooltipInfo) { + return; + } + + // Clean out expired mouse movements + + const expiryTime = 1000; + + if (!empty(state.mouseMovementTimestamps)) { + const firstRecentMovementIndex = + state.mouseMovementTimestamps + .findIndex(value => Date.now() - value <= expiryTime); + + if (firstRecentMovementIndex === -1) { + state.mouseMovementTimestamps.splice(0); + state.mouseMovementPositions.splice(0); + } else if (firstRecentMovementIndex > 0) { + state.mouseMovementTimestamps.splice(0, firstRecentMovementIndex - 1); + state.mouseMovementPositions.splice(0, firstRecentMovementIndex - 1); + } + } + + const currentMovementDistance = + Math.sqrt(domEvent.movementX ** 2 + domEvent.movementY ** 2); + + state.mouseMovementTimestamps.push(Date.now()); + state.mouseMovementPositions.push([domEvent.screenX, domEvent.screenY]); + + // We can't really compute speed without having + // at least two data points! + if (state.mouseMovementPositions.length < 2) { + return; + } + + const movementTravelDistances = + state.mouseMovementPositions.map((current, index, array) => { + if (index === 0) return 0; + + const previous = array[index - 1]; + const deltaX = current[0] - previous[0]; + const deltaY = current[1] - previous[1]; + return Math.sqrt(deltaX ** 2 + deltaY ** 2); + }); + + const totalTravelDistance = + accumulateSum(movementTravelDistances); + + // In seconds rather than milliseconds. + const timeSinceFirstMovement = + (Date.now() - state.mouseMovementTimestamps[0]) / 1000; + + const averageSpeed = + Math.floor(totalTravelDistance / timeSinceFirstMovement); + + if (averageSpeed > settings.maximumIdleSpeed) { + clearTimeout(state.mouseIdleTimeout); + state.mouseIdleTimeout = null; + } + + if (state.mouseIdleTimeout) { + return; + } + + state.mouseIdleTimeout = + setTimeout(() => { + state.mouseIdleTimeout = null; + showArtistExternalLinkTooltipInfo(); + }, settings.mouseIdleShowInfoDelay); +} + +function showArtistExternalLinkTooltipInfo() { + const info = artistExternalLinkTooltipInfo; + const {state} = info; + + state.showingTooltipInfo = true; + + for (const tooltip of info.tooltips) { + tooltip.classList.add('show-info'); + } +} + +function hideArtistExternalLinkTooltipInfo() { + const info = artistExternalLinkTooltipInfo; + const {state} = info; + + state.showingTooltipInfo = false; + + for (const tooltip of info.tooltips) { + tooltip.classList.remove('show-info'); + } +} + +clientSteps.getPageReferences.push(getArtistExternalLinkTooltipPageReferences); +clientSteps.addInternalListeners.push(addArtistExternalLinkTooltipInternalListeners); +clientSteps.addPageListeners.push(addArtistExternalLinkTooltipPageListeners); + // Sticky commentary sidebar ------------------------------ const albumCommentarySidebarInfo = initInfo('albumCommentarySidebarInfo', { diff --git a/src/static/site6.css b/src/static/site6.css index 36662e7..a29158c 100644 --- a/src/static/site6.css +++ b/src/static/site6.css @@ -594,6 +594,48 @@ li:not(:first-child:last-child) .tooltip, user-select: none; cursor: default; + + display: grid; + + grid-template-columns: + [icon-start] auto [icon-end domain-start] auto [domain-end]; +} + +.icons-tooltip .icon { + grid-column-start: icon-start; + grid-column-end: icon-end; +} + +.icons-tooltip .icon-platform { + display: none; + + grid-column-start: domain-start; + grid-column-end: domain-end; + + --icon-platform-opacity: 0.8; + padding-right: 4px; + opacity: 0.8; +} + +.icons-tooltip.show-info .icon-platform { + display: inline; + animation: icon-platform 0.2s forwards linear; +} + +@keyframes icon-platform { + from { + opacity: 0; + } + + to { + opacity: var(--icon-platform-opacity); + } +} + +.icons-tooltip .icon:hover + .icon-platform { + --icon-platform-opacity: 1; + text-decoration: underline; + text-decoration-color: #ffffffaa; } .datetimestamp-tooltip .tooltip-content, -- cgit 1.3.0-6-gf8a5