diff options
| author | Hsieh Chin Fan <pham@topo.tw> | 2024-09-23 12:55:49 +0800 |
|---|---|---|
| committer | Hsieh Chin Fan <pham@topo.tw> | 2024-09-23 13:34:43 +0800 |
| commit | 3bd8fd3da8e37fee57ff47eb43d3f97bfffa7c40 (patch) | |
| tree | e58ac34f1e7195bb403299bd28e5f3af91550532 | |
| parent | 5eefd9c2e4f948cddecf313231afdc62aa1531cd (diff) | |
feat: remove keydown events from dumbymap
* events now handed by editor
* this fix document.onkeydown reset when generatedMaps() called more
than one time
* apply "bind" to remove methods about user interaction from generateMaps()
* refactor focusNextmap(), using array to store rendered maps
BREAKING CHANGE:
generateMaps() now return dumbymap Object, contains key elements and
methods
| -rw-r--r-- | src/dumbymap.mjs | 107 | ||||
| -rw-r--r-- | src/editor.mjs | 17 |
2 files changed, 61 insertions, 63 deletions
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs index 42a507f..d642504 100644 --- a/src/dumbymap.mjs +++ b/src/dumbymap.mjs | |||
| @@ -137,19 +137,58 @@ export const markdown2HTML = (container, mdContent) => { | |||
| 137 | //}}} | 137 | //}}} |
| 138 | } | 138 | } |
| 139 | // FIXME Don't use hard-coded CSS selector | 139 | // FIXME Don't use hard-coded CSS selector |
| 140 | export const generateMaps = async (container, callback) => { | 140 | // TODO Use UI to switch layouts |
| 141 | function focusNextMap(reverse = false) { | ||
| 142 | const mapNum = this.renderedMaps.length | ||
| 143 | if (mapNum === 0) return | ||
| 144 | // Get current focused map element | ||
| 145 | const currentFocus = this.container.querySelector('.map-container[data-focus]') | ||
| 146 | |||
| 147 | // Remove class name of focus for ALL candidates | ||
| 148 | // This may trigger animation | ||
| 149 | Array.from(this.container.querySelectorAll('.map-container')) | ||
| 150 | .forEach(ele => ele.removeAttribute('data-focus')) | ||
| 151 | |||
| 152 | // Get next existing map element | ||
| 153 | const padding = reverse ? -1 : 1 | ||
| 154 | let nextIndex = currentFocus ? this.renderedMaps.indexOf(currentFocus) + padding : 0 | ||
| 155 | nextIndex = (nextIndex + mapNum) % mapNum | ||
| 156 | const nextFocus = this.renderedMaps[nextIndex] | ||
| 157 | nextFocus.setAttribute('data-focus', "true") | ||
| 158 | |||
| 159 | return nextFocus | ||
| 160 | } | ||
| 161 | function focusDelay() { | ||
| 162 | return window.getComputedStyle(this.showcase).display === 'none' ? 50 : 300 | ||
| 163 | } | ||
| 164 | |||
| 165 | function switchToNextLayout() { | ||
| 166 | const currentLayoutName = this.container.getAttribute('data-layout') | ||
| 167 | const currentIndex = layouts.map(l => l.name).indexOf(currentLayoutName) | ||
| 168 | const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % layouts.length | ||
| 169 | const nextLayout = layouts[nextIndex] | ||
| 170 | this.container.setAttribute("data-layout", nextLayout.name) | ||
| 171 | } | ||
| 172 | |||
| 173 | export const generateMaps = (container, callback) => { | ||
| 141 | container.classList.add('Dumby') | 174 | container.classList.add('Dumby') |
| 142 | const htmlHolder = container.querySelector('.SemanticHtml') ?? container | 175 | const htmlHolder = container.querySelector('.SemanticHtml') ?? container |
| 143 | const blocks = Array.from(htmlHolder.querySelectorAll('.dumby-block')) | 176 | const blocks = Array.from(htmlHolder.querySelectorAll('.dumby-block')) |
| 144 | const showcase = document.createElement('div') | 177 | const showcase = document.createElement('div') |
| 145 | container.appendChild(showcase) | 178 | container.appendChild(showcase) |
| 146 | showcase.classList.add('Showcase') | 179 | showcase.classList.add('Showcase') |
| 180 | const renderedMaps = [] | ||
| 147 | 181 | ||
| 148 | const dumbymap = { | 182 | const dumbymap = { |
| 149 | container, | 183 | container, |
| 150 | htmlHolder, | 184 | htmlHolder, |
| 151 | showcase, | 185 | showcase, |
| 152 | blocks, | 186 | blocks, |
| 187 | renderedMaps, | ||
| 188 | } | ||
| 189 | dumbymap.utils = { | ||
| 190 | focusNextMap: throttle(focusNextMap.bind(dumbymap), focusDelay.bind(dumbymap)), | ||
| 191 | switchToNextLayout: throttle(switchToNextLayout.bind(dumbymap), 300), | ||
| 153 | } | 192 | } |
| 154 | 193 | ||
| 155 | // LeaderLine {{{ | 194 | // LeaderLine {{{ |
| @@ -249,7 +288,8 @@ export const generateMaps = async (container, callback) => { | |||
| 249 | 288 | ||
| 250 | // Placeholder for map in Showcase, it should has the same DOMRect | 289 | // Placeholder for map in Showcase, it should has the same DOMRect |
| 251 | const placeholder = target.cloneNode(true) | 290 | const placeholder = target.cloneNode(true) |
| 252 | placeholder.classList.remove('map-container') | 291 | placeholder.removeAttribute('id') |
| 292 | placeholder.classList.remove('map-container', 'data-focus') | ||
| 253 | target.parentElement.replaceChild(placeholder, target) | 293 | target.parentElement.replaceChild(placeholder, target) |
| 254 | 294 | ||
| 255 | // HACK Trigger CSS transition, if placeholde is the olny chil element in block, | 295 | // HACK Trigger CSS transition, if placeholde is the olny chil element in block, |
| @@ -292,58 +332,6 @@ export const generateMaps = async (container, callback) => { | |||
| 292 | const defaultLayout = layouts[0] | 332 | const defaultLayout = layouts[0] |
| 293 | container.setAttribute("data-layout", defaultLayout.name) | 333 | container.setAttribute("data-layout", defaultLayout.name) |
| 294 | 334 | ||
| 295 | const switchToNextLayout = throttle(() => { | ||
| 296 | const currentLayoutName = container.getAttribute('data-layout') | ||
| 297 | const currentIndex = layouts.map(l => l.name).indexOf(currentLayoutName) | ||
| 298 | const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % layouts.length | ||
| 299 | const nextLayout = layouts[nextIndex] | ||
| 300 | container.setAttribute("data-layout", nextLayout.name) | ||
| 301 | }, 300) | ||
| 302 | |||
| 303 | // TODO Use UI to switch layouts | ||
| 304 | const focusNextMap = (reverse = false) => { | ||
| 305 | // Decide how many candidates could be focused | ||
| 306 | const selector = '.map-container, [data-placeholder]' | ||
| 307 | const candidates = Array.from(htmlHolder.querySelectorAll(selector)) | ||
| 308 | if (candidates.length <= 1) return | ||
| 309 | |||
| 310 | // Get current focused element | ||
| 311 | const currentFocus = htmlHolder.querySelector('.map-container[data-focus=true]') | ||
| 312 | ?? htmlHolder.querySelector('[data-placeholder]') | ||
| 313 | |||
| 314 | // Remove class name of focus for ALL candidates | ||
| 315 | // This may trigger animation | ||
| 316 | Array.from(container.querySelectorAll('.map-container')) | ||
| 317 | .forEach(ele => ele.removeAttribute('data-focus')); | ||
| 318 | |||
| 319 | // Focus next focus element | ||
| 320 | const nextIndex = currentFocus | ||
| 321 | ? (candidates.indexOf(currentFocus) + (reverse ? -1 : 1)) % candidates.length | ||
| 322 | : 0 | ||
| 323 | const nextFocus = candidates.at(nextIndex) | ||
| 324 | nextFocus.setAttribute('data-focus', "true") | ||
| 325 | } | ||
| 326 | const focusDelay = () => getComputedStyle(showcase).display === 'none' ? 50 : 300 | ||
| 327 | const focusNextMapWithThrottle = throttle(focusNextMap, focusDelay) | ||
| 328 | |||
| 329 | const originalKeyDown = document.onkeydown | ||
| 330 | document.onkeydown = (e) => { | ||
| 331 | const event = originalKeyDown(e) | ||
| 332 | if (!event) return | ||
| 333 | |||
| 334 | // Switch to next layout | ||
| 335 | if (event.key === 'x' && container.querySelector('.map-container')) { | ||
| 336 | e.preventDefault() | ||
| 337 | switchToNextLayout() | ||
| 338 | } | ||
| 339 | |||
| 340 | // Use Tab to change focus map | ||
| 341 | if (event.key === 'Tab') { | ||
| 342 | e.preventDefault() | ||
| 343 | focusNextMapWithThrottle(event.shiftKey) | ||
| 344 | } | ||
| 345 | } | ||
| 346 | |||
| 347 | // observe layout change | 335 | // observe layout change |
| 348 | const layoutObserver = new MutationObserver((mutations) => { | 336 | const layoutObserver = new MutationObserver((mutations) => { |
| 349 | const mutation = mutations.at(-1) | 337 | const mutation = mutations.at(-1) |
| @@ -358,7 +346,9 @@ export const generateMaps = async (container, callback) => { | |||
| 358 | } | 346 | } |
| 359 | 347 | ||
| 360 | Object.values(dumbymap) | 348 | Object.values(dumbymap) |
| 361 | .flat() | 349 | .filter(e => e instanceof HTMLElement) |
| 350 | .forEach(e => e.removeAttribute('style')) | ||
| 351 | dumbymap.blocks | ||
| 362 | .forEach(e => e.removeAttribute('style')) | 352 | .forEach(e => e.removeAttribute('style')) |
| 363 | 353 | ||
| 364 | if (newLayout) { | 354 | if (newLayout) { |
| @@ -386,6 +376,8 @@ export const generateMaps = async (container, callback) => { | |||
| 386 | // Render Maps {{{ | 376 | // Render Maps {{{ |
| 387 | 377 | ||
| 388 | const afterEachMapLoaded = (mapContainer) => { | 378 | const afterEachMapLoaded = (mapContainer) => { |
| 379 | renderedMaps.push(mapContainer) | ||
| 380 | renderedMaps.sort((a, b) => mapIdList.indexOf(a.id) - mapIdList.indexOf(b.id)) | ||
| 389 | mapContainer.setAttribute('tabindex', "-1") | 381 | mapContainer.setAttribute('tabindex', "-1") |
| 390 | 382 | ||
| 391 | const observer = mapFocusObserver() | 383 | const observer = mapFocusObserver() |
| @@ -454,12 +446,11 @@ export const generateMaps = async (container, callback) => { | |||
| 454 | }) | 446 | }) |
| 455 | }) | 447 | }) |
| 456 | 448 | ||
| 457 | const renderAllTargets = Promise.all(renderTargets) | 449 | Promise.all(renderTargets) |
| 458 | .then(() => { | 450 | .then(() => { |
| 459 | console.info('Finish Rendering') | 451 | console.info('Finish Rendering') |
| 460 | 452 | ||
| 461 | const maps = htmlHolder.querySelectorAll('.map-container') ?? [] | 453 | const maps = htmlHolder.querySelectorAll('.map-container') ?? [] |
| 462 | focusNextMap() | ||
| 463 | Array.from(maps) | 454 | Array.from(maps) |
| 464 | .forEach(ele => { | 455 | .forEach(ele => { |
| 465 | callback(ele) | 456 | callback(ele) |
| @@ -479,5 +470,5 @@ export const generateMaps = async (container, callback) => { | |||
| 479 | }) | 470 | }) |
| 480 | 471 | ||
| 481 | //}}} | 472 | //}}} |
| 482 | return renderAllTargets | 473 | return dumbymap |
| 483 | } | 474 | } |
diff --git a/src/editor.mjs b/src/editor.mjs index 897a0d1..4d8dbe8 100644 --- a/src/editor.mjs +++ b/src/editor.mjs | |||
| @@ -8,6 +8,7 @@ import { createDocLinks } from './dumbymap.mjs' | |||
| 8 | 8 | ||
| 9 | const HtmlContainer = document.querySelector(".DumbyMap") | 9 | const HtmlContainer = document.querySelector(".DumbyMap") |
| 10 | const textArea = document.querySelector(".editor textarea") | 10 | const textArea = document.querySelector(".editor textarea") |
| 11 | let dumbymap | ||
| 11 | 12 | ||
| 12 | const toggleEditing = () => { | 13 | const toggleEditing = () => { |
| 13 | if (document.body.getAttribute("data-mode") === "editing") { | 14 | if (document.body.getAttribute("data-mode") === "editing") { |
| @@ -152,7 +153,7 @@ const debounceForMap = (() => { | |||
| 152 | return function(...args) { | 153 | return function(...args) { |
| 153 | clearTimeout(timer); | 154 | clearTimeout(timer); |
| 154 | timer = setTimeout(() => { | 155 | timer = setTimeout(() => { |
| 155 | generateMaps.apply(this, args) | 156 | dumbymap = generateMaps.apply(this, args) |
| 156 | }, 1000); | 157 | }, 1000); |
| 157 | } | 158 | } |
| 158 | })() | 159 | })() |
| @@ -548,10 +549,16 @@ document.onkeydown = (e) => { | |||
| 548 | e.preventDefault() | 549 | e.preventDefault() |
| 549 | return null | 550 | return null |
| 550 | } | 551 | } |
| 551 | if (cm.hasFocus()) { | 552 | if (!cm.hasFocus()) { |
| 552 | return null | 553 | e.preventDefault() |
| 553 | } else { | 554 | if (!dumbymap) return |
| 554 | return e | 555 | |
| 556 | if (e.key === 'Tab') { | ||
| 557 | dumbymap.utils.focusNextMap(e.shiftKey) | ||
| 558 | } | ||
| 559 | if (e.key === 'x') { | ||
| 560 | dumbymap.utils.switchToNextLayout() | ||
| 561 | } | ||
| 555 | } | 562 | } |
| 556 | } | 563 | } |
| 557 | 564 | ||