diff options
Diffstat (limited to 'src/dumbymap.mjs')
-rw-r--r-- | src/dumbymap.mjs | 591 |
1 files changed, 306 insertions, 285 deletions
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs index 56375f7..cb029de 100644 --- a/src/dumbymap.mjs +++ b/src/dumbymap.mjs | |||
@@ -1,23 +1,23 @@ | |||
1 | import MarkdownIt from 'markdown-it' | 1 | import MarkdownIt from "markdown-it"; |
2 | import MarkdownItAnchor from 'markdown-it-anchor' | 2 | import MarkdownItAnchor from "markdown-it-anchor"; |
3 | import MarkdownItFootnote from 'markdown-it-footnote' | 3 | 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 { renderWith, defaultAliases, parseConfigsFromYaml } from 'mapclay' | 7 | import { renderWith, defaultAliases, parseConfigsFromYaml } from "mapclay"; |
8 | import { onRemove, animateRectTransition, throttle } from './utils' | 8 | import { onRemove, animateRectTransition, throttle } from "./utils"; |
9 | import { Layout, SideBySide, Overlay } from './Layout' | 9 | import { Layout, SideBySide, Overlay } from "./Layout"; |
10 | import * as utils from './dumbyUtils' | 10 | import * as utils from "./dumbyUtils"; |
11 | 11 | ||
12 | const docLinkSelector = 'a[href^="#"][title^="=>"]' | 12 | const docLinkSelector = 'a[href^="#"][title^="=>"]'; |
13 | const geoLinkSelector = 'a[href^="geo:"]' | 13 | const geoLinkSelector = 'a[href^="geo:"]'; |
14 | 14 | ||
15 | const layouts = [ | 15 | const layouts = [ |
16 | new Layout({ name: "normal" }), | 16 | new Layout({ name: "normal" }), |
17 | new SideBySide({ name: "side-by-side" }), | 17 | new SideBySide({ name: "side-by-side" }), |
18 | new Overlay({ name: "overlay" }), | 18 | new Overlay({ name: "overlay" }), |
19 | ] | 19 | ]; |
20 | const mapCache = {} | 20 | const mapCache = {}; |
21 | 21 | ||
22 | // FUNCTION: Get DocLinks from special anchor element {{{ | 22 | // FUNCTION: Get DocLinks from special anchor element {{{ |
23 | /** | 23 | /** |
@@ -25,34 +25,34 @@ const mapCache = {} | |||
25 | * | 25 | * |
26 | * @param {HTMLElement} Elements contains anchor elements for doclinks | 26 | * @param {HTMLElement} Elements contains anchor elements for doclinks |
27 | */ | 27 | */ |
28 | export const createDocLink = (link) => { | 28 | export const createDocLink = link => { |
29 | link.classList.add('with-leader-line', 'doclink') | 29 | link.classList.add("with-leader-line", "doclink"); |
30 | link.lines = [] | 30 | link.lines = []; |
31 | 31 | ||
32 | link.onmouseover = () => { | 32 | link.onmouseover = () => { |
33 | const label = decodeURIComponent(link.href.split('#')[1]) | 33 | const label = decodeURIComponent(link.href.split("#")[1]); |
34 | const selector = link.title.split('=>')[1] ?? '#' + label | 34 | const selector = link.title.split("=>")[1] ?? "#" + label; |
35 | const target = document.querySelector(selector) | 35 | const target = document.querySelector(selector); |
36 | if (!target?.checkVisibility()) return | 36 | if (!target?.checkVisibility()) return; |
37 | 37 | ||
38 | const line = new LeaderLine({ | 38 | const line = new LeaderLine({ |
39 | start: link, | 39 | start: link, |
40 | end: target, | 40 | end: target, |
41 | middleLabel: LeaderLine.pathLabel({ | 41 | middleLabel: LeaderLine.pathLabel({ |
42 | text: label, | 42 | text: label, |
43 | fontWeight: 'bold', | 43 | fontWeight: "bold", |
44 | }), | 44 | }), |
45 | hide: true, | 45 | hide: true, |
46 | path: "magnet" | 46 | path: "magnet", |
47 | }) | 47 | }); |
48 | link.lines.push(line) | 48 | link.lines.push(line); |
49 | line.show('draw', { duration: 300, }) | 49 | line.show("draw", { duration: 300 }); |
50 | } | 50 | }; |
51 | link.onmouseout = () => { | 51 | link.onmouseout = () => { |
52 | link.lines.forEach(line => line.remove()) | 52 | link.lines.forEach(line => line.remove()); |
53 | link.lines.length = 0 | 53 | link.lines.length = 0; |
54 | } | 54 | }; |
55 | } | 55 | }; |
56 | // }}} | 56 | // }}} |
57 | // FUNCTION: Get GeoLinks from special anchor element {{{ | 57 | // FUNCTION: Get GeoLinks from special anchor element {{{ |
58 | /** | 58 | /** |
@@ -62,92 +62,98 @@ export const createDocLink = (link) => { | |||
62 | * @returns {Boolean} ture is link is created, false if coordinates are invalid | 62 | * @returns {Boolean} ture is link is created, false if coordinates are invalid |
63 | */ | 63 | */ |
64 | export const createGeoLink = (link, callback = null) => { | 64 | export const createGeoLink = (link, callback = null) => { |
65 | const url = new URL(link.href) | 65 | const url = new URL(link.href); |
66 | const xyInParams = url.searchParams.get('xy') | 66 | const xyInParams = url.searchParams.get("xy"); |
67 | const xy = xyInParams | 67 | const xy = xyInParams |
68 | ? xyInParams.split(',')?.map(Number) | 68 | ? xyInParams.split(",")?.map(Number) |
69 | : url?.href?.match(/^geo:([0-9.,]+)/)?.at(1)?.split(',')?.reverse()?.map(Number) | 69 | : url?.href |
70 | ?.match(/^geo:([0-9.,]+)/) | ||
71 | ?.at(1) | ||
72 | ?.split(",") | ||
73 | ?.reverse() | ||
74 | ?.map(Number); | ||
70 | 75 | ||
71 | if (!xy || isNaN(xy[0]) || isNaN(xy[1])) return false | 76 | if (!xy || isNaN(xy[0]) || isNaN(xy[1])) return false; |
72 | 77 | ||
73 | // Geo information in link | 78 | // Geo information in link |
74 | link.url = url | 79 | link.url = url; |
75 | link.xy = xy | 80 | link.xy = xy; |
76 | link.classList.add('with-leader-line', 'geolink') | 81 | link.classList.add("with-leader-line", "geolink"); |
77 | link.targets = link.url.searchParams.get('id')?.split(',') ?? null | 82 | link.targets = link.url.searchParams.get("id")?.split(",") ?? null; |
78 | 83 | ||
79 | // LeaderLine | 84 | // LeaderLine |
80 | link.lines = [] | 85 | link.lines = []; |
81 | callback?.call(this, link) | 86 | callback?.call(this, link); |
82 | 87 | ||
83 | return true | 88 | return true; |
84 | } | 89 | }; |
85 | // }}} | 90 | // }}} |
86 | 91 | ||
87 | export const markdown2HTML = (container, mdContent) => { | 92 | export const markdown2HTML = (container, mdContent) => { |
88 | // Render: Markdown -> HTML {{{ | 93 | // Render: Markdown -> HTML {{{ |
89 | Array.from(container.children).map(e => e.remove()) | 94 | Array.from(container.children).map(e => e.remove()); |
90 | 95 | ||
91 | 96 | container.innerHTML = '<div class="SemanticHtml"></div>'; | |
92 | container.innerHTML = '<div class="SemanticHtml"></div>' | 97 | const htmlHolder = container.querySelector(".SemanticHtml"); |
93 | const htmlHolder = container.querySelector('.SemanticHtml') | ||
94 | 98 | ||
95 | const md = MarkdownIt({ | 99 | const md = MarkdownIt({ |
96 | html: true, | 100 | html: true, |
97 | breaks: true, | 101 | breaks: true, |
98 | }) | 102 | }) |
99 | .use(MarkdownItAnchor, { | 103 | .use(MarkdownItAnchor, { |
100 | permalink: MarkdownItAnchor.permalink.linkInsideHeader({ placement: 'before' }) | 104 | permalink: MarkdownItAnchor.permalink.linkInsideHeader({ |
105 | placement: "before", | ||
106 | }), | ||
101 | }) | 107 | }) |
102 | .use(MarkdownItFootnote) | 108 | .use(MarkdownItFootnote) |
103 | .use(MarkdownItFrontMatter) | 109 | .use(MarkdownItFrontMatter) |
104 | .use(MarkdownItTocDoneRight) | 110 | .use(MarkdownItTocDoneRight); |
105 | 111 | ||
106 | // FIXME A better way to generate blocks | 112 | // FIXME A better way to generate blocks |
107 | md.renderer.rules.dumby_block_open = () => '<div>' | 113 | md.renderer.rules.dumby_block_open = () => "<div>"; |
108 | md.renderer.rules.dumby_block_close = () => '</div>' | 114 | md.renderer.rules.dumby_block_close = () => "</div>"; |
109 | 115 | ||
110 | md.core.ruler.before('block', 'dumby_block', (state) => { | 116 | md.core.ruler.before("block", "dumby_block", state => { |
111 | state.tokens.push(new state.Token('dumby_block_open', '', 1)) | 117 | state.tokens.push(new state.Token("dumby_block_open", "", 1)); |
112 | }) | 118 | }); |
113 | 119 | ||
114 | // Add close tag for block with more than 2 empty lines | 120 | // Add close tag for block with more than 2 empty lines |
115 | md.block.ruler.before('table', 'dumby_block', (state, startLine) => { | 121 | md.block.ruler.before("table", "dumby_block", (state, startLine) => { |
116 | if ( | 122 | if ( |
117 | state.src[state.bMarks[startLine - 1]] === '\n' && | 123 | state.src[state.bMarks[startLine - 1]] === "\n" && |
118 | state.src[state.bMarks[startLine - 2]] === '\n' && | 124 | state.src[state.bMarks[startLine - 2]] === "\n" && |
119 | state.tokens.at(-1).type !== 'list_item_open' // Quick hack for not adding tag after "::marker" for <li> | 125 | state.tokens.at(-1).type !== "list_item_open" // Quick hack for not adding tag after "::marker" for <li> |
120 | ) { | 126 | ) { |
121 | state.push('dumby_block_close', '', -1); | 127 | state.push("dumby_block_close", "", -1); |
122 | state.push('dumby_block_open', '', 1); | 128 | state.push("dumby_block_open", "", 1); |
123 | } | 129 | } |
124 | }) | 130 | }); |
125 | 131 | ||
126 | md.core.ruler.after('block', 'dumby_block', (state) => { | 132 | md.core.ruler.after("block", "dumby_block", state => { |
127 | state.tokens.push(new state.Token('dumby_block_close', '', -1)) | 133 | state.tokens.push(new state.Token("dumby_block_close", "", -1)); |
128 | }) | 134 | }); |
129 | 135 | ||
130 | const contentWithToc = '${toc}\n\n\n' + mdContent | 136 | const contentWithToc = "${toc}\n\n\n" + mdContent; |
131 | htmlHolder.innerHTML = md.render(contentWithToc); | 137 | htmlHolder.innerHTML = md.render(contentWithToc); |
132 | 138 | ||
133 | // TODO Do this in markdown-it | 139 | // TODO Do this in markdown-it |
134 | const blocks = htmlHolder.querySelectorAll(':scope > div:not(:has(nav))') | 140 | const blocks = htmlHolder.querySelectorAll(":scope > div:not(:has(nav))"); |
135 | blocks.forEach(b => { | 141 | blocks.forEach(b => { |
136 | b.classList.add('dumby-block') | 142 | b.classList.add("dumby-block"); |
137 | b.setAttribute('data-total', blocks.length) | 143 | b.setAttribute("data-total", blocks.length); |
138 | }) | 144 | }); |
139 | 145 | ||
140 | return container | 146 | return container; |
141 | //}}} | 147 | //}}} |
142 | } | 148 | }; |
143 | export const generateMaps = (container, {delay, mapCallback}) => { | 149 | export const generateMaps = (container, { delay, mapCallback }) => { |
144 | container.classList.add('Dumby') | 150 | container.classList.add("Dumby"); |
145 | const htmlHolder = container.querySelector('.SemanticHtml') ?? container | 151 | const htmlHolder = container.querySelector(".SemanticHtml") ?? container; |
146 | const blocks = Array.from(htmlHolder.querySelectorAll('.dumby-block')) | 152 | const blocks = Array.from(htmlHolder.querySelectorAll(".dumby-block")); |
147 | const showcase = document.createElement('div') | 153 | const showcase = document.createElement("div"); |
148 | container.appendChild(showcase) | 154 | container.appendChild(showcase); |
149 | showcase.classList.add('Showcase') | 155 | showcase.classList.add("Showcase"); |
150 | const renderMaps = [] | 156 | const renderMaps = []; |
151 | 157 | ||
152 | const dumbymap = { | 158 | const dumbymap = { |
153 | layouts, | 159 | layouts, |
@@ -161,310 +167,325 @@ export const generateMaps = (container, {delay, mapCallback}) => { | |||
161 | switchToNextLayout: throttle(utils.switchToNextLayout, 300), | 167 | switchToNextLayout: throttle(utils.switchToNextLayout, 300), |
162 | focusNextBlock: utils.focusNextBlock, | 168 | focusNextBlock: utils.focusNextBlock, |
163 | removeBlockFocus: utils.removeBlockFocus, | 169 | removeBlockFocus: utils.removeBlockFocus, |
164 | } | 170 | }, |
165 | } | 171 | }; |
166 | Object.entries(dumbymap.utils) | 172 | Object.entries(dumbymap.utils).forEach(([util, func]) => { |
167 | .forEach(([util, func]) => { | 173 | dumbymap.utils[util] = func.bind(dumbymap); |
168 | dumbymap.utils[util] = func.bind(dumbymap) | 174 | }); |
169 | }) | ||
170 | 175 | ||
171 | // LeaderLine {{{ | 176 | // LeaderLine {{{ |
172 | 177 | ||
173 | Array.from(container.querySelectorAll(docLinkSelector)) | 178 | Array.from(container.querySelectorAll(docLinkSelector)).filter(createDocLink); |
174 | .filter(createDocLink) | ||
175 | 179 | ||
176 | // Get anchors with "geo:" scheme | 180 | // Get anchors with "geo:" scheme |
177 | htmlHolder.anchors = [] | 181 | htmlHolder.anchors = []; |
178 | const geoLinkCallback = (link) => { | 182 | const geoLinkCallback = link => { |
179 | link.onmouseover = () => addLeaderLines(link) | 183 | link.onmouseover = () => addLeaderLines(link); |
180 | link.onmouseout = () => removeLeaderLines(link) | 184 | link.onmouseout = () => removeLeaderLines(link); |
181 | link.onclick = (event) => { | 185 | link.onclick = event => { |
182 | event.preventDefault() | 186 | event.preventDefault(); |
183 | htmlHolder.anchors | 187 | htmlHolder.anchors |
184 | .filter(isAnchorPointedBy(link)) | 188 | .filter(isAnchorPointedBy(link)) |
185 | .forEach(updateMapByMarker(link.xy)) | 189 | .forEach(updateMapByMarker(link.xy)); |
186 | // TODO Just hide leader line and show it again | 190 | // TODO Just hide leader line and show it again |
187 | removeLeaderLines(link) | 191 | removeLeaderLines(link); |
188 | } | 192 | }; |
189 | } | 193 | }; |
190 | const geoLinks = Array.from(container.querySelectorAll(geoLinkSelector)) | 194 | const geoLinks = Array.from( |
191 | .filter(l => createGeoLink(l, geoLinkCallback)) | 195 | container.querySelectorAll(geoLinkSelector), |
192 | 196 | ).filter(l => createGeoLink(l, geoLinkCallback)); | |
193 | const isAnchorPointedBy = (link) => (anchor) => { | 197 | |
194 | const mapContainer = anchor.closest('.mapclay') | 198 | const isAnchorPointedBy = link => anchor => { |
195 | const isTarget = !link.targets || link.targets.includes(mapContainer.id) | 199 | const mapContainer = anchor.closest(".mapclay"); |
196 | return anchor.title === link.url.pathname && isTarget | 200 | const isTarget = !link.targets || link.targets.includes(mapContainer.id); |
197 | } | 201 | return anchor.title === link.url.pathname && isTarget; |
198 | 202 | }; | |
199 | const isAnchorVisible = (anchor) => { | 203 | |
200 | const mapContainer = anchor.closest('.mapclay') | 204 | const isAnchorVisible = anchor => { |
201 | return insideWindow(anchor) && insideParent(anchor, mapContainer) | 205 | const mapContainer = anchor.closest(".mapclay"); |
202 | } | 206 | return insideWindow(anchor) && insideParent(anchor, mapContainer); |
203 | 207 | }; | |
204 | const drawLeaderLine = (link) => (anchor) => { | 208 | |
209 | const drawLeaderLine = link => anchor => { | ||
205 | const line = new LeaderLine({ | 210 | const line = new LeaderLine({ |
206 | start: link, | 211 | start: link, |
207 | end: anchor, | 212 | end: anchor, |
208 | hide: true, | 213 | hide: true, |
209 | middleLabel: link.url.searchParams.get('text'), | 214 | middleLabel: link.url.searchParams.get("text"), |
210 | path: "magnet", | 215 | path: "magnet", |
211 | }) | 216 | }); |
212 | line.show('draw', { duration: 300, }) | 217 | line.show("draw", { duration: 300 }); |
213 | return line | 218 | return line; |
214 | } | 219 | }; |
215 | 220 | ||
216 | const addLeaderLines = (link) => { | 221 | const addLeaderLines = link => { |
217 | link.lines = htmlHolder.anchors | 222 | link.lines = htmlHolder.anchors |
218 | .filter(isAnchorPointedBy(link)) | 223 | .filter(isAnchorPointedBy(link)) |
219 | .filter(isAnchorVisible) | 224 | .filter(isAnchorVisible) |
220 | .map(drawLeaderLine(link)) | 225 | .map(drawLeaderLine(link)); |
221 | } | 226 | }; |
222 | 227 | ||
223 | const removeLeaderLines = (link) => { | 228 | const removeLeaderLines = link => { |
224 | if (!link.lines) return | 229 | if (!link.lines) return; |
225 | link.lines.forEach(line => line.remove()) | 230 | link.lines.forEach(line => line.remove()); |
226 | link.lines = [] | 231 | link.lines = []; |
227 | } | 232 | }; |
228 | 233 | ||
229 | const updateMapByMarker = (xy) => (marker) => { | 234 | const updateMapByMarker = xy => marker => { |
230 | const renderer = marker.closest('.mapclay')?.renderer | 235 | const renderer = marker.closest(".mapclay")?.renderer; |
231 | renderer.updateCamera({ center: xy }, true) | 236 | renderer.updateCamera({ center: xy }, true); |
232 | } | 237 | }; |
233 | 238 | ||
234 | const insideWindow = (element) => { | 239 | const insideWindow = element => { |
235 | const rect = element.getBoundingClientRect() | 240 | const rect = element.getBoundingClientRect(); |
236 | return rect.left > 0 && | 241 | return ( |
242 | rect.left > 0 && | ||
237 | rect.right < window.innerWidth + rect.width && | 243 | rect.right < window.innerWidth + rect.width && |
238 | rect.top > 0 && | 244 | rect.top > 0 && |
239 | rect.bottom < window.innerHeight + rect.height | 245 | rect.bottom < window.innerHeight + rect.height |
240 | } | 246 | ); |
247 | }; | ||
241 | 248 | ||
242 | const insideParent = (childElement, parentElement) => { | 249 | const insideParent = (childElement, parentElement) => { |
243 | const childRect = childElement.getBoundingClientRect(); | 250 | const childRect = childElement.getBoundingClientRect(); |
244 | const parentRect = parentElement.getBoundingClientRect(); | 251 | const parentRect = parentElement.getBoundingClientRect(); |
245 | const offset = 20 | 252 | const offset = 20; |
246 | 253 | ||
247 | return childRect.left > parentRect.left + offset && | 254 | return ( |
255 | childRect.left > parentRect.left + offset && | ||
248 | childRect.right < parentRect.right - offset && | 256 | childRect.right < parentRect.right - offset && |
249 | childRect.top > parentRect.top + offset && | 257 | childRect.top > parentRect.top + offset && |
250 | childRect.bottom < parentRect.bottom - offset | 258 | childRect.bottom < parentRect.bottom - offset |
251 | } | 259 | ); |
260 | }; | ||
252 | //}}} | 261 | //}}} |
253 | // Draggable Blocks {{{ | 262 | // Draggable Blocks {{{ |
254 | // Add draggable part for blocks | 263 | // Add draggable part for blocks |
255 | 264 | ||
256 | |||
257 | // }}} | 265 | // }}} |
258 | // CSS observer {{{ | 266 | // CSS observer {{{ |
259 | // Focus Map {{{ | 267 | // Focus Map {{{ |
260 | // Set focusArea | 268 | // Set focusArea |
261 | 269 | ||
262 | const mapFocusObserver = () => new MutationObserver((mutations) => { | 270 | const mapFocusObserver = () => |
263 | const mutation = mutations.at(-1) | 271 | new MutationObserver(mutations => { |
264 | const target = mutation.target | 272 | const mutation = mutations.at(-1); |
265 | const focus = target.getAttribute(mutation.attributeName).includes('focus') | 273 | const target = mutation.target; |
266 | const shouldBeInShowcase = focus && showcase.checkVisibility({ | 274 | const focus = target |
267 | contentVisibilityAuto: true, | 275 | .getAttribute(mutation.attributeName) |
268 | opacityProperty: true, | 276 | .includes("focus"); |
269 | visibilityProperty: true, | 277 | const shouldBeInShowcase = |
270 | }) | 278 | focus && |
271 | 279 | showcase.checkVisibility({ | |
272 | if (shouldBeInShowcase) { | 280 | contentVisibilityAuto: true, |
273 | if (showcase.contains(target)) return | 281 | opacityProperty: true, |
274 | 282 | visibilityProperty: true, | |
275 | // Placeholder for map in Showcase, it should has the same DOMRect | 283 | }); |
276 | const placeholder = target.cloneNode(true) | 284 | |
277 | placeholder.removeAttribute('id') | 285 | if (shouldBeInShowcase) { |
278 | placeholder.classList.remove('mapclay', 'focus') | 286 | if (showcase.contains(target)) return; |
279 | target.parentElement.replaceChild(placeholder, target) | 287 | |
280 | 288 | // Placeholder for map in Showcase, it should has the same DOMRect | |
281 | // FIXME Maybe use @start-style for CSS | 289 | const placeholder = target.cloneNode(true); |
282 | // Trigger CSS transition, if placeholde is the olny chil element in block, | 290 | placeholder.removeAttribute("id"); |
283 | // reduce its height to zero. | 291 | placeholder.classList.remove("mapclay", "focus"); |
284 | // To make sure the original height of placeholder is applied, DOM changes seems needed | 292 | target.parentElement.replaceChild(placeholder, target); |
285 | // then set data-attribute for CSS selector to change height to 0 | 293 | |
286 | placeholder.getBoundingClientRect() | 294 | // FIXME Maybe use @start-style for CSS |
287 | placeholder.setAttribute('data-placeholder', target.id) | 295 | // Trigger CSS transition, if placeholde is the olny chil element in block, |
288 | 296 | // reduce its height to zero. | |
289 | // To fit showcase, remove all inline style | 297 | // To make sure the original height of placeholder is applied, DOM changes seems needed |
290 | target.removeAttribute('style') | 298 | // then set data-attribute for CSS selector to change height to 0 |
291 | showcase.appendChild(target) | 299 | placeholder.getBoundingClientRect(); |
292 | 300 | placeholder.setAttribute("data-placeholder", target.id); | |
293 | // Resume rect from Semantic HTML to Showcase, with animation | 301 | |
294 | animateRectTransition(target, placeholder.getBoundingClientRect(), { | 302 | // To fit showcase, remove all inline style |
295 | duration: 300, | 303 | target.removeAttribute("style"); |
296 | resume: true | 304 | showcase.appendChild(target); |
297 | }) | 305 | |
298 | } else if (showcase.contains(target)) { | 306 | // Resume rect from Semantic HTML to Showcase, with animation |
299 | // Check placeholder is inside Semantic HTML | 307 | animateRectTransition(target, placeholder.getBoundingClientRect(), { |
300 | const placeholder = htmlHolder.querySelector(`[data-placeholder="${target.id}"]`) | 308 | duration: 300, |
301 | if (!placeholder) throw Error(`Cannot fine placeholder for map "${target.id}"`) | 309 | resume: true, |
302 | 310 | }); | |
303 | // Consider animation may fail, write callback | 311 | } else if (showcase.contains(target)) { |
304 | const afterAnimation = () => { | 312 | // Check placeholder is inside Semantic HTML |
305 | placeholder.parentElement.replaceChild(target, placeholder) | 313 | const placeholder = htmlHolder.querySelector( |
306 | target.style = placeholder.style.cssText | 314 | `[data-placeholder="${target.id}"]`, |
307 | placeholder.remove() | 315 | ); |
316 | if (!placeholder) | ||
317 | throw Error(`Cannot fine placeholder for map "${target.id}"`); | ||
318 | |||
319 | // Consider animation may fail, write callback | ||
320 | const afterAnimation = () => { | ||
321 | placeholder.parentElement.replaceChild(target, placeholder); | ||
322 | target.style = placeholder.style.cssText; | ||
323 | placeholder.remove(); | ||
324 | }; | ||
325 | |||
326 | // animation from Showcase to placeholder | ||
327 | animateRectTransition(target, placeholder.getBoundingClientRect(), { | ||
328 | duration: 300, | ||
329 | }).finished.finally(afterAnimation); | ||
308 | } | 330 | } |
309 | 331 | }); | |
310 | // animation from Showcase to placeholder | ||
311 | animateRectTransition(target, placeholder.getBoundingClientRect(), { duration: 300 }) | ||
312 | .finished | ||
313 | .finally(afterAnimation) | ||
314 | } | ||
315 | }) | ||
316 | // }}} | 332 | // }}} |
317 | // Layout {{{ | 333 | // Layout {{{ |
318 | // press key to switch layout | 334 | // press key to switch layout |
319 | const defaultLayout = layouts[0] | 335 | const defaultLayout = layouts[0]; |
320 | container.setAttribute("data-layout", defaultLayout.name) | 336 | container.setAttribute("data-layout", defaultLayout.name); |
321 | 337 | ||
322 | // observe layout change | 338 | // observe layout change |
323 | const layoutObserver = new MutationObserver((mutations) => { | 339 | const layoutObserver = new MutationObserver(mutations => { |
324 | const mutation = mutations.at(-1) | 340 | const mutation = mutations.at(-1); |
325 | const oldLayout = mutation.oldValue | 341 | const oldLayout = mutation.oldValue; |
326 | const newLayout = container.getAttribute(mutation.attributeName) | 342 | const newLayout = container.getAttribute(mutation.attributeName); |
327 | 343 | ||
328 | // Apply handler for leaving/entering layouts | 344 | // Apply handler for leaving/entering layouts |
329 | if (oldLayout) { | 345 | if (oldLayout) { |
330 | layouts.find(l => l.name === oldLayout) | 346 | layouts |
331 | ?.leaveHandler | 347 | .find(l => l.name === oldLayout) |
332 | ?.call(this, dumbymap) | 348 | ?.leaveHandler?.call(this, dumbymap); |
333 | } | 349 | } |
334 | 350 | ||
335 | Object.values(dumbymap) | 351 | Object.values(dumbymap) |
336 | .flat() | 352 | .flat() |
337 | .filter(ele => ele instanceof HTMLElement) | 353 | .filter(ele => ele instanceof HTMLElement) |
338 | .forEach(ele => ele.removeAttribute('style')) | 354 | .forEach(ele => ele.removeAttribute("style")); |
339 | 355 | ||
340 | if (newLayout) { | 356 | if (newLayout) { |
341 | layouts.find(l => l.name === newLayout) | 357 | layouts |
342 | ?.enterHandler | 358 | .find(l => l.name === newLayout) |
343 | ?.call(this, dumbymap) | 359 | ?.enterHandler?.call(this, dumbymap); |
344 | } | 360 | } |
345 | 361 | ||
346 | // Since layout change may show/hide showcase, the current focused map should do something | 362 | // Since layout change may show/hide showcase, the current focused map should do something |
347 | // Reset attribute triggers MutationObserver which is observing it | 363 | // Reset attribute triggers MutationObserver which is observing it |
348 | const focusMap = container.querySelector('.mapclay.focus') | 364 | const focusMap = |
349 | ?? container.querySelector('.mapclay') | 365 | container.querySelector(".mapclay.focus") ?? |
350 | focusMap?.classList?.add('focus') | 366 | container.querySelector(".mapclay"); |
367 | focusMap?.classList?.add("focus"); | ||
351 | }); | 368 | }); |
352 | layoutObserver.observe(container, { | 369 | layoutObserver.observe(container, { |
353 | attributes: true, | 370 | attributes: true, |
354 | attributeFilter: ["data-layout"], | 371 | attributeFilter: ["data-layout"], |
355 | attributeOldValue: true, | 372 | attributeOldValue: true, |
356 | characterDataOldValue: true | 373 | characterDataOldValue: true, |
357 | }); | 374 | }); |
358 | 375 | ||
359 | onRemove(htmlHolder, () => layoutObserver.disconnect()) | 376 | onRemove(htmlHolder, () => layoutObserver.disconnect()); |
360 | //}}} | 377 | //}}} |
361 | //}}} | 378 | //}}} |
362 | // Render Maps {{{ | 379 | // Render Maps {{{ |
363 | 380 | ||
364 | const afterMapRendered = (renderer) => { | 381 | const afterMapRendered = renderer => { |
365 | const mapElement = renderer.target | 382 | const mapElement = renderer.target; |
366 | mapElement.setAttribute('tabindex', "-1") | 383 | mapElement.setAttribute("tabindex", "-1"); |
367 | if (mapElement.getAttribute('data-render') === 'fulfilled') { | 384 | if (mapElement.getAttribute("data-render") === "fulfilled") { |
368 | mapCache[mapElement.id] = renderer | 385 | mapCache[mapElement.id] = renderer; |
369 | } | 386 | } |
370 | 387 | ||
371 | // Execute callback from caller | 388 | // Execute callback from caller |
372 | mapCallback?.call(this, mapElement) | 389 | mapCallback?.call(this, mapElement); |
373 | const markers = geoLinks | 390 | const markers = geoLinks |
374 | .filter(link => !link.targets || link.targets.includes(mapElement.id)) | 391 | .filter(link => !link.targets || link.targets.includes(mapElement.id)) |
375 | .map(link => ({ xy: link.xy, title: link.url.pathname })) | 392 | .map(link => ({ xy: link.xy, title: link.url.pathname })); |
376 | 393 | ||
377 | // Add markers with Geolinks | 394 | // Add markers with Geolinks |
378 | renderer.addMarkers(markers) | 395 | renderer.addMarkers(markers); |
379 | mapElement.querySelectorAll('.marker') | 396 | mapElement |
380 | .forEach(marker => htmlHolder.anchors.push(marker)) | 397 | .querySelectorAll(".marker") |
398 | .forEach(marker => htmlHolder.anchors.push(marker)); | ||
381 | 399 | ||
382 | // Work with Mutation Observer | 400 | // Work with Mutation Observer |
383 | const observer = mapFocusObserver() | 401 | const observer = mapFocusObserver(); |
384 | mapFocusObserver().observe(mapElement, { | 402 | mapFocusObserver().observe(mapElement, { |
385 | attributes: true, | 403 | attributes: true, |
386 | attributeFilter: ["class"], | 404 | attributeFilter: ["class"], |
387 | attributeOldValue: true | 405 | attributeOldValue: true, |
388 | }); | 406 | }); |
389 | onRemove(mapElement, () => observer.disconnect()) | 407 | onRemove(mapElement, () => observer.disconnect()); |
390 | } | 408 | }; |
391 | 409 | ||
392 | // Set unique ID for map container | 410 | // Set unique ID for map container |
393 | const mapIdList = [] | 411 | const mapIdList = []; |
394 | const assignMapId = (config) => { | 412 | const assignMapId = config => { |
395 | let mapId = config.id | 413 | let mapId = config.id; |
396 | if (!mapId) { | 414 | if (!mapId) { |
397 | mapId = config.use?.split('/')?.at(-1) | 415 | mapId = config.use?.split("/")?.at(-1); |
398 | let counter = 1 | 416 | let counter = 1; |
399 | while (!mapId || mapIdList.includes(mapId)) { | 417 | while (!mapId || mapIdList.includes(mapId)) { |
400 | mapId = `${config.use ?? "unnamed"}-${counter}` | 418 | mapId = `${config.use ?? "unnamed"}-${counter}`; |
401 | counter++ | 419 | counter++; |
402 | } | 420 | } |
403 | config.id = mapId | 421 | config.id = mapId; |
404 | } | 422 | } |
405 | mapIdList.push(mapId) | 423 | mapIdList.push(mapId); |
406 | return config | 424 | return config; |
407 | } | 425 | }; |
408 | 426 | ||
409 | // Render each code block with "language-map" class | 427 | // Render each code block with "language-map" class |
410 | const elementsWithMapConfig = Array.from(container.querySelectorAll('pre:has(.language-map)') ?? []) | 428 | const elementsWithMapConfig = Array.from( |
429 | container.querySelectorAll("pre:has(.language-map)") ?? [], | ||
430 | ); | ||
411 | // Add default aliases into each config | 431 | // Add default aliases into each config |
412 | const configConverter = (config => ({ | 432 | const configConverter = config => ({ |
413 | use: config.use ?? 'Leaflet', | 433 | use: config.use ?? "Leaflet", |
414 | width: "100%", | 434 | width: "100%", |
415 | ...config, | 435 | ...config, |
416 | aliases: { | 436 | aliases: { |
417 | ...defaultAliases, | 437 | ...defaultAliases, |
418 | ...config.aliases ?? {} | 438 | ...(config.aliases ?? {}), |
419 | }, | 439 | }, |
420 | })) | 440 | }); |
421 | const render = renderWith(configConverter) | 441 | const render = renderWith(configConverter); |
422 | elementsWithMapConfig | 442 | elementsWithMapConfig.forEach(target => { |
423 | .forEach(target => { | 443 | // Get text in code block starts with markdown text '```map' |
424 | // Get text in code block starts with markdown text '```map' | 444 | const configText = target |
425 | const configText = target.querySelector('.language-map') | 445 | .querySelector(".language-map") |
426 | .textContent | 446 | .textContent // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content |
427 | // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content | 447 | // replace it by normal space |
428 | // replace it by normal space | 448 | .replace(/\u00A0/g, "\u0020"); |
429 | .replace(/\u00A0/g, '\u0020') | 449 | |
430 | 450 | let configList = []; | |
431 | let configList = [] | 451 | try { |
432 | try { | 452 | configList = parseConfigsFromYaml(configText).map(assignMapId); |
433 | configList = parseConfigsFromYaml(configText).map(assignMapId) | 453 | } catch (_) { |
434 | } catch (_) { | 454 | console.warn("Fail to parse yaml config for element", target); |
435 | console.warn('Fail to parse yaml config for element', target) | 455 | return; |
436 | return | 456 | } |
437 | } | ||
438 | 457 | ||
439 | // If map in cache has the same ID, just put it into target | 458 | // If map in cache has the same ID, just put it into target |
440 | // So user won't feel anything changes when editing markdown | 459 | // So user won't feel anything changes when editing markdown |
441 | configList.forEach(config => { | 460 | configList.forEach(config => { |
442 | const cache = mapCache[config.id] | 461 | const cache = mapCache[config.id]; |
443 | if (!cache) return; | 462 | if (!cache) return; |
444 | 463 | ||
445 | target.appendChild(cache.target) | 464 | target.appendChild(cache.target); |
446 | config.target = cache.target | 465 | config.target = cache.target; |
447 | }) | 466 | }); |
448 | 467 | ||
449 | // trivial: if map cache is applied, do not show yaml text | 468 | // trivial: if map cache is applied, do not show yaml text |
450 | if (target.querySelector('.mapclay')) { | 469 | if (target.querySelector(".mapclay")) { |
451 | target.querySelectorAll(':scope > :not([data-render=fulfilled])') | 470 | target |
452 | .forEach(e => e.remove()) | 471 | .querySelectorAll(":scope > :not([data-render=fulfilled])") |
453 | } | 472 | .forEach(e => e.remove()); |
473 | } | ||
454 | 474 | ||
455 | // Render maps with delay | 475 | // Render maps with delay |
456 | const timer = setTimeout(() => | 476 | const timer = setTimeout( |
477 | () => | ||
457 | render(target, configList).forEach(renderMap => { | 478 | render(target, configList).forEach(renderMap => { |
458 | renderMaps.push(renderMap) | 479 | renderMaps.push(renderMap); |
459 | renderMap.then(afterMapRendered) | 480 | renderMap.then(afterMapRendered); |
460 | }), | 481 | }), |
461 | delay ?? 1000 | 482 | delay ?? 1000, |
462 | ) | 483 | ); |
463 | onRemove(htmlHolder, () => { | 484 | onRemove(htmlHolder, () => { |
464 | clearTimeout(timer) | 485 | clearTimeout(timer); |
465 | }) | 486 | }); |
466 | }) | 487 | }); |
467 | 488 | ||
468 | //}}} | 489 | //}}} |
469 | return Object.seal(dumbymap) | 490 | return Object.seal(dumbymap); |
470 | } | 491 | }; |