diff options
| author | Hsieh Chin Fan <pham@topo.tw> | 2024-09-21 10:50:03 +0800 |
|---|---|---|
| committer | Hsieh Chin Fan <pham@topo.tw> | 2024-09-21 11:01:03 +0800 |
| commit | 5c0a292a1518976d653d9a917669a2edc0c685eb (patch) | |
| tree | cc9cb40c549b1a4f7c9b45fd6f39fb51123b8e62 /src | |
| parent | 0f00c7ab65aa551fde0b2a0c2b4fcfe20a9d160e (diff) | |
refactor: apply Layout Class
* Use {name, enterHandler, leaverHandler} for layout switch
* Move "overlay" to another module
Diffstat (limited to 'src')
| -rw-r--r-- | src/OverlayLayout.mjs | 80 | ||||
| -rw-r--r-- | src/dumbymap.mjs | 142 |
2 files changed, 127 insertions, 95 deletions
diff --git a/src/OverlayLayout.mjs b/src/OverlayLayout.mjs new file mode 100644 index 0000000..c87033b --- /dev/null +++ b/src/OverlayLayout.mjs | |||
| @@ -0,0 +1,80 @@ | |||
| 1 | import PlainDraggable from 'plain-draggable' | ||
| 2 | import { onRemove } from './utils' | ||
| 3 | |||
| 4 | export class OverlayLayout { | ||
| 5 | name = "overlay" | ||
| 6 | |||
| 7 | enterHandler = (dumbymap) => { | ||
| 8 | const container = dumbymap.htmlHolder | ||
| 9 | const moveIntoDraggable = (block) => { | ||
| 10 | // Create draggable block | ||
| 11 | const draggableBlock = document.createElement('div') | ||
| 12 | draggableBlock.classList.add('draggable-block') | ||
| 13 | |||
| 14 | // Add draggable part | ||
| 15 | const draggablePart = document.createElement('div'); | ||
| 16 | draggablePart.classList.add('draggable') | ||
| 17 | draggablePart.textContent = '☰' | ||
| 18 | draggablePart.title = 'Use middle-click to remove block' | ||
| 19 | draggablePart.onmouseup = (e) => { | ||
| 20 | if (e.button === 1) { | ||
| 21 | // Hide block with middle click | ||
| 22 | draggableBlock.style.display = "none"; | ||
| 23 | } | ||
| 24 | } | ||
| 25 | |||
| 26 | // Set elements | ||
| 27 | draggableBlock.appendChild(draggablePart) | ||
| 28 | draggableBlock.appendChild(block) | ||
| 29 | container.appendChild(draggableBlock) | ||
| 30 | |||
| 31 | // Add draggable instance | ||
| 32 | const draggableInstance = new PlainDraggable(draggableBlock, { | ||
| 33 | handle: draggablePart, | ||
| 34 | snap: { x: { step: 20 }, y: { step: 20 } }, | ||
| 35 | }) | ||
| 36 | |||
| 37 | // Reposition draggable instance when resized | ||
| 38 | new ResizeObserver(() => { | ||
| 39 | try { | ||
| 40 | draggableInstance.position(); | ||
| 41 | } catch (_) { | ||
| 42 | null | ||
| 43 | } | ||
| 44 | }).observe(draggableBlock); | ||
| 45 | |||
| 46 | // Callback for remove | ||
| 47 | onRemove(draggableBlock, () => { | ||
| 48 | draggableInstance.remove() | ||
| 49 | }) | ||
| 50 | |||
| 51 | return draggableInstance | ||
| 52 | } | ||
| 53 | |||
| 54 | // Create draggable blocks and set each position by previous one | ||
| 55 | let [x, y] = [0, 0] | ||
| 56 | dumbymap.blocks.map(moveIntoDraggable) | ||
| 57 | .forEach(draggable => { | ||
| 58 | draggable.left = x | ||
| 59 | draggable.top = y | ||
| 60 | const rect = draggable.element.getBoundingClientRect() | ||
| 61 | x += rect.width + 30 | ||
| 62 | if (x > window.innerWidth) { | ||
| 63 | y += 200 | ||
| 64 | x = x % window.innerWidth | ||
| 65 | } | ||
| 66 | }) | ||
| 67 | } | ||
| 68 | leaveHandler = (dumbymap) => { | ||
| 69 | const container = dumbymap.htmlHolder | ||
| 70 | const resumeFromDraggable = (block) => { | ||
| 71 | const draggableContainer = block.closest('.draggable-block') | ||
| 72 | if (!draggableContainer) return | ||
| 73 | container.appendChild(block) | ||
| 74 | block.removeAttribute('style') | ||
| 75 | draggableContainer.remove() | ||
| 76 | } | ||
| 77 | dumbymap.blocks.forEach(resumeFromDraggable) | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs index d7a29e9..d2492db 100644 --- a/src/dumbymap.mjs +++ b/src/dumbymap.mjs | |||
| @@ -4,13 +4,28 @@ import MarkdownItFootnote from 'markdown-it-footnote' | |||
| 4 | import MarkdownItFrontMatter from 'markdown-it-front-matter' | 4 | import MarkdownItFrontMatter from 'markdown-it-front-matter' |
| 5 | import MarkdownItTocDoneRight from 'markdown-it-toc-done-right' | 5 | import MarkdownItTocDoneRight from 'markdown-it-toc-done-right' |
| 6 | import LeaderLine from 'leader-line' | 6 | import LeaderLine from 'leader-line' |
| 7 | import PlainDraggable from 'plain-draggable' | ||
| 8 | import { renderWith, parseConfigsFromYaml } from 'mapclay' | 7 | import { renderWith, parseConfigsFromYaml } from 'mapclay' |
| 9 | import { onRemove, animateRectTransition, throttle } from './utils' | 8 | import { onRemove, animateRectTransition, throttle } from './utils' |
| 10 | 9 | ||
| 11 | // FUNCTION: Get DocLinks from special anchor element {{{ | 10 | // FUNCTION: Get DocLinks from special anchor element {{{ |
| 11 | import { OverlayLayout } from './OverlayLayout' | ||
| 12 | 12 | ||
| 13 | const docLinkSelector = 'a[href^="#"][title^="=>"]' | 13 | const docLinkSelector = 'a[href^="#"][title^="=>"]' |
| 14 | |||
| 15 | class Layout { | ||
| 16 | constructor({ name, enterHandler = null, leaveHandler = null }) { | ||
| 17 | this.name = name | ||
| 18 | this.enterHandler = enterHandler | ||
| 19 | this.leaveHandler = leaveHandler | ||
| 20 | } | ||
| 21 | valueOf = () => this.name | ||
| 22 | } | ||
| 23 | |||
| 24 | const layouts = [ | ||
| 25 | new Layout({ name: "none" }), | ||
| 26 | new Layout({ name: "side" }), | ||
| 27 | new OverlayLayout(), | ||
| 28 | ] | ||
| 14 | export const createDocLinks = (container) => Array.from(container.querySelectorAll(docLinkSelector)) | 29 | export const createDocLinks = (container) => Array.from(container.querySelectorAll(docLinkSelector)) |
| 15 | .map(link => { | 30 | .map(link => { |
| 16 | link.classList.add('with-leader-line', 'doclink') | 31 | link.classList.add('with-leader-line', 'doclink') |
| @@ -122,9 +137,18 @@ export const markdown2HTML = (container, mdContent) => { | |||
| 122 | } | 137 | } |
| 123 | // FIXME Don't use hard-coded CSS selector | 138 | // FIXME Don't use hard-coded CSS selector |
| 124 | export const generateMaps = async (container, callback) => { | 139 | export const generateMaps = async (container, callback) => { |
| 125 | container.classList.add('DumbyMap') | ||
| 126 | const htmlHolder = container.querySelector('.SemanticHtml') ?? container | 140 | const htmlHolder = container.querySelector('.SemanticHtml') ?? container |
| 141 | const showcase = document.createElement('div') | ||
| 142 | container.appendChild(showcase) | ||
| 143 | showcase.classList.add('Showcase') | ||
| 144 | |||
| 145 | const dumbymap = { | ||
| 146 | blocks: Array.from(htmlHolder.querySelectorAll('.dumby-block')), | ||
| 147 | htmlHolder: htmlHolder, | ||
| 148 | showcase: showcase, | ||
| 149 | } | ||
| 127 | 150 | ||
| 151 | container.classList.add('DumbyMap') | ||
| 128 | // LeaderLine {{{ | 152 | // LeaderLine {{{ |
| 129 | 153 | ||
| 130 | // Get anchors with "geo:" scheme | 154 | // Get anchors with "geo:" scheme |
| @@ -205,66 +229,11 @@ export const generateMaps = async (container, callback) => { | |||
| 205 | // Draggable Blocks {{{ | 229 | // Draggable Blocks {{{ |
| 206 | // Add draggable part for blocks | 230 | // Add draggable part for blocks |
| 207 | 231 | ||
| 208 | const dumbyBlocks = Array.from(htmlHolder.querySelectorAll('.dumby-block')) | ||
| 209 | const moveIntoDraggable = (block) => { | ||
| 210 | // Create draggable block | ||
| 211 | const draggableBlock = document.createElement('div') | ||
| 212 | draggableBlock.classList.add('draggable-block') | ||
| 213 | |||
| 214 | // Add draggable part | ||
| 215 | const draggablePart = document.createElement('div'); | ||
| 216 | draggablePart.classList.add('draggable') | ||
| 217 | draggablePart.textContent = '☰' | ||
| 218 | draggablePart.title = 'Use middle-click to remove block' | ||
| 219 | draggablePart.onmouseup = (e) => { | ||
| 220 | if (e.button === 1) { | ||
| 221 | // Hide block with middle click | ||
| 222 | draggableBlock.style.display = "none"; | ||
| 223 | } | ||
| 224 | } | ||
| 225 | |||
| 226 | // Set elements | ||
| 227 | draggableBlock.appendChild(draggablePart) | ||
| 228 | draggableBlock.appendChild(block) | ||
| 229 | htmlHolder.appendChild(draggableBlock) | ||
| 230 | |||
| 231 | // Add draggable instance | ||
| 232 | const draggableInstance = new PlainDraggable(draggableBlock, { | ||
| 233 | handle: draggablePart, | ||
| 234 | snap: { x: { step: 20 }, y: { step: 20 } }, | ||
| 235 | }) | ||
| 236 | |||
| 237 | // Reposition draggable instance when resized | ||
| 238 | new ResizeObserver(() => { | ||
| 239 | try { | ||
| 240 | draggableInstance.position(); | ||
| 241 | } catch (_) { | ||
| 242 | null | ||
| 243 | } | ||
| 244 | }).observe(draggableBlock); | ||
| 245 | |||
| 246 | // Callback for remove | ||
| 247 | onRemove(draggableBlock, () => { | ||
| 248 | draggableInstance.remove() | ||
| 249 | }) | ||
| 250 | 232 | ||
| 251 | return draggableInstance | ||
| 252 | } | ||
| 253 | |||
| 254 | const resumeFromDraggable = (block) => { | ||
| 255 | const draggableContainer = block.closest('.draggable-block') | ||
| 256 | if (!draggableContainer) return | ||
| 257 | htmlHolder.appendChild(block) | ||
| 258 | block.removeAttribute('style') | ||
| 259 | draggableContainer.remove() | ||
| 260 | } | ||
| 261 | // }}} | 233 | // }}} |
| 262 | // CSS observer {{{ | 234 | // CSS observer {{{ |
| 263 | // Focus Map {{{ | 235 | // Focus Map {{{ |
| 264 | // Set focusArea | 236 | // Set focusArea |
| 265 | const showcase = document.createElement('div') | ||
| 266 | container.appendChild(showcase) | ||
| 267 | showcase.classList.add('Showcase') | ||
| 268 | 237 | ||
| 269 | const mapFocusObserver = () => new MutationObserver((mutations) => { | 238 | const mapFocusObserver = () => new MutationObserver((mutations) => { |
| 270 | const mutation = mutations.at(-1) | 239 | const mutation = mutations.at(-1) |
| @@ -314,20 +283,20 @@ export const generateMaps = async (container, callback) => { | |||
| 314 | // Layout {{{ | 283 | // Layout {{{ |
| 315 | 284 | ||
| 316 | // press key to switch layout | 285 | // press key to switch layout |
| 317 | const layouts = ['none', 'side', 'overlay'] | 286 | const defaultLayout = layouts[0] |
| 318 | container.setAttribute("data-layout", layouts[0]) | 287 | container.setAttribute("data-layout", defaultLayout.name) |
| 319 | 288 | ||
| 320 | const switchToNextLayout = throttle(() => { | 289 | const switchToNextLayout = throttle(() => { |
| 321 | let currentLayout = container.getAttribute('data-layout') | 290 | const currentLayoutName = container.getAttribute('data-layout') |
| 322 | currentLayout = currentLayout ? currentLayout : 'none' | 291 | const currentIndex = layouts.map(l => l.name).indexOf(currentLayoutName) |
| 323 | const nextIndex = (layouts.indexOf(currentLayout) + 1) % layouts.length | 292 | const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % layouts.length |
| 324 | const nextLayout = layouts[nextIndex] | 293 | const nextLayout = layouts[nextIndex] |
| 325 | 294 | container.setAttribute("data-layout", nextLayout.name) | |
| 326 | container.setAttribute("data-layout", nextLayout) | ||
| 327 | }, 300) | 295 | }, 300) |
| 328 | 296 | ||
| 329 | // TODO Use UI to switch layouts | 297 | // TODO Use UI to switch layouts |
| 330 | // Focus to next map with throttle | 298 | // Focus to next map with throttle |
| 299 | |||
| 331 | const focusNextMap = throttle((reverse = false) => { | 300 | const focusNextMap = throttle((reverse = false) => { |
| 332 | // Decide how many candidates could be focused | 301 | // Decide how many candidates could be focused |
| 333 | const selector = '.map-container, [data-placeholder]' | 302 | const selector = '.map-container, [data-placeholder]' |
| @@ -371,42 +340,25 @@ export const generateMaps = async (container, callback) => { | |||
| 371 | // observe layout change | 340 | // observe layout change |
| 372 | const layoutObserver = new MutationObserver((mutations) => { | 341 | const layoutObserver = new MutationObserver((mutations) => { |
| 373 | const mutation = mutations.at(-1) | 342 | const mutation = mutations.at(-1) |
| 343 | const oldLayout = mutation.oldValue | ||
| 374 | const layout = container.getAttribute(mutation.attributeName) | 344 | const layout = container.getAttribute(mutation.attributeName) |
| 375 | 345 | ||
| 376 | // Trigger Mutation Observer | 346 | if (oldLayout) { |
| 347 | layouts.find(l => l.name === oldLayout) | ||
| 348 | ?.leaveHandler | ||
| 349 | ?.call(this, dumbymap) | ||
| 350 | } | ||
| 351 | if (layout) { | ||
| 352 | layouts.find(l => l.name === layout) | ||
| 353 | ?.enterHandler | ||
| 354 | ?.call(this, dumbymap) | ||
| 355 | } | ||
| 356 | |||
| 357 | // Since layout change may show/hide showcase, the current focused map should do something | ||
| 358 | // Reset attribute triggers MutationObserver which is observing it | ||
| 377 | const focusMap = container.querySelector('.map-container[data-focus=true]') | 359 | const focusMap = container.querySelector('.map-container[data-focus=true]') |
| 378 | ?? container.querySelector('.map-container') | 360 | ?? container.querySelector('.map-container') |
| 379 | focusMap?.setAttribute('data-focus', 'true') | 361 | focusMap?.setAttribute('data-focus', 'true') |
| 380 | |||
| 381 | // Check empty block with map-container in showcase | ||
| 382 | dumbyBlocks.forEach(b => { | ||
| 383 | const contentChildren = Array.from(b.querySelectorAll(':scope > :not(.draggable)')) ?? [] | ||
| 384 | if (contentChildren.length === 1 | ||
| 385 | && elementsWithMapConfig.includes(contentChildren[0]) | ||
| 386 | && !contentChildren[0].querySelector('.map-container') | ||
| 387 | ) { | ||
| 388 | b.style.display = "none" | ||
| 389 | } else { | ||
| 390 | b.style.display = "block" | ||
| 391 | } | ||
| 392 | }) | ||
| 393 | |||
| 394 | if (layout === 'overlay') { | ||
| 395 | let [x, y] = [0, 0] | ||
| 396 | dumbyBlocks.map(moveIntoDraggable) | ||
| 397 | .forEach(draggable => { | ||
| 398 | draggable.left = x | ||
| 399 | draggable.top = y | ||
| 400 | const rect = draggable.element.getBoundingClientRect() | ||
| 401 | x += rect.width + 30 | ||
| 402 | if (x > window.innerWidth) { | ||
| 403 | y += 200 | ||
| 404 | x = x % window.innerWidth | ||
| 405 | } | ||
| 406 | }) | ||
| 407 | } else { | ||
| 408 | dumbyBlocks.forEach(resumeFromDraggable) | ||
| 409 | } | ||
| 410 | }); | 362 | }); |
| 411 | layoutObserver.observe(container, { | 363 | layoutObserver.observe(container, { |
| 412 | attributes: true, | 364 | attributes: true, |