From 45fb872e3c9db62da126c94c3219133b4945b532 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 21 Nov 2023 07:23:58 -0400 Subject: css: allow custom scroll margin offset per-element --- src/static/site5.css | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src/static') diff --git a/src/static/site5.css b/src/static/site5.css index bb83fe67..6cdc0c35 100644 --- a/src/static/site5.css +++ b/src/static/site5.css @@ -1310,6 +1310,10 @@ h3.content-heading { /* Sticky heading */ +[id] { + --custom-scroll-offset: 0px; +} + #content [id] { /* Adjust scroll margin. */ scroll-margin-top: calc( @@ -1317,6 +1321,7 @@ h3.content-heading { + 33px /* Sticky subheading */ - 1em /* One line of text (align bottom) */ - 12px /* Padding for hanging letters & focus ring */ + + var(--custom-scroll-offset) /* Customizable offset */ ); } -- cgit 1.3.0-6-gf8a5 From 7f56163ad123b3ba8da431630d3da67f5444adb6 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 21 Nov 2023 07:25:30 -0400 Subject: css: allow sticky heading text to occupy blank area in thin layout --- src/static/site5.css | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'src/static') diff --git a/src/static/site5.css b/src/static/site5.css index 6cdc0c35..ea27e35e 100644 --- a/src/static/site5.css +++ b/src/static/site5.css @@ -1766,6 +1766,13 @@ html[data-language-code="preview-en"][data-url-key="localized.home"] #content z-index: 2; } + /* Let sticky heading text span past lower-index cover art */ + + .content-sticky-heading-container.has-cover .content-sticky-heading-row, + .content-sticky-heading-container.has-cover .content-sticky-subheading-row { + grid-template-columns: 1fr 90px; + } + /* Disable grid features, just line header children up vertically */ #header { -- cgit 1.3.0-6-gf8a5 From 745eb45531afb233a49433b18e3095238eb41b84 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 21 Nov 2023 07:22:15 -0400 Subject: client: internal beforeHashLinkScrolls listener --- src/static/client3.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'src/static') diff --git a/src/static/client3.js b/src/static/client3.js index 8372a268..4a5dffc2 100644 --- a/src/static/client3.js +++ b/src/static/client3.js @@ -576,6 +576,7 @@ const hashLinkInfo = clientInfo.hashLinkInfo = { }, event: { + beforeHashLinkScrolls: [], whenHashLinkClicked: [], }, }; @@ -638,6 +639,21 @@ function addHashLinkListeners() { return; } + // Don't do anything if the target element isn't actually visible! + if (target.offsetParent === null) { + return; + } + + // Allow event handlers to prevent scrolling. + for (const handler of event.beforeHashLinkScrolls) { + if (handler({ + link: hashLink, + target, + }) === false) { + return; + } + } + // Hide skipper box right away, so the layout is updated on time for the // math operations coming up next. const skipper = document.getElementById('skippers'); @@ -675,6 +691,7 @@ function addHashLinkListeners() { for (const handler of event.whenHashLinkClicked) { handler({ link: hashLink, + target, }); } }); -- cgit 1.3.0-6-gf8a5 From bad238355e19c4fef5e5f3b41df88fa9b1b84aaa Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 21 Nov 2023 07:31:45 -0400 Subject: content, client, css: generateAdditionalNamesBox --- src/static/client3.js | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/static/site5.css | 66 +++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) (limited to 'src/static') diff --git a/src/static/client3.js b/src/static/client3.js index 4a5dffc2..94ba4a23 100644 --- a/src/static/client3.js +++ b/src/static/client3.js @@ -1260,6 +1260,96 @@ function loadImage(imageUrl, onprogress) { }); } +// "Additional names" box --------------------------------- + +const additionalNamesBoxInfo = clientInfo.additionalNamesBox = { + box: null, + links: null, + mainContentContainer: null, + + state: { + visible: false, + }, +}; + +function getAdditionalNamesBoxReferences() { + const info = additionalNamesBoxInfo; + + info.box = + document.getElementById('additional-names-box'); + + info.links = + document.querySelectorAll('a[href="#additional-names-box"]'); + + info.mainContentContainer = + document.querySelector('#content .main-content-container'); +} + +function addAdditionalNamesBoxInternalListeners() { + const info = additionalNamesBoxInfo; + + hashLinkInfo.event.beforeHashLinkScrolls.push(({target}) => { + if (target === info.box) { + return false; + } + }); +} + +function addAdditionalNamesBoxListeners() { + const info = additionalNamesBoxInfo; + + for (const link of info.links) { + link.addEventListener('click', domEvent => { + handleAdditionalNamesBoxLinkClicked(domEvent); + }); + } +} + +function handleAdditionalNamesBoxLinkClicked(domEvent) { + const info = additionalNamesBoxInfo; + const {state} = info; + + domEvent.preventDefault(); + + if (!info.box || !info.mainContentContainer) return; + + const margin = + +(cssProp(info.box, 'scroll-margin-top').replace('px', '')); + + const {top} = + (state.visible + ? info.box.getBoundingClientRect() + : info.mainContentContainer.getBoundingClientRect()); + + if (top + 20 < margin || top > 0.4 * window.innerHeight) { + if (!state.visible) { + toggleAdditionalNamesBox(); + } + + window.scrollTo({ + top: window.scrollY + top - margin, + behavior: 'smooth', + }); + } else { + toggleAdditionalNamesBox(); + } +} + +function toggleAdditionalNamesBox() { + const info = additionalNamesBoxInfo; + const {state} = info; + + state.visible = !state.visible; + info.box.style.display = + (state.visible + ? 'block' + : 'none'); +} + +clientSteps.getPageReferences.push(getAdditionalNamesBoxReferences); +clientSteps.addInternalListeners.push(addAdditionalNamesBoxInternalListeners); +clientSteps.addPageListeners.push(addAdditionalNamesBoxListeners); + // Group contributions table ------------------------------ const groupContributionsTableInfo = diff --git a/src/static/site5.css b/src/static/site5.css index ea27e35e..31b2995b 100644 --- a/src/static/site5.css +++ b/src/static/site5.css @@ -802,6 +802,68 @@ html[data-url-key="localized.listing"][data-url-value0="random"] #content a:not( opacity: 0.7; } +/* Additional names (heading and box) */ + +h1 a[href="#additional-names-box"] { + color: inherit; + text-decoration: underline; + text-decoration-style: dotted; +} + +h1 a[href="#additional-names-box"]:hover { + text-decoration-style: solid; +} + +#additional-names-box { + --custom-scroll-offset: calc(0.5em - 2px); + + margin: 1em 0 1em -10px; + padding: 15px 20px 10px 20px; + width: max-content; + max-width: min(60vw, 600px); + + border: 1px dotted var(--primary-color); + border-radius: 6px; + + background: + linear-gradient(var(--bg-color), var(--bg-color)), + linear-gradient(#000000bb, #000000bb), + var(--primary-color); + + box-shadow: 0 -2px 6px -1px var(--dim-color) inset; + + display: none; +} + +#additional-names-box > :first-child { margin-top: 0; } +#additional-names-box > :last-child { margin-bottom: 0; } + +#additional-names-box p { + padding-left: 10px; + padding-right: 10px; + margin-bottom: 0; + font-style: oblique; +} + +#additional-names-box ul { + padding-left: 10px; + margin-top: 0.5em; +} + +#additional-names-box li .additional-name { + margin-right: 0.25em; +} + +#additional-names-box li .additional-name .content-image { + margin-bottom: 0.25em; + margin-top: 0.5em; +} + +#additional-names-box li .annotation { + opacity: 0.8; + display: inline-block; +} + /* Images */ .image-container { @@ -1760,6 +1822,10 @@ html[data-language-code="preview-en"][data-url-key="localized.home"] #content max-width: unset; } + #additional-names-box { + max-width: unset; + } + /* Show sticky heading above cover art */ .content-sticky-heading-container { -- cgit 1.3.0-6-gf8a5 From c11edada828dc734cce6988e5819630a73326085 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 21 Jul 2023 20:06:32 -0300 Subject: content, test: linkContribution: tooltip icons --- src/static/client3.js | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/static/site5.css | 42 ++++++++++++++ 2 files changed, 192 insertions(+) (limited to 'src/static') diff --git a/src/static/client3.js b/src/static/client3.js index 8372a268..091d1fcf 100644 --- a/src/static/client3.js +++ b/src/static/client3.js @@ -958,6 +958,8 @@ clientSteps.addPageListeners.push(addScrollListenerForStickyHeadings); // Image overlay ------------------------------------------ +// TODO: Update to clientSteps style. + function addImageOverlayClickHandlers() { const container = document.getElementById('image-overlay-container'); @@ -1245,6 +1247,8 @@ function loadImage(imageUrl, onprogress) { // Group contributions table ------------------------------ +// TODO: Update to clientSteps style. + const groupContributionsTableInfo = Array.from(document.querySelectorAll('#content dl')) .filter(dl => dl.querySelector('a.group-contributions-sort-button')) @@ -1277,6 +1281,152 @@ for (const info of groupContributionsTableInfo) { }); } +// Artist link icon tooltips ------------------------------ + +// TODO: Update to clientSteps style. + +const linkIconTooltipInfo = + Array.from(document.querySelectorAll('span.contribution.has-tooltip')) + .map(span => ({ + mainLink: span.querySelector('a'), + iconsContainer: span.querySelector('span.icons-tooltip'), + iconLinks: span.querySelectorAll('span.icons-tooltip a'), + })); + +for (const info of linkIconTooltipInfo) { + const focusElements = + [info.mainLink, ...info.iconLinks]; + + const hoverElements = + [info.mainLink, info.iconsContainer]; + + let hidden = true; + + const show = () => { + info.iconsContainer.classList.add('visible'); + info.iconsContainer.inert = false; + hidden = false; + }; + + const hide = () => { + info.iconsContainer.classList.remove('visible'); + info.iconsContainer.inert = true; + hidden = true; + }; + + const considerHiding = () => { + if (hoverElements.some(el => el.matches(':hover'))) { + return; + } + + if (focusElements.includes(document.activeElement)) { + return; + } + + if (justTouched) { + return; + } + + hide(); + }; + + // Hover (pointer) + + let hoverTimeout; + + info.mainLink.addEventListener('mouseenter', () => { + if (hidden) { + hoverTimeout = setTimeout(show, 250); + } + }); + + info.mainLink.addEventListener('mouseout', () => { + if (hidden) { + clearTimeout(hoverTimeout); + } else { + considerHiding(); + } + }); + + info.iconsContainer.addEventListener('mouseout', () => { + if (!hidden) { + considerHiding(); + } + }); + + // Focus (keyboard) + + let focusTimeout; + + info.mainLink.addEventListener('focus', () => { + focusTimeout = setTimeout(show, 750); + }); + + info.mainLink.addEventListener('blur', () => { + clearTimeout(focusTimeout); + }); + + info.iconsContainer.addEventListener('focusout', () => { + requestAnimationFrame(considerHiding); + }); + + info.mainLink.addEventListener('blur', () => { + requestAnimationFrame(considerHiding); + }); + + // Touch (finger) + + let justTouched = false; + let touchTimeout; + + info.mainLink.addEventListener('touchend', event => { + let wasTarget = false; + + for (const touch of event.changedTouches) { + if (touch.target === info.mainLink) { + wasTarget = true; + break; + } + } + + if (!wasTarget) { + return; + } + + justTouched = true; + + clearTimeout(touchTimeout); + touchTimeout = setTimeout(() => { + justTouched = false; + }, 250); + + show(); + }); + + info.mainLink.addEventListener('click', event => { + if (hidden && justTouched) { + event.preventDefault(); + event.target.focus(); + show(); + } + }); + + document.body.addEventListener('touchend', event => { + const touches = [...event.changedTouches, ...event.touches]; + for (const {clientX, clientY} of touches) { + const touchEl = document.elementFromPoint(clientX, clientY); + if (!touchEl) continue; + + for (const hoverEl of hoverElements) { + if (touchEl === hoverEl) return; + if (hoverEl.contains(touchEl)) return; + } + } + + hide(); + }); +} + // Sticky commentary sidebar ------------------------------ const albumCommentarySidebarInfo = clientInfo.albumCommentarySidebarInfo = { diff --git a/src/static/site5.css b/src/static/site5.css index bb83fe67..06696799 100644 --- a/src/static/site5.css +++ b/src/static/site5.css @@ -427,6 +427,7 @@ a { a:hover { text-decoration: underline; + text-decoration-style: solid !important; } a.current { @@ -472,11 +473,52 @@ a:not([href]):hover { white-space: nowrap; } +.contribution { + position: relative; +} + +.contribution.has-tooltip a { + text-decoration: underline; + text-decoration-style: dotted; +} + .icons { font-style: normal; white-space: nowrap; } +.icons-tooltip { + position: absolute; + z-index: 999; + left: -12px; + top: calc(1em - 2px); + padding: 4px 12px 6px 8px; +} + +.icons-tooltip:not(.visible) { + display: none; +} + +.icons-tooltip-content { + display: block; + padding: 6px 2px 2px 2px; + background: var(--bg-black-color); + border: 1px dotted var(--primary-color); + border-radius: 4px; + + -webkit-user-select: none; + user-select: none; + cursor: default; +} + +.icons a:hover { + filter: brightness(1.4); +} + +.icons a { + padding: 0 3px; +} + .icon { display: inline-block; width: 24px; -- cgit 1.3.0-6-gf8a5 From 10140f5b90e0fa9b38cdacfa23b10d96fb6fd189 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 9 Nov 2023 22:30:23 -0400 Subject: client: dispatchInternalEvent utility --- src/static/client3.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) (limited to 'src/static') diff --git a/src/static/client3.js b/src/static/client3.js index 091d1fcf..84a66e3b 100644 --- a/src/static/client3.js +++ b/src/static/client3.js @@ -81,6 +81,31 @@ function fetchData(type, directory) { ); } +function dispatchInternalEvent(event, eventName, ...args) { + const [infoName] = + Object.entries(clientInfo) + .find(pair => pair[1].event === event); + + if (!infoName) { + throw new Error(`Expected event to be stored on clientInfo`); + } + + const {[eventName]: listeners} = event; + + if (!listeners) { + throw new Error(`Event name "${eventName}" isn't stored on ${infoName}.event`); + } + + for (const listener of listeners) { + try { + listener(...args); + } catch (error) { + console.warn(`Uncaught error in listener for ${infoName}.${eventName}`); + console.debug(error); + } + } +} + // JS-based links ----------------------------------------- const scriptedLinkInfo = clientInfo.scriptedLinkInfo = { @@ -672,6 +697,8 @@ function addHashLinkListeners() { processScrollingAfterHashLinkClicked(); + dispatchInternalEvent(event, 'whenHashLinkClicked', {link: hashLink}); + for (const handler of event.whenHashLinkClicked) { handler({ link: hashLink, -- cgit 1.3.0-6-gf8a5 From c34da87fb949c7797a1f273264720798dc7341ab Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 9 Nov 2023 22:32:08 -0400 Subject: client: add hoverable tooltip system, logic pulled from info cards While this system comprehensively covers everything that info cards did (which was generally smarter hovering logic than newer code for external icon tooltips), it isn't focus- and touch-capable yet, so isn't quite done within this commit. However, the interface this system provides to others is baked and fully implemented here. --- src/static/client3.js | 208 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 200 insertions(+), 8 deletions(-) (limited to 'src/static') diff --git a/src/static/client3.js b/src/static/client3.js index 84a66e3b..acd85880 100644 --- a/src/static/client3.js +++ b/src/static/client3.js @@ -354,17 +354,209 @@ if ( }); } -// Data & info card --------------------------------------- +// Tooltip-style hover (infrastructure) ------------------- -/* -const NORMAL_HOVER_INFO_DELAY = 750; -const FAST_HOVER_INFO_DELAY = 250; -const END_FAST_HOVER_DELAY = 500; -const HIDE_HOVER_DELAY = 250; +const hoverableTooltipInfo = clientInfo.hoverableTooltipInfo = { + settings: { + normalHoverInfoDelay: 400, + fastHoveringInfoDelay: 150, + + endFastHoveringDelay: 500, + + hideTooltipDelay: 500, + }, + + state: { + // These maps store a record for each registered element and related state + // and registration info, if applicable. + registeredTooltips: new Map(), + registeredHoverables: new Map(), + + // These are common across all tooltips, rather than stored individually, + // based on the principles that 1) only a single tooltip can be displayed + // at once, and 2) only a single hoverable can be hovered at a once. + hoverTimeout: null, + hideTimeout: null, + currentlyShownTooltip: null, + + // Fast hovering is a global mode which is activated as soon as any tooltip + // is displayed and turns off after a delay of no hoverables being hovered. + // Note that fast hovering may be turned off while hovering a tooltip, but + // it will never be turned off while idling over a hoverable. + fastHovering: false, + endFastHoveringTimeout: false, + }, + + event: { + whenTooltipShouldBeShown: [], + whenTooltipShouldBeHidden: [], + }, +}; + +// Adds DOM event listeners, so must be called during addPageListeners step. +function registerTooltipElement(tooltip) { + const {state} = hoverableTooltipInfo; + + if (!tooltip) + throw new Error(`Expected tooltip`); + + if (state.registeredTooltips.has(tooltip)) + throw new Error(`This tooltip is already registered`); + + // No state or registration info here. + state.registeredTooltips.set(tooltip, {}); + + tooltip.addEventListener('mouseenter', () => { + handleTooltipMouseEntered(tooltip); + }); -let fastHover = false; -let endFastHoverTimeout = null; + tooltip.addEventListener('mouseleave', () => { + handleTooltipMouseLeft(tooltip); + }); +} + +// Adds DOM event listeners, so must be called during addPageListeners step. +function registerTooltipHoverableElement(hoverable, tooltip) { + const {state} = hoverableTooltipInfo; + + if (!hoverable || !tooltip) + if (hoverable) + throw new Error(`Expected hoverable and tooltip, got only hoverable`); + else + throw new Error(`Expected hoverable and tooltip, got neither`); + + if (!state.registeredTooltips.has(tooltip)) + throw new Error(`Register tooltip before registering hoverable`); + + if (state.registeredHoverables.has(hoverable)) + throw new Error(`This hoverable is already registered`); + + state.registeredHoverables.set(hoverable, {tooltip}); + + hoverable.addEventListener('mouseenter', () => { + handleTooltipHoverableMouseEntered(hoverable); + }); + + hoverable.addEventListener('mouseleave', () => { + handleTooltipHoverableMouseLeft(hoverable); + }); +} + +function handleTooltipMouseEntered(tooltip) { + const {state} = hoverableTooltipInfo; + + if (state.currentlyShownTooltip !== tooltip) return; + + // Don't time out the current tooltip while hovering it. + if (state.hideTimeout) { + clearTimeout(state.hideTimeout); + state.hideTimeout = null; + } +} +function handleTooltipMouseLeft(tooltip) { + const {state, settings} = hoverableTooltipInfo; + + if (state.currentlyShownTooltip !== tooltip) return; + + // Start timing out the current tooltip when it's left. This could be + // canceled by mousing over a hoverable, or back over the tooltip again. + if (!state.hideTimeout) { + state.hideTimeout = + setTimeout(() => { + state.hideTimeout = null; + hideCurrentlyShownTooltip(); + }, settings.hideTooltipDelay); + } +} + +function handleTooltipHoverableMouseEntered(hoverable) { + const {event, settings, state} = hoverableTooltipInfo; + + const hoverTimeoutDelay = + (state.fastHovering + ? settings.fastHoveringInfoDelay + : settings.normalHoverInfoDelay); + + // Start a timer to show the corresponding tooltip, with the delay depending + // on whether fast hovering or not. This could be canceled by mousing out of + // the hoverable. + state.hoverTimeout = + setTimeout(() => { + state.hoverTimeout = null; + state.fastHovering = true; + showTooltipFromHoverable(hoverable); + }, hoverTimeoutDelay); + + // Don't stop fast hovering while over any hoverable. + if (state.endFastHoveringTimeout) { + clearTimeout(state.endFastHoveringTimeout); + state.endFastHoveringTimeout = null; + } + + // Don't time out the current tooltip while over any hoverable. + if (state.hideTimeout) { + clearTimeout(state.hideTimeout); + state.hideTimeout = null; + } +} + +function handleTooltipHoverableMouseLeft(hoverable) { + const {state, settings} = hoverableTooltipInfo; + + // Don't show a tooltip when not over a hoverable! + if (state.hoverTimeout) { + clearTimeout(state.hoverTimeout); + state.hoverTimeout = null; + } + + // Start timing out fast hovering (if active) when not over a hoverable. + // This will only be canceled by mousing over another hoverable. + if (state.fastHovering && !state.endFastHoveringTimeout) { + state.endFastHoveringTimeout = + setTimeout(() => { + state.endFastHoveringTimeout = null; + state.fastHovering = false; + }, settings.endFastHoveringDelay); + } + + // Start timing out the current tooltip when mousing not over a hoverable. + // This could be canceled by mousing over another hoverable, or over the + // currently shown tooltip. + if (state.currentlyShownTooltip && !state.hideTimeout) { + state.hideTimeout = + setTimeout(() => { + state.hideTimeout = null; + hideCurrentlyShownTooltip(); + }, settings.hideTooltipDelay); + } +} + +function hideCurrentlyShownTooltip() { + const {event, state} = hoverableTooltipInfo; + const {currentlyShownTooltip: tooltip} = state; + + if (!tooltip) return; + + dispatchInternalEvent(event, 'whenTooltipShouldBeHidden', {tooltip}); + + state.currentlyShownTooltip = null; +} + +function showTooltipFromHoverable(hoverable) { + const {event, state} = hoverableTooltipInfo; + const {tooltip} = state.registeredHoverables.get(hoverable); + + hideCurrentlyShownTooltip(); + + dispatchInternalEvent(event, 'whenTooltipShouldBeShown', {hoverable, tooltip}); + + state.currentlyShownTooltip = tooltip; +} + +// Data & info card --------------------------------------- + +/* function colorLink(a, color) { console.warn('Info card link colors temporarily disabled: chroma.js required, no dependency linking for client.js yet'); return; -- cgit 1.3.0-6-gf8a5 From 15f72dcf7bec602b979621d6c9e9c6d11617ffbb Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 9 Nov 2023 22:35:18 -0400 Subject: client: integrate new tooltip system into external link icons Reference code currently retained, waiting for focus and touch support in the new tooltip system. But this commit should fully cover all the new integration needed! --- src/static/client3.js | 64 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) (limited to 'src/static') diff --git a/src/static/client3.js b/src/static/client3.js index acd85880..57922022 100644 --- a/src/static/client3.js +++ b/src/static/client3.js @@ -1502,8 +1502,67 @@ for (const info of groupContributionsTableInfo) { // Artist link icon tooltips ------------------------------ -// TODO: Update to clientSteps style. +const externalIconTooltipInfo = clientInfo.externalIconTooltipInfo = { + hoverableLinks: null, + iconContainers: null, +}; + +function getExternalIconTooltipReferences() { + const info = externalIconTooltipInfo; + + const spans = + Array.from(document.querySelectorAll('span.contribution.has-tooltip')); + + info.hoverableLinks = + spans + .map(span => span.querySelector('a')); + + info.iconContainers = + spans + .map(span => span.querySelector('span.icons-tooltip')); +} + +function addExternalIconTooltipInternalListeners() { + const info = externalIconTooltipInfo; + + hoverableTooltipInfo.event.whenTooltipShouldBeShown.push(({tooltip}) => { + if (!info.iconContainers.includes(tooltip)) return; + showExternalIconTooltip(tooltip); + }); + + hoverableTooltipInfo.event.whenTooltipShouldBeHidden.push(({tooltip}) => { + if (!info.iconContainers.includes(tooltip)) return; + hideExternalIconTooltip(tooltip); + }); +} +function showExternalIconTooltip(iconContainer) { + iconContainer.classList.add('visible'); + iconContainer.inert = false; +} + +function hideExternalIconTooltip(iconContainer) { + iconContainer.classList.remove('visible'); + iconContainer.inert = true; +} + +function addExternalIconTooltipPageListeners() { + const info = externalIconTooltipInfo; + + for (const {hoverable, tooltip} of stitchArrays({ + hoverable: info.hoverableLinks, + tooltip: info.iconContainers, + })) { + registerTooltipElement(tooltip); + registerTooltipHoverableElement(hoverable, tooltip); + } +} + +clientSteps.getPageReferences.push(getExternalIconTooltipReferences); +clientSteps.addInternalListeners.push(addExternalIconTooltipInternalListeners); +clientSteps.addPageListeners.push(addExternalIconTooltipPageListeners); + +/* const linkIconTooltipInfo = Array.from(document.querySelectorAll('span.contribution.has-tooltip')) .map(span => ({ @@ -1538,7 +1597,7 @@ for (const info of linkIconTooltipInfo) { return; } - if (focusElements.includes(document.activeElement)) { + if () { return; } @@ -1645,6 +1704,7 @@ for (const info of linkIconTooltipInfo) { hide(); }); } +*/ // Sticky commentary sidebar ------------------------------ -- cgit 1.3.0-6-gf8a5 From 4c319007bdf151064ffed7d275001414b95f24d6 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 10 Nov 2023 15:48:10 -0400 Subject: client: add basic tooltip focus behavior --- src/static/client3.js | 135 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 128 insertions(+), 7 deletions(-) (limited to 'src/static') diff --git a/src/static/client3.js b/src/static/client3.js index 57922022..57bc21a8 100644 --- a/src/static/client3.js +++ b/src/static/client3.js @@ -361,6 +361,8 @@ const hoverableTooltipInfo = clientInfo.hoverableTooltipInfo = { normalHoverInfoDelay: 400, fastHoveringInfoDelay: 150, + focusInfoDelay: 750, + endFastHoveringDelay: 500, hideTooltipDelay: 500, @@ -374,10 +376,13 @@ const hoverableTooltipInfo = clientInfo.hoverableTooltipInfo = { // These are common across all tooltips, rather than stored individually, // based on the principles that 1) only a single tooltip can be displayed - // at once, and 2) only a single hoverable can be hovered at a once. + // at once, and 2) likewise, only a single hoverable can be hovered, + // focused, or otherwise active at once. hoverTimeout: null, + focusTimeout: null, hideTimeout: null, currentlyShownTooltip: null, + currentlyActiveHoverable: null, // Fast hovering is a global mode which is activated as soon as any tooltip // is displayed and turns off after a delay of no hoverables being hovered. @@ -413,6 +418,19 @@ function registerTooltipElement(tooltip) { tooltip.addEventListener('mouseleave', () => { handleTooltipMouseLeft(tooltip); }); + + tooltip.addEventListener('focusin', () => { + handleTooltipReceivedFocus(tooltip); + }); + + tooltip.addEventListener('focusout', event => { + // This event gets activated for tabbing *between* links inside the + // tooltip, which is no good and certainly doesn't represent the focus + // leaving the tooltip. + if (tooltip.contains(event.relatedTarget)) return; + + handleTooltipLostFocus(tooltip); + }); } // Adds DOM event listeners, so must be called during addPageListeners step. @@ -440,6 +458,14 @@ function registerTooltipHoverableElement(hoverable, tooltip) { hoverable.addEventListener('mouseleave', () => { handleTooltipHoverableMouseLeft(hoverable); }); + + hoverable.addEventListener('focusin', () => { + handleTooltipHoverableReceivedFocus(hoverable); + }); + + hoverable.addEventListener('focusout', () => { + handleTooltipHoverableLostFocus(hoverable); + }); } function handleTooltipMouseEntered(tooltip) { @@ -455,7 +481,7 @@ function handleTooltipMouseEntered(tooltip) { } function handleTooltipMouseLeft(tooltip) { - const {state, settings} = hoverableTooltipInfo; + const {settings, state} = hoverableTooltipInfo; if (state.currentlyShownTooltip !== tooltip) return; @@ -470,6 +496,34 @@ function handleTooltipMouseLeft(tooltip) { } } +function handleTooltipReceivedFocus(tooltip) { + const {state} = hoverableTooltipInfo; + + // Cancel the tooltip-hiding timeout if it exists. The tooltip will never + // be hidden while it contains the focus anyway, but this ensures the timeout + // will be suitably reset when the tooltip loses focus. + if (state.hideTimeout) { + clearTimeout(state.hideTimeout); + state.hideTimeout = null; + } +} + +function handleTooltipLostFocus(tooltip) { + const {settings, state} = hoverableTooltipInfo; + + // Start timing out the current tooltip when it loses focus. This will be + // canceled if the tooltip receives focus again. Another tooltip might also + // display before this timeout runs, but since this is the same timeout name + // as all tooltip interactions, it'll get cleared appropriately. + if (!state.hideTimeout) { + state.hideTimeout = + setTimeout(() => { + state.hideTimeout = null; + hideCurrentlyShownTooltip(); + }, settings.hideTooltipDelay); + } +} + function handleTooltipHoverableMouseEntered(hoverable) { const {event, settings, state} = hoverableTooltipInfo; @@ -502,7 +556,7 @@ function handleTooltipHoverableMouseEntered(hoverable) { } function handleTooltipHoverableMouseLeft(hoverable) { - const {state, settings} = hoverableTooltipInfo; + const {settings, state} = hoverableTooltipInfo; // Don't show a tooltip when not over a hoverable! if (state.hoverTimeout) { @@ -532,26 +586,93 @@ function handleTooltipHoverableMouseLeft(hoverable) { } } +function handleTooltipHoverableReceivedFocus(hoverable) { + const {settings, state} = hoverableTooltipInfo; + + // Start a timer to show the corresponding tooltip. + state.focusTimeout = + setTimeout(() => { + state.focusTimeout = null; + showTooltipFromHoverable(hoverable); + }, settings.focusInfoDelay); +} + +function handleTooltipHoverableLostFocus(hoverable) { + const {settings, state} = hoverableTooltipInfo; + + // Don't show a tooltip from focusing a hoverable if it isn't focused + // anymore! If another hoverable is receiving focus, that will be evaluated + // and set its own focus timeout after we clear the previous one here. + if (state.focusTimeout) { + clearTimeout(state.focusTimeout); + state.focusTimeout = null; + } + + // Start timing out the current tooltip when the hoverable loses focus. + // Yes, even if focus is going *into* that very tooltip! This timeout will + // be immediately canceled, in that case. + if (!state.hideTimeout) { + state.hideTimeout = + setTimeout(() => { + state.hideTimeout = null; + hideCurrentlyShownTooltip(); + }, settings.hideTooltipDelay); + } +} + +function currentlyShownTooltipHasFocus() { + const {state} = hoverableTooltipInfo; + + const { + currentlyShownTooltip: tooltip, + currentlyActiveHoverable: hoverable, + } = state; + + // If there's no tooltip, it can't possibly have focus. + if (!tooltip) return false; + + // If the tooltip literally contains (or is) the focused element, then that's + // the principle condition we're looking for. + if (tooltip.contains(document.activeElement)) return true; + + // If the hoverable *which opened the tooltip* is focused, then that also + // represents the tooltip being focused (in its currently shown state). + if (hoverable.contains(document.activeElement)) return true; + + return false; +} + function hideCurrentlyShownTooltip() { const {event, state} = hoverableTooltipInfo; const {currentlyShownTooltip: tooltip} = state; - if (!tooltip) return; + // If there was no tooltip to begin with, we're functionally in the desired + // state already, so return true. + if (!tooltip) return true; - dispatchInternalEvent(event, 'whenTooltipShouldBeHidden', {tooltip}); + // Never hide the tooltip if it's focused. + if (currentlyShownTooltipHasFocus()) return false; state.currentlyShownTooltip = null; + state.currentlyActiveHoverable = null; + + dispatchInternalEvent(event, 'whenTooltipShouldBeHidden', {tooltip}); + + return true; } function showTooltipFromHoverable(hoverable) { const {event, state} = hoverableTooltipInfo; const {tooltip} = state.registeredHoverables.get(hoverable); - hideCurrentlyShownTooltip(); + if (!hideCurrentlyShownTooltip()) return false; + + state.currentlyShownTooltip = tooltip; + state.currentlyActiveHoverable = hoverable; dispatchInternalEvent(event, 'whenTooltipShouldBeShown', {hoverable, tooltip}); - state.currentlyShownTooltip = tooltip; + return true; } // Data & info card --------------------------------------- -- cgit 1.3.0-6-gf8a5 From 7a234a0b80f5db5d84388f661f473b561b2b0953 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 11 Nov 2023 17:42:40 -0400 Subject: client: more specialized tooltip focus behavior --- src/static/client3.js | 95 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 58 insertions(+), 37 deletions(-) (limited to 'src/static') diff --git a/src/static/client3.js b/src/static/client3.js index 57bc21a8..d4e47f0a 100644 --- a/src/static/client3.js +++ b/src/static/client3.js @@ -358,13 +358,26 @@ if ( const hoverableTooltipInfo = clientInfo.hoverableTooltipInfo = { settings: { + // Hovering has two speed settings. The normal setting is used by default, + // and once a tooltip is displayed as a result of hover, the entire tooltip + // system will enter a "fast hover mode" - hovering will activate tooltips + // sooner. "Fast hover mode" is disabled after a sustained duration of not + // hovering over any hoverables; it's meant only to accelerate switching + // tooltips while still deciding, or getting a quick overview across more + // than one tooltip. normalHoverInfoDelay: 400, fastHoveringInfoDelay: 150, + endFastHoveringDelay: 500, + // Focusing has a single speed setting, which is how long it will take to + // enter a functional "focus mode" (though it's not actually implemented + // in terms of this state). As soon as "focus mode" is entered, the tooltip + // for the current hoverable is displayed, and focusing another hoverable + // will cause the current tooltip to be swapped for that one immediately. + // "Focus mode" ends as soon as anything apart from a tooltip or hoverable + // is focused, and it will be necessary to wait on this delay again. focusInfoDelay: 750, - endFastHoveringDelay: 500, - hideTooltipDelay: 500, }, @@ -383,6 +396,7 @@ const hoverableTooltipInfo = clientInfo.hoverableTooltipInfo = { hideTimeout: null, currentlyShownTooltip: null, currentlyActiveHoverable: null, + tooltipWasJustHidden: false, // Fast hovering is a global mode which is activated as soon as any tooltip // is displayed and turns off after a delay of no hoverables being hovered. @@ -419,17 +433,17 @@ function registerTooltipElement(tooltip) { handleTooltipMouseLeft(tooltip); }); - tooltip.addEventListener('focusin', () => { - handleTooltipReceivedFocus(tooltip); + tooltip.addEventListener('focusin', event => { + handleTooltipReceivedFocus(tooltip, event.relatedTarget); }); tooltip.addEventListener('focusout', event => { // This event gets activated for tabbing *between* links inside the // tooltip, which is no good and certainly doesn't represent the focus // leaving the tooltip. - if (tooltip.contains(event.relatedTarget)) return; + if (currentlyShownTooltipHasFocus(event.relatedTarget)) return; - handleTooltipLostFocus(tooltip); + handleTooltipLostFocus(tooltip, event.relatedTarget); }); } @@ -459,12 +473,12 @@ function registerTooltipHoverableElement(hoverable, tooltip) { handleTooltipHoverableMouseLeft(hoverable); }); - hoverable.addEventListener('focusin', () => { - handleTooltipHoverableReceivedFocus(hoverable); + hoverable.addEventListener('focusin', event => { + handleTooltipHoverableReceivedFocus(hoverable, event.relatedTarget); }); - hoverable.addEventListener('focusout', () => { - handleTooltipHoverableLostFocus(hoverable); + hoverable.addEventListener('focusout', event => { + handleTooltipHoverableLostFocus(hoverable, event.relatedTarget); }); } @@ -508,20 +522,11 @@ function handleTooltipReceivedFocus(tooltip) { } } -function handleTooltipLostFocus(tooltip) { +function handleTooltipLostFocus(tooltip, newlyFocusedElement) { const {settings, state} = hoverableTooltipInfo; - // Start timing out the current tooltip when it loses focus. This will be - // canceled if the tooltip receives focus again. Another tooltip might also - // display before this timeout runs, but since this is the same timeout name - // as all tooltip interactions, it'll get cleared appropriately. - if (!state.hideTimeout) { - state.hideTimeout = - setTimeout(() => { - state.hideTimeout = null; - hideCurrentlyShownTooltip(); - }, settings.hideTooltipDelay); - } + // Hide the current tooltip right away when it loses focus. + hideCurrentlyShownTooltip(); } function handleTooltipHoverableMouseEntered(hoverable) { @@ -586,18 +591,30 @@ function handleTooltipHoverableMouseLeft(hoverable) { } } -function handleTooltipHoverableReceivedFocus(hoverable) { +function handleTooltipHoverableReceivedFocus(hoverable, previouslyFocusedElement) { const {settings, state} = hoverableTooltipInfo; - // Start a timer to show the corresponding tooltip. + // By default, display the corresponding tooltip after a delay. + state.focusTimeout = setTimeout(() => { state.focusTimeout = null; showTooltipFromHoverable(hoverable); }, settings.focusInfoDelay); + + // If a tooltip was just hidden - which is almost certainly a result of the + // focus changing - then display this tooltip immediately, canceling the + // above timeout. + + if (state.tooltipWasJustHidden) { + clearTimeout(state.focusTimeout); + state.focusTimeout = null; + + showTooltipFromHoverable(hoverable); + } } -function handleTooltipHoverableLostFocus(hoverable) { +function handleTooltipHoverableLostFocus(hoverable, newlyFocusedElement) { const {settings, state} = hoverableTooltipInfo; // Don't show a tooltip from focusing a hoverable if it isn't focused @@ -608,19 +625,15 @@ function handleTooltipHoverableLostFocus(hoverable) { state.focusTimeout = null; } - // Start timing out the current tooltip when the hoverable loses focus. - // Yes, even if focus is going *into* that very tooltip! This timeout will - // be immediately canceled, in that case. - if (!state.hideTimeout) { - state.hideTimeout = - setTimeout(() => { - state.hideTimeout = null; - hideCurrentlyShownTooltip(); - }, settings.hideTooltipDelay); + // Unless focus is entering the tooltip itself, hide the tooltip immediately. + // This will set the tooltipWasJustHidden flag, which is detected by a newly + // focused hoverable, if applicable. + if (!currentlyShownTooltipHasFocus(newlyFocusedElement)) { + hideCurrentlyShownTooltip(); } } -function currentlyShownTooltipHasFocus() { +function currentlyShownTooltipHasFocus(focusElement = document.activeElement) { const {state} = hoverableTooltipInfo; const { @@ -633,11 +646,11 @@ function currentlyShownTooltipHasFocus() { // If the tooltip literally contains (or is) the focused element, then that's // the principle condition we're looking for. - if (tooltip.contains(document.activeElement)) return true; + if (tooltip.contains(focusElement)) return true; // If the hoverable *which opened the tooltip* is focused, then that also // represents the tooltip being focused (in its currently shown state). - if (hoverable.contains(document.activeElement)) return true; + if (hoverable.contains(focusElement)) return true; return false; } @@ -656,6 +669,12 @@ function hideCurrentlyShownTooltip() { state.currentlyShownTooltip = null; state.currentlyActiveHoverable = null; + // Set this for one tick of the event cycle. + state.tooltipWasJustHidden = true; + setTimeout(() => { + state.tooltipWasJustHidden = false; + }); + dispatchInternalEvent(event, 'whenTooltipShouldBeHidden', {tooltip}); return true; @@ -670,6 +689,8 @@ function showTooltipFromHoverable(hoverable) { state.currentlyShownTooltip = tooltip; state.currentlyActiveHoverable = hoverable; + state.tooltipWasJustHidden = false; + dispatchInternalEvent(event, 'whenTooltipShouldBeShown', {hoverable, tooltip}); return true; -- cgit 1.3.0-6-gf8a5 From db44a5ea5fd8cb3be1d491687acb64eba966abea Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 12 Nov 2023 10:59:17 -0400 Subject: client: most of touch implementation for tooltips --- src/static/client3.js | 195 +++++++++++++++++++++++--------------------------- 1 file changed, 90 insertions(+), 105 deletions(-) (limited to 'src/static') diff --git a/src/static/client3.js b/src/static/client3.js index d4e47f0a..5cc34461 100644 --- a/src/static/client3.js +++ b/src/static/client3.js @@ -393,10 +393,12 @@ const hoverableTooltipInfo = clientInfo.hoverableTooltipInfo = { // focused, or otherwise active at once. hoverTimeout: null, focusTimeout: null, + touchTimeout: null, hideTimeout: null, currentlyShownTooltip: null, currentlyActiveHoverable: null, tooltipWasJustHidden: false, + hoverableWasRecentlyTouched: false, // Fast hovering is a global mode which is activated as soon as any tooltip // is displayed and turns off after a delay of no hoverables being hovered. @@ -474,11 +476,19 @@ function registerTooltipHoverableElement(hoverable, tooltip) { }); hoverable.addEventListener('focusin', event => { - handleTooltipHoverableReceivedFocus(hoverable, event.relatedTarget); + handleTooltipHoverableReceivedFocus(hoverable, event); }); hoverable.addEventListener('focusout', event => { - handleTooltipHoverableLostFocus(hoverable, event.relatedTarget); + handleTooltipHoverableLostFocus(hoverable, event); + }); + + hoverable.addEventListener('touchend', event => { + handleTooltipHoverableTouchEnded(hoverable, event); + }); + + hoverable.addEventListener('click', event => { + handleTooltipHoverableClicked(hoverable, event); }); } @@ -522,7 +532,7 @@ function handleTooltipReceivedFocus(tooltip) { } } -function handleTooltipLostFocus(tooltip, newlyFocusedElement) { +function handleTooltipLostFocus(tooltip) { const {settings, state} = hoverableTooltipInfo; // Hide the current tooltip right away when it loses focus. @@ -591,7 +601,7 @@ function handleTooltipHoverableMouseLeft(hoverable) { } } -function handleTooltipHoverableReceivedFocus(hoverable, previouslyFocusedElement) { +function handleTooltipHoverableReceivedFocus(hoverable) { const {settings, state} = hoverableTooltipInfo; // By default, display the corresponding tooltip after a delay. @@ -614,7 +624,7 @@ function handleTooltipHoverableReceivedFocus(hoverable, previouslyFocusedElement } } -function handleTooltipHoverableLostFocus(hoverable, newlyFocusedElement) { +function handleTooltipHoverableLostFocus(hoverable, domEvent) { const {settings, state} = hoverableTooltipInfo; // Don't show a tooltip from focusing a hoverable if it isn't focused @@ -628,11 +638,63 @@ function handleTooltipHoverableLostFocus(hoverable, newlyFocusedElement) { // Unless focus is entering the tooltip itself, hide the tooltip immediately. // This will set the tooltipWasJustHidden flag, which is detected by a newly // focused hoverable, if applicable. - if (!currentlyShownTooltipHasFocus(newlyFocusedElement)) { + if (!currentlyShownTooltipHasFocus(domEvent.relatedTarget)) { hideCurrentlyShownTooltip(); } } +function handleTooltipHoverableTouchEnded(hoverable, domEvent) { + const {settings, state} = hoverableTooltipInfo; + const {tooltip} = state.registeredHoverables.get(hoverable); + + // Don't proceed if this hoverable's tooltip is already visible - in that + // case touching the hoverable again should behave just like a normal click. + if (state.currentlyShownTooltip === tooltip) return; + + const touchEndedOverHoverable = + Array.from(domEvent.changedTouches) + .some(touch => + hoverable.contains( + document.elementFromPoint(touch.clientX, touch.clientY))); + + if (!touchEndedOverHoverable) { + return; + } + + if (state.touchTimeout) { + clearTimeout(state.touchTimeout); + state.touchTimeout = null; + } + + // Show the tooltip right away. + showTooltipFromHoverable(hoverable); + + // Set a state, for a brief but not instantaneous period, indicating that a + // hoverable was recently touched. The touchend event may precede the click + // event by some time, and we don't want to navigate away from the page as + // a result of the click event which this touch precipitated. + state.hoverableWasRecentlyTouched = true; + state.touchTimeout = + setTimeout(() => { + state.hoverableWasRecentlyTouched = false; + }, 250); +} + +function handleTooltipHoverableClicked(hoverable, domEvent) { + const {state} = hoverableTooltipInfo; + const {tooltip} = state.registeredHoverables.get(hoverable); + + // Don't navigate away from the page if the this hoverable was recently + // touched (and had its tooltip activated). That flag won't be set if its + // tooltip was already open before the touch. + if ( + state.currentlyActiveHoverable === hoverable && + state.hoverableWasRecentlyTouched + ) { + event.preventDefault(); + } +} + function currentlyShownTooltipHasFocus(focusElement = document.activeElement) { const {state} = hoverableTooltipInfo; @@ -696,6 +758,28 @@ function showTooltipFromHoverable(hoverable) { return true; } +function addHoverableTooltipPageListeners() { + document.body.addEventListener('touchend', domEvent => { + const {state} = hoverableTooltipInfo; + + const touches = [...domEvent.changedTouches, ...domEvent.touches]; + const hoverables = Array.from(state.registeredHoverables.keys()); + + // TODO: https://github.com/tc39/proposal-iterator-helpers + const anyTouchOverAnyHoverable = + touches.some(({clientX, clientY}) => { + const element = document.elementFromPoint(clientX, clientY); + return hoverables.some(hoverable => hoverable.contains(element)); + }); + + if (!anyTouchOverAnyHoverable) { + hideCurrentlyShownTooltip(); + } + }); +} + +clientSteps.addPageListeners.push(addHoverableTooltipPageListeners); + // Data & info card --------------------------------------- /* @@ -864,53 +948,6 @@ const infoCard = (() => { }; })(); -function makeInfoCardLinkHandlers(type) { - let hoverTimeout = null; - - return { - mouseenter(evt) { - hoverTimeout = setTimeout( - () => { - fastHover = true; - infoCard.show(type, evt.target); - }, - fastHover ? FAST_HOVER_INFO_DELAY : NORMAL_HOVER_INFO_DELAY); - - clearTimeout(endFastHoverTimeout); - endFastHoverTimeout = null; - - infoCard.cancelHide(); - }, - - mouseleave() { - clearTimeout(hoverTimeout); - - if (fastHover && !endFastHoverTimeout) { - endFastHoverTimeout = setTimeout(() => { - endFastHoverTimeout = null; - fastHover = false; - }, END_FAST_HOVER_DELAY); - } - - infoCard.readyHide(); - }, - }; -} - -const infoCardLinkHandlers = { - track: makeInfoCardLinkHandlers('track'), -}; - -function addInfoCardLinkHandlers(type) { - for (const a of document.querySelectorAll(`a[data-${type}]`)) { - for (const [eventName, handler] of Object.entries( - infoCardLinkHandlers[type] - )) { - a.addEventListener(eventName, handler); - } - } -} - // Info cards are disa8led for now since they aren't quite ready for release, // 8ut you can try 'em out 8y setting this localStorage flag! // @@ -1793,58 +1830,6 @@ for (const info of linkIconTooltipInfo) { info.mainLink.addEventListener('blur', () => { requestAnimationFrame(considerHiding); }); - - // Touch (finger) - - let justTouched = false; - let touchTimeout; - - info.mainLink.addEventListener('touchend', event => { - let wasTarget = false; - - for (const touch of event.changedTouches) { - if (touch.target === info.mainLink) { - wasTarget = true; - break; - } - } - - if (!wasTarget) { - return; - } - - justTouched = true; - - clearTimeout(touchTimeout); - touchTimeout = setTimeout(() => { - justTouched = false; - }, 250); - - show(); - }); - - info.mainLink.addEventListener('click', event => { - if (hidden && justTouched) { - event.preventDefault(); - event.target.focus(); - show(); - } - }); - - document.body.addEventListener('touchend', event => { - const touches = [...event.changedTouches, ...event.touches]; - for (const {clientX, clientY} of touches) { - const touchEl = document.elementFromPoint(clientX, clientY); - if (!touchEl) continue; - - for (const hoverEl of hoverElements) { - if (touchEl === hoverEl) return; - if (hoverEl.contains(touchEl)) return; - } - } - - hide(); - }); } */ -- cgit 1.3.0-6-gf8a5 From 2d31d6daa66d711a6dc22b84ec0d4b79d776c4ba Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 12 Nov 2023 11:29:45 -0400 Subject: client: avoid processing touch events related to scrolling --- src/static/client3.js | 76 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 68 insertions(+), 8 deletions(-) (limited to 'src/static') diff --git a/src/static/client3.js b/src/static/client3.js index 5cc34461..db9e5505 100644 --- a/src/static/client3.js +++ b/src/static/client3.js @@ -406,6 +406,15 @@ const hoverableTooltipInfo = clientInfo.hoverableTooltipInfo = { // it will never be turned off while idling over a hoverable. fastHovering: false, endFastHoveringTimeout: false, + + // These track the identifiers of current touches and a record of current + // identifiers that are "banished" by scrolling - that is, touches which + // existed while the page scrolled and were probably responsible for that + // scrolling. This is a bit loose (we can't actually tell which touches + // caused the page to scroll) but it's intended to keep scrolling the page + // from causing the current tooltip to be hidden. + currentTouchIdentifiers: new Set(), + touchIdentifiersBanishedByScrolling: new Set(), }, event: { @@ -651,13 +660,25 @@ function handleTooltipHoverableTouchEnded(hoverable, domEvent) { // case touching the hoverable again should behave just like a normal click. if (state.currentlyShownTooltip === tooltip) return; - const touchEndedOverHoverable = - Array.from(domEvent.changedTouches) - .some(touch => - hoverable.contains( - document.elementFromPoint(touch.clientX, touch.clientY))); + const endedTouches = Array.from(domEvent.changedTouches); + + // Don't process touch events that were "banished" because the page was + // scrolled while those touches were active, and most likely as a result of + // them. + const unbanishedTouches = + endedTouches.filter(touch => + !state.touchIdentifiersBanishedByScrolling.has(touch.identifier)); + + if (empty(unbanishedTouches)) return; - if (!touchEndedOverHoverable) { + // Don't proceed if none of the (just-ended) touches ended over the + // hoverable. + const anyTouchEndedOverHoverable = + unbanishedTouches.some(touch => + hoverable.contains( + document.elementFromPoint(touch.clientX, touch.clientY))); + + if (!anyTouchEndedOverHoverable) { return; } @@ -759,15 +780,54 @@ function showTooltipFromHoverable(hoverable) { } function addHoverableTooltipPageListeners() { + document.body.addEventListener('touchstart', domEvent => { + const {state} = hoverableTooltipInfo; + for (const {identifier} of domEvent.changedTouches) { + state.currentTouchIdentifiers.add(identifier); + } + }); + + window.addEventListener('scroll', domEvent => { + const {state} = hoverableTooltipInfo; + for (const identifier of state.currentTouchIdentifiers) { + state.touchIdentifiersBanishedByScrolling.add(identifier); + } + }); + + document.body.addEventListener('touchend', domEvent => { + const {state} = hoverableTooltipInfo; + + const identifiers = + Array.from(domEvent.changedTouches) + .map(touch => touch.identifier); + + setTimeout(() => { + for (const identifier of identifiers) { + state.currentTouchIdentifiers.delete(identifier); + state.touchIdentifiersBanishedByScrolling.delete(identifier); + } + }); + }); + document.body.addEventListener('touchend', domEvent => { const {state} = hoverableTooltipInfo; - const touches = [...domEvent.changedTouches, ...domEvent.touches]; const hoverables = Array.from(state.registeredHoverables.keys()); + const endedTouches = Array.from(domEvent.changedTouches); + + // Don't process touch events that were "banished" because the page was + // scrolled while those touches were active, and most likely as a result of + // them. + const unbanishedTouches = + endedTouches.filter(touch => + !state.touchIdentifiersBanishedByScrolling.has(touch.identifier)); + + if (empty(unbanishedTouches)) return; + // TODO: https://github.com/tc39/proposal-iterator-helpers const anyTouchOverAnyHoverable = - touches.some(({clientX, clientY}) => { + unbanishedTouches.some(({clientX, clientY}) => { const element = document.elementFromPoint(clientX, clientY); return hoverables.some(hoverable => hoverable.contains(element)); }); -- cgit 1.3.0-6-gf8a5 From 5127fd36dcf5987f402cce0353768b1421d9b7b4 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 12 Nov 2023 11:34:43 -0400 Subject: client, css: minor tooltip fixes --- src/static/client3.js | 10 ++++++---- src/static/site5.css | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) (limited to 'src/static') diff --git a/src/static/client3.js b/src/static/client3.js index db9e5505..88df58de 100644 --- a/src/static/client3.js +++ b/src/static/client3.js @@ -813,6 +813,7 @@ function addHoverableTooltipPageListeners() { const {state} = hoverableTooltipInfo; const hoverables = Array.from(state.registeredHoverables.keys()); + const tooltips = Array.from(state.registeredTooltips.keys()); const endedTouches = Array.from(domEvent.changedTouches); @@ -825,14 +826,15 @@ function addHoverableTooltipPageListeners() { if (empty(unbanishedTouches)) return; - // TODO: https://github.com/tc39/proposal-iterator-helpers - const anyTouchOverAnyHoverable = + const anyTouchOverAnyHoverableOrTooltip = unbanishedTouches.some(({clientX, clientY}) => { const element = document.elementFromPoint(clientX, clientY); - return hoverables.some(hoverable => hoverable.contains(element)); + if (hoverables.some(el => el.contains(element))) return true; + if (tooltips.some(el => el.contains(element))) return true; + return false; }); - if (!anyTouchOverAnyHoverable) { + if (!anyTouchOverAnyHoverableOrTooltip) { hideCurrentlyShownTooltip(); } }); diff --git a/src/static/site5.css b/src/static/site5.css index 06696799..582681bb 100644 --- a/src/static/site5.css +++ b/src/static/site5.css @@ -489,7 +489,7 @@ a:not([href]):hover { .icons-tooltip { position: absolute; - z-index: 999; + z-index: 1; left: -12px; top: calc(1em - 2px); padding: 4px 12px 6px 8px; -- cgit 1.3.0-6-gf8a5 From 834a087643306090905a2c2f080324b1100c0710 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 12 Nov 2023 12:28:04 -0400 Subject: client: tooltip touch syntax cleanup --- src/static/client3.js | 49 ++++++++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 25 deletions(-) (limited to 'src/static') diff --git a/src/static/client3.js b/src/static/client3.js index 88df58de..9db9fc6c 100644 --- a/src/static/client3.js +++ b/src/static/client3.js @@ -660,21 +660,22 @@ function handleTooltipHoverableTouchEnded(hoverable, domEvent) { // case touching the hoverable again should behave just like a normal click. if (state.currentlyShownTooltip === tooltip) return; - const endedTouches = Array.from(domEvent.changedTouches); + const touches = Array.from(domEvent.changedTouches); + const identifiers = touches.map(touch => touch.identifier); // Don't process touch events that were "banished" because the page was // scrolled while those touches were active, and most likely as a result of // them. - const unbanishedTouches = - endedTouches.filter(touch => - !state.touchIdentifiersBanishedByScrolling.has(touch.identifier)); + filterMultipleArrays(touches, identifiers, + (_touch, identifier) => + !state.touchIdentifiersBanishedByScrolling.has(identifier)); - if (empty(unbanishedTouches)) return; + if (empty(touches)) return; // Don't proceed if none of the (just-ended) touches ended over the // hoverable. const anyTouchEndedOverHoverable = - unbanishedTouches.some(touch => + touches.some(touch => hoverable.contains( document.elementFromPoint(touch.clientX, touch.clientY))); @@ -780,29 +781,28 @@ function showTooltipFromHoverable(hoverable) { } function addHoverableTooltipPageListeners() { + const {state} = hoverableTooltipInfo; + + const getTouchIdentifiers = domEvent => + Array.from(domEvent.changedTouches) + .map(touch => touch.identifier) + .filter(identifier => typeof identifier !== 'undefined'); + document.body.addEventListener('touchstart', domEvent => { - const {state} = hoverableTooltipInfo; - for (const {identifier} of domEvent.changedTouches) { + for (const identifier of getTouchIdentifiers(domEvent)) { state.currentTouchIdentifiers.add(identifier); } }); - window.addEventListener('scroll', domEvent => { - const {state} = hoverableTooltipInfo; + window.addEventListener('scroll', () => { for (const identifier of state.currentTouchIdentifiers) { state.touchIdentifiersBanishedByScrolling.add(identifier); } }); document.body.addEventListener('touchend', domEvent => { - const {state} = hoverableTooltipInfo; - - const identifiers = - Array.from(domEvent.changedTouches) - .map(touch => touch.identifier); - setTimeout(() => { - for (const identifier of identifiers) { + for (const identifier of getTouchIdentifiers(domEvent)) { state.currentTouchIdentifiers.delete(identifier); state.touchIdentifiersBanishedByScrolling.delete(identifier); } @@ -810,24 +810,23 @@ function addHoverableTooltipPageListeners() { }); document.body.addEventListener('touchend', domEvent => { - const {state} = hoverableTooltipInfo; - const hoverables = Array.from(state.registeredHoverables.keys()); const tooltips = Array.from(state.registeredTooltips.keys()); - const endedTouches = Array.from(domEvent.changedTouches); + const touches = Array.from(domEvent.changedTouches); + const identifiers = touches.map(touch => touch.identifier); // Don't process touch events that were "banished" because the page was // scrolled while those touches were active, and most likely as a result of // them. - const unbanishedTouches = - endedTouches.filter(touch => - !state.touchIdentifiersBanishedByScrolling.has(touch.identifier)); + filterMultipleArrays(touches, identifiers, + (_touch, identifier) => + !state.touchIdentifiersBanishedByScrolling.has(identifier)); - if (empty(unbanishedTouches)) return; + if (empty(touches)) return; const anyTouchOverAnyHoverableOrTooltip = - unbanishedTouches.some(({clientX, clientY}) => { + touches.some(({clientX, clientY}) => { const element = document.elementFromPoint(clientX, clientY); if (hoverables.some(el => el.contains(element))) return true; if (tooltips.some(el => el.contains(element))) return true; -- cgit 1.3.0-6-gf8a5 From d443e32d044dd74cd1923e3538af0a63ff6c6835 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 12 Nov 2023 15:01:22 -0400 Subject: css: quick tooltip tweaks --- src/static/site5.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src/static') diff --git a/src/static/site5.css b/src/static/site5.css index 582681bb..8c2b07a1 100644 --- a/src/static/site5.css +++ b/src/static/site5.css @@ -506,8 +506,12 @@ a:not([href]):hover { border: 1px dotted var(--primary-color); border-radius: 4px; + -webkit-backdrop-filter: blur(2px); + backdrop-filter: blur(2px); + -webkit-user-select: none; - user-select: none; + user-select: none; + cursor: default; } -- cgit 1.3.0-6-gf8a5 From 0202375db8ccd03d98ed6c2ffbb800b67c026639 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 23 Nov 2023 09:16:23 -0400 Subject: content, css: vertical tooltips + basic external parsing --- src/static/site5.css | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) (limited to 'src/static') diff --git a/src/static/site5.css b/src/static/site5.css index 8c2b07a1..c1dfff82 100644 --- a/src/static/site5.css +++ b/src/static/site5.css @@ -477,7 +477,7 @@ a:not([href]):hover { position: relative; } -.contribution.has-tooltip a { +.contribution.has-tooltip > a { text-decoration: underline; text-decoration-style: dotted; } @@ -489,8 +489,8 @@ a:not([href]):hover { .icons-tooltip { position: absolute; - z-index: 1; - left: -12px; + z-index: 3; + left: -36px; top: calc(1em - 2px); padding: 4px 12px 6px 8px; } @@ -504,14 +504,21 @@ a:not([href]):hover { padding: 6px 2px 2px 2px; background: var(--bg-black-color); border: 1px dotted var(--primary-color); - border-radius: 4px; + border-radius: 6px; - -webkit-backdrop-filter: blur(2px); - backdrop-filter: blur(2px); + -webkit-backdrop-filter: + brightness(1.5) saturate(1.4) blur(4px); + + backdrop-filter: + brightness(1.5) saturate(1.4) blur(4px); -webkit-user-select: none; user-select: none; + box-shadow: + 0 3px 4px 4px #000000aa, + 0 -2px 4px -2px var(--primary-color) inset; + cursor: default; } @@ -538,6 +545,23 @@ a:not([href]):hover { fill: var(--primary-color); } +.icon.has-text { + display: block; + width: unset; + height: 1.4em; +} + +.icon.has-text > svg { + width: 18px; + height: 18px; + top: -0.1em; +} + +.icon.has-text > .icon-text { + margin-left: 24px; + padding-right: 8px; +} + .rerelease, .other-group-accent { opacity: 0.7; -- cgit 1.3.0-6-gf8a5 From 82bb115d8404b88fe8b1af1bf714b3c70969f99b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 21 Nov 2023 07:23:58 -0400 Subject: css: allow custom scroll margin offset per-element --- src/static/site5.css | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src/static') diff --git a/src/static/site5.css b/src/static/site5.css index c1dfff82..fb3cf057 100644 --- a/src/static/site5.css +++ b/src/static/site5.css @@ -1380,6 +1380,10 @@ h3.content-heading { /* Sticky heading */ +[id] { + --custom-scroll-offset: 0px; +} + #content [id] { /* Adjust scroll margin. */ scroll-margin-top: calc( @@ -1387,6 +1391,7 @@ h3.content-heading { + 33px /* Sticky subheading */ - 1em /* One line of text (align bottom) */ - 12px /* Padding for hanging letters & focus ring */ + + var(--custom-scroll-offset) /* Customizable offset */ ); } -- cgit 1.3.0-6-gf8a5 From 921f2d421d6ffb87fab1a2059a6c313b9c81f4f8 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 21 Nov 2023 07:25:30 -0400 Subject: css: allow sticky heading text to occupy blank area in thin layout --- src/static/site5.css | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'src/static') diff --git a/src/static/site5.css b/src/static/site5.css index fb3cf057..5a769545 100644 --- a/src/static/site5.css +++ b/src/static/site5.css @@ -1836,6 +1836,13 @@ html[data-language-code="preview-en"][data-url-key="localized.home"] #content z-index: 2; } + /* Let sticky heading text span past lower-index cover art */ + + .content-sticky-heading-container.has-cover .content-sticky-heading-row, + .content-sticky-heading-container.has-cover .content-sticky-subheading-row { + grid-template-columns: 1fr 90px; + } + /* Disable grid features, just line header children up vertically */ #header { -- cgit 1.3.0-6-gf8a5