aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/OverlayLayout.mjs80
-rw-r--r--src/dumbymap.mjs142
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 @@
1import PlainDraggable from 'plain-draggable'
2import { onRemove } from './utils'
3
4export 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'
4import MarkdownItFrontMatter from 'markdown-it-front-matter' 4import MarkdownItFrontMatter from 'markdown-it-front-matter'
5import MarkdownItTocDoneRight from 'markdown-it-toc-done-right' 5import MarkdownItTocDoneRight from 'markdown-it-toc-done-right'
6import LeaderLine from 'leader-line' 6import LeaderLine from 'leader-line'
7import PlainDraggable from 'plain-draggable'
8import { renderWith, parseConfigsFromYaml } from 'mapclay' 7import { renderWith, parseConfigsFromYaml } from 'mapclay'
9import { onRemove, animateRectTransition, throttle } from './utils' 8import { onRemove, animateRectTransition, throttle } from './utils'
10 9
11// FUNCTION: Get DocLinks from special anchor element {{{ 10// FUNCTION: Get DocLinks from special anchor element {{{
11import { OverlayLayout } from './OverlayLayout'
12 12
13const docLinkSelector = 'a[href^="#"][title^="=>"]' 13const docLinkSelector = 'a[href^="#"][title^="=>"]'
14
15class 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
24const layouts = [
25 new Layout({ name: "none" }),
26 new Layout({ name: "side" }),
27 new OverlayLayout(),
28]
14export const createDocLinks = (container) => Array.from(container.querySelectorAll(docLinkSelector)) 29export 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
124export const generateMaps = async (container, callback) => { 139export 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,