diff options
-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, |