From 1463a050754a9358d524ca9ffd777032f1a54639 Mon Sep 17 00:00:00 2001 From: Matthew Reichhoff Date: Sun, 14 Jun 2026 21:58:02 -0400 Subject: [PATCH 1/2] Revert "Looks like the blur/resize fix caused iOS flakiness" This reverts commit d5e87800b11e247c4c9fbf646e6773c727ff1a5f. --- public/js/modules/graph.js | 10 ++++++-- public/js/modules/main.js | 11 -------- public/js/modules/search.js | 51 +++++++++++++++++++++++++++---------- 3 files changed, 46 insertions(+), 26 deletions(-) diff --git a/public/js/modules/graph.js b/public/js/modules/graph.js index b30c7507..899287a7 100644 --- a/public/js/modules/graph.js +++ b/public/js/modules/graph.js @@ -1,6 +1,7 @@ import { switchToState, stateKeys, diagramKeys, switchDiagramView } from "./ui-orchestrator"; import { getActiveGraph } from "./options"; import { parsePinyin, trimTone, findPinyinRelationships } from "./pronunciation-parser"; +import { hanziBox } from "./dom.js"; import cytoscape from "cytoscape"; import fcose from 'cytoscape-fcose'; @@ -455,16 +456,21 @@ function toggleColorCodeVisibility() { let skipResize = false; let pendingSkipResizeTimeout = null; +function isSearchFocusedOnTouchDevice() { + return document.activeElement === hanziBox && window.matchMedia('(pointer: coarse)').matches; +} + function handleResize() { clearTimeout(pendingResizeTimeout); pendingResizeTimeout = setTimeout(() => { + const shouldSkipLayout = skipResize || isSearchFocusedOnTouchDevice(); // if the window resizes with the graph collapsed, re-expand it // note that switchDiagramView no-ops if we're going main-->main if (!window.matchMedia('(max-width:664px)').matches) { switchDiagramView(diagramKeys.main); } // TODO: probably want a sizeDirty bit we can check for when the graph isn't shown and a resize happens - if (!skipResize && cy && showingGraph) { + if (!shouldSkipLayout && cy && showingGraph) { cy.layout(mode === modes.graph ? layout(cy.nodes().length) : bfsLayout(root)).run(); } skipResize = false; @@ -519,4 +525,4 @@ function initialize() { matchMedia("(prefers-color-scheme: dark)").addEventListener("change", updateColorScheme); } -export { initialize, isInGraph } \ No newline at end of file +export { initialize, isInGraph } diff --git a/public/js/modules/main.js b/public/js/modules/main.js index 9aa2d36f..5e84b89d 100644 --- a/public/js/modules/main.js +++ b/public/js/modules/main.js @@ -101,20 +101,9 @@ Promise.all( search(hanziBox.value); switchToState(stateKeys.main); - // we're about to force a blur, which should hide the soft keyboard on android or ios - // but in some cases, the keyboard hiding triggers a resize, so you get an annoying graph re-render. - // This in very rare cases could cause cause a skipped re-layout on window size change - // but that should be rare. - document.dispatchEvent(new Event('skip-graph-resize')); hanziBox.blur(); }); - // similar to the blur logic above, the soft keyboard will show. Skip the next resize event. - // Same edge case with possible skipped 'real' resizes, but that should be very rare. - hanziBox.addEventListener('focus', function () { - document.dispatchEvent(new Event('skip-graph-resize')); - }); - // TODO(refactor): this belongs in explore rather than main? let oldState = readExploreState(); // precedence goes to the direct URL entered first, then to anything hanging around in history, then localstorage. diff --git a/public/js/modules/search.js b/public/js/modules/search.js index 597b8102..4e17ec1f 100644 --- a/public/js/modules/search.js +++ b/public/js/modules/search.js @@ -244,13 +244,20 @@ function sendDataToWorker() { }); } -let skipBlur = false; +let lastPointerDown = null; -function clearIfOutsideSearchControl(event) { - if (!searchControl.contains(event.target) && !hanziBox.contains(event.target)) { +function isInSearchUi(target) { + return target && ( + searchControl.contains(target) || + searchSuggestionsContainer.contains(target) || + hanziBox.contains(target) + ); +} + +function handleDocumentPointerDown(event) { + lastPointerDown = { target: event.target, time: Date.now() }; + if (!isInSearchUi(event.target)) { clearSuggestions(); - } else { - document.addEventListener('mousedown', clearIfOutsideSearchControl, { once: true }); } } @@ -262,17 +269,35 @@ async function initialize(term, mode) { // it sends, so allow waiting. const ensureLoaded = new Promise(ready => searchSuggestionsWorker.addEventListener("message", ready, { once: true })); hanziBox.addEventListener('input', suggestSearches); - hanziBox.addEventListener('blur', function () { - if (skipBlur) { - skipBlur = false; - document.addEventListener('mousedown', clearIfOutsideSearchControl, { once: true }); + hanziBox.addEventListener('blur', function (event) { + document.dispatchEvent(new Event('skip-graph-resize')); + setTimeout(function () { + const recentPointerTarget = lastPointerDown && Date.now() - lastPointerDown.time < 500 + ? lastPointerDown.target + : null; + if ( + isInSearchUi(event.relatedTarget) || + isInSearchUi(document.activeElement) || + isInSearchUi(recentPointerTarget) + ) { + return; + } + clearSuggestions(); + }, 0); + }); + hanziBox.addEventListener('focus', showControlsIfEligible); + document.addEventListener('pointerdown', handleDocumentPointerDown); + document.addEventListener('touchstart', function (event) { + if (window.PointerEvent) { return; } - clearSuggestions() + handleDocumentPointerDown(event); }); - hanziBox.addEventListener('focus', showControlsIfEligible); - searchControl.addEventListener('mousedown', function () { - skipBlur = true; + document.addEventListener('mousedown', function (event) { + if (window.PointerEvent) { + return; + } + handleDocumentPointerDown(event); }); if (term) { await ensureLoaded; From a30ab5af459c1f5839f802ec65e51e2b414e406b Mon Sep 17 00:00:00 2001 From: Matthew Reichhoff Date: Sun, 14 Jun 2026 22:53:23 -0400 Subject: [PATCH 2/2] Attempt to fix search suggestion flakiness --- public/js/modules/search.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/public/js/modules/search.js b/public/js/modules/search.js index 4e17ec1f..00d2ce4d 100644 --- a/public/js/modules/search.js +++ b/public/js/modules/search.js @@ -197,7 +197,7 @@ function renderSearchSuggestions(query, suggestions, tokens, container) { item.classList.add('search-suggestion'); renderSuggestion('', suggestion, item); container.appendChild(item); - item.addEventListener('mousedown', function () { + item.addEventListener('click', function () { notFoundElement.style.display = 'none'; document.dispatchEvent(new CustomEvent('graph-update', { detail: suggestion })); document.dispatchEvent(new CustomEvent('explore-update', { detail: { words: [suggestion] } })); @@ -210,7 +210,7 @@ function renderSearchSuggestions(query, suggestions, tokens, container) { item.classList.add('search-suggestion'); renderSuggestion(priorWordsForDisplay, suggestion, item); container.appendChild(item); - item.addEventListener('mousedown', function () { + item.addEventListener('click', function () { multiWordSearch(priorWordsForDisplay + suggestion, allButLastToken.concat(suggestion)); clearSuggestions(); switchToState(stateKeys.main); @@ -261,6 +261,12 @@ function handleDocumentPointerDown(event) { } } +function refreshSearchUiPointerGesture() { + if (lastPointerDown && isInSearchUi(lastPointerDown.target)) { + lastPointerDown.time = Date.now(); + } +} + async function initialize(term, mode) { searchSuggestionsWorker = new Worker('/js/modules/search-suggestions-worker.js'); sendDataToWorker(); @@ -287,12 +293,19 @@ async function initialize(term, mode) { }); hanziBox.addEventListener('focus', showControlsIfEligible); document.addEventListener('pointerdown', handleDocumentPointerDown); + document.addEventListener('pointermove', refreshSearchUiPointerGesture, { passive: true }); document.addEventListener('touchstart', function (event) { if (window.PointerEvent) { return; } handleDocumentPointerDown(event); }); + document.addEventListener('touchmove', function () { + if (window.PointerEvent) { + return; + } + refreshSearchUiPointerGesture(); + }, { passive: true }); document.addEventListener('mousedown', function (event) { if (window.PointerEvent) { return;