diff options
| author | Hsieh Chin Fan <pham@topo.tw> | 2024-09-20 23:51:40 +0800 |
|---|---|---|
| committer | Hsieh Chin Fan <pham@topo.tw> | 2024-09-21 11:01:03 +0800 |
| commit | 0f00c7ab65aa551fde0b2a0c2b4fcfe20a9d160e (patch) | |
| tree | 3f7cea54164019a3bc8820ea67e722313e06836f /src | |
| parent | f48923d9421af46f0ca7ad5127d6f11f103df951 (diff) | |
feat: throttle for layout and map focus
* add throttle for layout switch
* apply throttle for 'Tab' key (focus next map)
* remove throttle from DOMRect animation
Diffstat (limited to 'src')
| -rw-r--r-- | src/dumbymap.mjs | 70 |
1 files changed, 40 insertions, 30 deletions
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs index d18fab8..d7a29e9 100644 --- a/src/dumbymap.mjs +++ b/src/dumbymap.mjs | |||
| @@ -266,9 +266,6 @@ export const generateMaps = async (container, callback) => { | |||
| 266 | container.appendChild(showcase) | 266 | container.appendChild(showcase) |
| 267 | showcase.classList.add('Showcase') | 267 | showcase.classList.add('Showcase') |
| 268 | 268 | ||
| 269 | const toShowcaseWithThrottle = throttle(animateRectTransition, 300) | ||
| 270 | const fromShowCaseWithThrottle = throttle(animateRectTransition, 300) | ||
| 271 | |||
| 272 | const mapFocusObserver = () => new MutationObserver((mutations) => { | 269 | const mapFocusObserver = () => new MutationObserver((mutations) => { |
| 273 | const mutation = mutations.at(-1) | 270 | const mutation = mutations.at(-1) |
| 274 | const target = mutation.target | 271 | const target = mutation.target |
| @@ -289,16 +286,14 @@ export const generateMaps = async (container, callback) => { | |||
| 289 | showcase.appendChild(target) | 286 | showcase.appendChild(target) |
| 290 | 287 | ||
| 291 | // Resume rect from Semantic HTML to Showcase, with animation | 288 | // Resume rect from Semantic HTML to Showcase, with animation |
| 292 | toShowcaseWithThrottle(target, placeholder.getBoundingClientRect(), { | 289 | animateRectTransition(target, placeholder.getBoundingClientRect(), { |
| 293 | duration: 300, | 290 | duration: 300, |
| 294 | resume: true | 291 | resume: true |
| 295 | }) | 292 | }) |
| 296 | } else if (showcase.contains(target)) { | 293 | } else if (showcase.contains(target)) { |
| 297 | const placeholder = htmlHolder.querySelector(`[data-placeholder="${target.id}"]`) | 294 | const placeholder = htmlHolder.querySelector(`[data-placeholder="${target.id}"]`) |
| 298 | if (!placeholder) throw Error(`Cannot fine placeholder for map "${target.id}"`) | 295 | if (!placeholder) throw Error(`Cannot fine placeholder for map "${target.id}"`) |
| 299 | const animation = fromShowCaseWithThrottle(target, placeholder.getBoundingClientRect(), { | 296 | const animation = animateRectTransition(target, placeholder.getBoundingClientRect(), { duration: 300 }) |
| 300 | duration: 300 | ||
| 301 | }) | ||
| 302 | 297 | ||
| 303 | const afterAnimation = () => { | 298 | const afterAnimation = () => { |
| 304 | placeholder.parentElement.replaceChild(target, placeholder) | 299 | placeholder.parentElement.replaceChild(target, placeholder) |
| @@ -322,7 +317,40 @@ export const generateMaps = async (container, callback) => { | |||
| 322 | const layouts = ['none', 'side', 'overlay'] | 317 | const layouts = ['none', 'side', 'overlay'] |
| 323 | container.setAttribute("data-layout", layouts[0]) | 318 | container.setAttribute("data-layout", layouts[0]) |
| 324 | 319 | ||
| 325 | // FIXME Use UI to switch layouts | 320 | const switchToNextLayout = throttle(() => { |
| 321 | let currentLayout = container.getAttribute('data-layout') | ||
| 322 | currentLayout = currentLayout ? currentLayout : 'none' | ||
| 323 | const nextIndex = (layouts.indexOf(currentLayout) + 1) % layouts.length | ||
| 324 | const nextLayout = layouts[nextIndex] | ||
| 325 | |||
| 326 | container.setAttribute("data-layout", nextLayout) | ||
| 327 | }, 300) | ||
| 328 | |||
| 329 | // TODO Use UI to switch layouts | ||
| 330 | // Focus to next map with throttle | ||
| 331 | const focusNextMap = throttle((reverse = false) => { | ||
| 332 | // Decide how many candidates could be focused | ||
| 333 | const selector = '.map-container, [data-placeholder]' | ||
| 334 | const candidates = Array.from(htmlHolder.querySelectorAll(selector)) | ||
| 335 | if (candidates.length <= 1) return | ||
| 336 | |||
| 337 | // Get current focused element | ||
| 338 | const currentFocus = htmlHolder.querySelector('.map-container[data-focus=true]') | ||
| 339 | ?? htmlHolder.querySelector('[data-placeholder]') | ||
| 340 | |||
| 341 | // Remove class name of focus for ALL candidates | ||
| 342 | // This may trigger animation | ||
| 343 | Array.from(container.querySelectorAll('.map-container')) | ||
| 344 | .forEach(ele => ele.removeAttribute('data-focus')); | ||
| 345 | |||
| 346 | // Focus next focus element | ||
| 347 | const nextIndex = currentFocus | ||
| 348 | ? (candidates.indexOf(currentFocus) + (reverse ? -1 : 1)) % candidates.length | ||
| 349 | : 0 | ||
| 350 | const nextFocus = candidates.at(nextIndex) | ||
| 351 | nextFocus.setAttribute('data-focus', "true") | ||
| 352 | }, 300) | ||
| 353 | |||
| 326 | const originalKeyDown = document.onkeydown | 354 | const originalKeyDown = document.onkeydown |
| 327 | document.onkeydown = (e) => { | 355 | document.onkeydown = (e) => { |
| 328 | const event = originalKeyDown(e) | 356 | const event = originalKeyDown(e) |
| @@ -330,32 +358,13 @@ export const generateMaps = async (container, callback) => { | |||
| 330 | 358 | ||
| 331 | if (event.key === 'x' && container.querySelector('.map-container')) { | 359 | if (event.key === 'x' && container.querySelector('.map-container')) { |
| 332 | e.preventDefault() | 360 | e.preventDefault() |
| 333 | let currentLayout = container.getAttribute('data-layout') | 361 | switchToNextLayout() |
| 334 | currentLayout = currentLayout ? currentLayout : 'none' | ||
| 335 | const nextIndex = (layouts.indexOf(currentLayout) + 1) % layouts.length | ||
| 336 | const nextLayout = layouts[nextIndex] | ||
| 337 | |||
| 338 | container.setAttribute("data-layout", nextLayout) | ||
| 339 | } | 362 | } |
| 340 | 363 | ||
| 341 | // Use Tab to change focus map | 364 | // Use Tab to change focus map |
| 342 | if (event.key === 'Tab') { | 365 | if (event.key === 'Tab') { |
| 343 | e.preventDefault() | 366 | e.preventDefault() |
| 344 | 367 | focusNextMap(event.shiftkey) | |
| 345 | const selector = '.map-container, [data-placeholder]' | ||
| 346 | const candidates = Array.from(htmlHolder.querySelectorAll(selector)) | ||
| 347 | if (candidates.length <= 1) return | ||
| 348 | |||
| 349 | const currentFocus = htmlHolder.querySelector('.map-container[data-focus=true]') | ||
| 350 | ?? htmlHolder.querySelector('[data-placeholder]') | ||
| 351 | Array.from(container.querySelectorAll('.map-container')).forEach(e => | ||
| 352 | e.removeAttribute('data-focus') | ||
| 353 | ); | ||
| 354 | const index = currentFocus | ||
| 355 | ? (candidates.indexOf(currentFocus) + (event.shiftKey ? -1 : 1)) % candidates.length | ||
| 356 | : 0 | ||
| 357 | const nextFocus = candidates.at(index) | ||
| 358 | nextFocus.setAttribute('data-focus', "true") | ||
| 359 | } | 368 | } |
| 360 | } | 369 | } |
| 361 | 370 | ||
| @@ -402,7 +411,8 @@ export const generateMaps = async (container, callback) => { | |||
| 402 | layoutObserver.observe(container, { | 411 | layoutObserver.observe(container, { |
| 403 | attributes: true, | 412 | attributes: true, |
| 404 | attributeFilter: ["data-layout"], | 413 | attributeFilter: ["data-layout"], |
| 405 | attributeOldValue: true | 414 | attributeOldValue: true, |
| 415 | characterDataOldValue: true | ||
| 406 | }); | 416 | }); |
| 407 | 417 | ||
| 408 | onRemove(htmlHolder, () => layoutObserver.disconnect()) | 418 | onRemove(htmlHolder, () => layoutObserver.disconnect()) |