aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/dumbymap.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'src/dumbymap.mjs')
-rw-r--r--src/dumbymap.mjs591
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 @@
1import MarkdownIt from 'markdown-it' 1import MarkdownIt from "markdown-it";
2import MarkdownItAnchor from 'markdown-it-anchor' 2import MarkdownItAnchor from "markdown-it-anchor";
3import MarkdownItFootnote from 'markdown-it-footnote' 3import 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 { renderWith, defaultAliases, parseConfigsFromYaml } from 'mapclay' 7import { renderWith, defaultAliases, parseConfigsFromYaml } from "mapclay";
8import { onRemove, animateRectTransition, throttle } from './utils' 8import { onRemove, animateRectTransition, throttle } from "./utils";
9import { Layout, SideBySide, Overlay } from './Layout' 9import { Layout, SideBySide, Overlay } from "./Layout";
10import * as utils from './dumbyUtils' 10import * as utils from "./dumbyUtils";
11 11
12const docLinkSelector = 'a[href^="#"][title^="=>"]' 12const docLinkSelector = 'a[href^="#"][title^="=>"]';
13const geoLinkSelector = 'a[href^="geo:"]' 13const geoLinkSelector = 'a[href^="geo:"]';
14 14
15const layouts = [ 15const 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];
20const mapCache = {} 20const 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 */
28export const createDocLink = (link) => { 28export 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 */
64export const createGeoLink = (link, callback = null) => { 64export 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
87export const markdown2HTML = (container, mdContent) => { 92export 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};
143export const generateMaps = (container, {delay, mapCallback}) => { 149export 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};