diff options
Diffstat (limited to 'src/dumbymap.mjs')
-rw-r--r-- | src/dumbymap.mjs | 107 |
1 files changed, 49 insertions, 58 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 | } |