aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/dumbymap.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'src/dumbymap.mjs')
-rw-r--r--src/dumbymap.mjs174
1 files changed, 88 insertions, 86 deletions
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs
index 241c6b9..dc22021 100644
--- a/src/dumbymap.mjs
+++ b/src/dumbymap.mjs
@@ -1,21 +1,21 @@
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
@@ -26,12 +26,12 @@ const mapCache = {};
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
@@ -40,13 +40,13 @@ export const createDocLink = 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());
@@ -63,13 +63,13 @@ export const createDocLink = link => {
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 69 : url?.href
70 ?.match(/^geo:([0-9.,]+)/) 70 ?.match(/^geo:([0-9.,]+)/)
71 ?.at(1) 71 ?.at(1)
72 ?.split(",") 72 ?.split(',')
73 ?.reverse() 73 ?.reverse()
74 ?.map(Number); 74 ?.map(Number);
75 75
@@ -78,8 +78,8 @@ export const createGeoLink = (link, callback = null) => {
78 // Geo information in link 78 // Geo information in link
79 link.url = url; 79 link.url = url;
80 link.xy = xy; 80 link.xy = xy;
81 link.classList.add("with-leader-line", "geolink"); 81 link.classList.add('with-leader-line', 'geolink');
82 link.targets = link.url.searchParams.get("id")?.split(",") ?? null; 82 link.targets = link.url.searchParams.get('id')?.split(',') ?? null;
83 83
84 // LeaderLine 84 // LeaderLine
85 link.lines = []; 85 link.lines = [];
@@ -94,7 +94,7 @@ export const markdown2HTML = (container, mdContent) => {
94 Array.from(container.children).map(e => e.remove()); 94 Array.from(container.children).map(e => e.remove());
95 95
96 container.innerHTML = '<div class="SemanticHtml"></div>'; 96 container.innerHTML = '<div class="SemanticHtml"></div>';
97 const htmlHolder = container.querySelector(".SemanticHtml"); 97 const htmlHolder = container.querySelector('.SemanticHtml');
98 98
99 const md = MarkdownIt({ 99 const md = MarkdownIt({
100 html: true, 100 html: true,
@@ -102,7 +102,7 @@ export const markdown2HTML = (container, mdContent) => {
102 }) 102 })
103 .use(MarkdownItAnchor, { 103 .use(MarkdownItAnchor, {
104 permalink: MarkdownItAnchor.permalink.linkInsideHeader({ 104 permalink: MarkdownItAnchor.permalink.linkInsideHeader({
105 placement: "before", 105 placement: 'before',
106 }), 106 }),
107 }) 107 })
108 .use(MarkdownItFootnote) 108 .use(MarkdownItFootnote)
@@ -110,49 +110,49 @@ export const markdown2HTML = (container, mdContent) => {
110 .use(MarkdownItTocDoneRight); 110 .use(MarkdownItTocDoneRight);
111 111
112 // FIXME A better way to generate blocks 112 // FIXME A better way to generate blocks
113 md.renderer.rules.dumby_block_open = () => "<div>"; 113 md.renderer.rules.dumby_block_open = () => '<div>';
114 md.renderer.rules.dumby_block_close = () => "</div>"; 114 md.renderer.rules.dumby_block_close = () => '</div>';
115 115
116 md.core.ruler.before("block", "dumby_block", state => { 116 md.core.ruler.before('block', 'dumby_block', state => {
117 state.tokens.push(new state.Token("dumby_block_open", "", 1)); 117 state.tokens.push(new state.Token('dumby_block_open', '', 1));
118 }); 118 });
119 119
120 // Add close tag for block with more than 2 empty lines 120 // Add close tag for block with more than 2 empty lines
121 md.block.ruler.before("table", "dumby_block", (state, startLine) => { 121 md.block.ruler.before('table', 'dumby_block', (state, startLine) => {
122 if ( 122 if (
123 state.src[state.bMarks[startLine - 1]] === "\n" && 123 state.src[state.bMarks[startLine - 1]] === '\n' &&
124 state.src[state.bMarks[startLine - 2]] === "\n" && 124 state.src[state.bMarks[startLine - 2]] === '\n' &&
125 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>
126 ) { 126 ) {
127 state.push("dumby_block_close", "", -1); 127 state.push('dumby_block_close', '', -1);
128 state.push("dumby_block_open", "", 1); 128 state.push('dumby_block_open', '', 1);
129 } 129 }
130 }); 130 });
131 131
132 md.core.ruler.after("block", "dumby_block", state => { 132 md.core.ruler.after('block', 'dumby_block', state => {
133 state.tokens.push(new state.Token("dumby_block_close", "", -1)); 133 state.tokens.push(new state.Token('dumby_block_close', '', -1));
134 }); 134 });
135 135
136 const contentWithToc = "${toc}\n\n\n" + mdContent; 136 const contentWithToc = '${toc}\n\n\n' + mdContent;
137 htmlHolder.innerHTML = md.render(contentWithToc); 137 htmlHolder.innerHTML = md.render(contentWithToc);
138 138
139 // TODO Do this in markdown-it 139 // TODO Do this in markdown-it
140 const blocks = htmlHolder.querySelectorAll(":scope > div:not(:has(nav))"); 140 const blocks = htmlHolder.querySelectorAll(':scope > div:not(:has(nav))');
141 blocks.forEach(b => { 141 blocks.forEach(b => {
142 b.classList.add("dumby-block"); 142 b.classList.add('dumby-block');
143 b.setAttribute("data-total", blocks.length); 143 b.setAttribute('data-total', blocks.length);
144 }); 144 });
145 145
146 return container; 146 return container;
147 //}}} 147 //}}}
148}; 148};
149export const generateMaps = (container, { delay, mapCallback }) => { 149export const generateMaps = (container, { delay, mapCallback }) => {
150 container.classList.add("Dumby"); 150 container.classList.add('Dumby');
151 const htmlHolder = container.querySelector(".SemanticHtml") ?? container; 151 const htmlHolder = container.querySelector('.SemanticHtml') ?? container;
152 const blocks = Array.from(htmlHolder.querySelectorAll(".dumby-block")); 152 const blocks = Array.from(htmlHolder.querySelectorAll('.dumby-block'));
153 const showcase = document.createElement("div"); 153 const showcase = document.createElement('div');
154 container.appendChild(showcase); 154 container.appendChild(showcase);
155 showcase.classList.add("Showcase"); 155 showcase.classList.add('Showcase');
156 const renderMaps = []; 156 const renderMaps = [];
157 157
158 const dumbymap = { 158 const dumbymap = {
@@ -196,13 +196,13 @@ export const generateMaps = (container, { delay, mapCallback }) => {
196 ).filter(l => createGeoLink(l, geoLinkCallback)); 196 ).filter(l => createGeoLink(l, geoLinkCallback));
197 197
198 const isAnchorPointedBy = link => anchor => { 198 const isAnchorPointedBy = link => anchor => {
199 const mapContainer = anchor.closest(".mapclay"); 199 const mapContainer = anchor.closest('.mapclay');
200 const isTarget = !link.targets || link.targets.includes(mapContainer.id); 200 const isTarget = !link.targets || link.targets.includes(mapContainer.id);
201 return anchor.title === link.url.pathname && isTarget; 201 return anchor.title === link.url.pathname && isTarget;
202 }; 202 };
203 203
204 const isAnchorVisible = anchor => { 204 const isAnchorVisible = anchor => {
205 const mapContainer = anchor.closest(".mapclay"); 205 const mapContainer = anchor.closest('.mapclay');
206 return insideWindow(anchor) && insideParent(anchor, mapContainer); 206 return insideWindow(anchor) && insideParent(anchor, mapContainer);
207 }; 207 };
208 208
@@ -211,10 +211,10 @@ export const generateMaps = (container, { delay, mapCallback }) => {
211 start: link, 211 start: link,
212 end: anchor, 212 end: anchor,
213 hide: true, 213 hide: true,
214 middleLabel: link.url.searchParams.get("text"), 214 middleLabel: link.url.searchParams.get('text'),
215 path: "magnet", 215 path: 'magnet',
216 }); 216 });
217 line.show("draw", { duration: 300 }); 217 line.show('draw', { duration: 300 });
218 return line; 218 return line;
219 }; 219 };
220 220
@@ -232,7 +232,7 @@ export const generateMaps = (container, { delay, mapCallback }) => {
232 }; 232 };
233 233
234 const updateMapByMarker = xy => marker => { 234 const updateMapByMarker = xy => marker => {
235 const renderer = marker.closest(".mapclay")?.renderer; 235 const renderer = marker.closest('.mapclay')?.renderer;
236 renderer.updateCamera({ center: xy }, true); 236 renderer.updateCamera({ center: xy }, true);
237 }; 237 };
238 238
@@ -273,7 +273,7 @@ export const generateMaps = (container, { delay, mapCallback }) => {
273 const target = mutation.target; 273 const target = mutation.target;
274 const focus = target 274 const focus = target
275 .getAttribute(mutation.attributeName) 275 .getAttribute(mutation.attributeName)
276 .includes("focus"); 276 .includes('focus');
277 const shouldBeInShowcase = 277 const shouldBeInShowcase =
278 focus && 278 focus &&
279 showcase.checkVisibility({ 279 showcase.checkVisibility({
@@ -287,8 +287,8 @@ export const generateMaps = (container, { delay, mapCallback }) => {
287 287
288 // Placeholder for map in Showcase, it should has the same DOMRect 288 // Placeholder for map in Showcase, it should has the same DOMRect
289 const placeholder = target.cloneNode(true); 289 const placeholder = target.cloneNode(true);
290 placeholder.removeAttribute("id"); 290 placeholder.removeAttribute('id');
291 placeholder.classList.remove("mapclay", "focus"); 291 placeholder.classList.remove('mapclay', 'focus');
292 target.parentElement.replaceChild(placeholder, target); 292 target.parentElement.replaceChild(placeholder, target);
293 293
294 // FIXME Maybe use @start-style for CSS 294 // FIXME Maybe use @start-style for CSS
@@ -297,10 +297,10 @@ export const generateMaps = (container, { delay, mapCallback }) => {
297 // To make sure the original height of placeholder is applied, DOM changes seems needed 297 // To make sure the original height of placeholder is applied, DOM changes seems needed
298 // then set data-attribute for CSS selector to change height to 0 298 // then set data-attribute for CSS selector to change height to 0
299 placeholder.getBoundingClientRect(); 299 placeholder.getBoundingClientRect();
300 placeholder.setAttribute("data-placeholder", target.id); 300 placeholder.setAttribute('data-placeholder', target.id);
301 301
302 // To fit showcase, remove all inline style 302 // To fit showcase, remove all inline style
303 target.removeAttribute("style"); 303 target.removeAttribute('style');
304 showcase.appendChild(target); 304 showcase.appendChild(target);
305 305
306 // Resume rect from Semantic HTML to Showcase, with animation 306 // Resume rect from Semantic HTML to Showcase, with animation
@@ -333,7 +333,7 @@ export const generateMaps = (container, { delay, mapCallback }) => {
333 // Layout {{{ 333 // Layout {{{
334 // press key to switch layout 334 // press key to switch layout
335 const defaultLayout = layouts[0]; 335 const defaultLayout = layouts[0];
336 container.setAttribute("data-layout", defaultLayout.name); 336 container.setAttribute('data-layout', defaultLayout.name);
337 337
338 // observe layout change 338 // observe layout change
339 const layoutObserver = new MutationObserver(mutations => { 339 const layoutObserver = new MutationObserver(mutations => {
@@ -351,7 +351,7 @@ export const generateMaps = (container, { delay, mapCallback }) => {
351 Object.values(dumbymap) 351 Object.values(dumbymap)
352 .flat() 352 .flat()
353 .filter(ele => ele instanceof HTMLElement) 353 .filter(ele => ele instanceof HTMLElement)
354 .forEach(ele => ele.removeAttribute("style")); 354 .forEach(ele => ele.removeAttribute('style'));
355 355
356 if (newLayout) { 356 if (newLayout) {
357 layouts 357 layouts
@@ -362,13 +362,13 @@ export const generateMaps = (container, { delay, mapCallback }) => {
362 // 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
363 // Reset attribute triggers MutationObserver which is observing it 363 // Reset attribute triggers MutationObserver which is observing it
364 const focusMap = 364 const focusMap =
365 container.querySelector(".mapclay.focus") ?? 365 container.querySelector('.mapclay.focus') ??
366 container.querySelector(".mapclay"); 366 container.querySelector('.mapclay');
367 focusMap?.classList?.add("focus"); 367 focusMap?.classList?.add('focus');
368 }); 368 });
369 layoutObserver.observe(container, { 369 layoutObserver.observe(container, {
370 attributes: true, 370 attributes: true,
371 attributeFilter: ["data-layout"], 371 attributeFilter: ['data-layout'],
372 attributeOldValue: true, 372 attributeOldValue: true,
373 characterDataOldValue: true, 373 characterDataOldValue: true,
374 }); 374 });
@@ -380,8 +380,10 @@ export const generateMaps = (container, { delay, mapCallback }) => {
380 380
381 const afterMapRendered = renderer => { 381 const afterMapRendered = renderer => {
382 const mapElement = renderer.target; 382 const mapElement = renderer.target;
383 mapElement.setAttribute("tabindex", "-1"); 383 //FIXME
384 if (mapElement.getAttribute("data-render") === "fulfilled") { 384 mapElement.renderer = renderer;
385 mapElement.setAttribute('tabindex', '-1');
386 if (mapElement.getAttribute('data-render') === 'fulfilled') {
385 mapCache[mapElement.id] = renderer; 387 mapCache[mapElement.id] = renderer;
386 } 388 }
387 389
@@ -394,14 +396,14 @@ export const generateMaps = (container, { delay, mapCallback }) => {
394 // Add markers with Geolinks 396 // Add markers with Geolinks
395 renderer.addMarkers(markers); 397 renderer.addMarkers(markers);
396 mapElement 398 mapElement
397 .querySelectorAll(".marker") 399 .querySelectorAll('.marker')
398 .forEach(marker => htmlHolder.anchors.push(marker)); 400 .forEach(marker => htmlHolder.anchors.push(marker));
399 401
400 // Work with Mutation Observer 402 // Work with Mutation Observer
401 const observer = mapFocusObserver(); 403 const observer = mapFocusObserver();
402 mapFocusObserver().observe(mapElement, { 404 mapFocusObserver().observe(mapElement, {
403 attributes: true, 405 attributes: true,
404 attributeFilter: ["class"], 406 attributeFilter: ['class'],
405 attributeOldValue: true, 407 attributeOldValue: true,
406 }); 408 });
407 onRemove(mapElement, () => observer.disconnect()); 409 onRemove(mapElement, () => observer.disconnect());
@@ -412,10 +414,10 @@ export const generateMaps = (container, { delay, mapCallback }) => {
412 const assignMapId = config => { 414 const assignMapId = config => {
413 let mapId = config.id; 415 let mapId = config.id;
414 if (!mapId) { 416 if (!mapId) {
415 mapId = config.use?.split("/")?.at(-1); 417 mapId = config.use?.split('/')?.at(-1);
416 let counter = 1; 418 let counter = 1;
417 while (!mapId || mapIdList.includes(mapId)) { 419 while (!mapId || mapIdList.includes(mapId)) {
418 mapId = `${config.use ?? "unnamed"}-${counter}`; 420 mapId = `${config.use ?? 'unnamed'}-${counter}`;
419 counter++; 421 counter++;
420 } 422 }
421 config.id = mapId; 423 config.id = mapId;
@@ -426,21 +428,21 @@ export const generateMaps = (container, { delay, mapCallback }) => {
426 428
427 // Render each code block with "language-map" class 429 // Render each code block with "language-map" class
428 const elementsWithMapConfig = Array.from( 430 const elementsWithMapConfig = Array.from(
429 container.querySelectorAll('pre:has(.language-map)') ?? [] 431 container.querySelectorAll('pre:has(.language-map)') ?? [],
430 ) 432 );
431 /** 433 /**
432 * updateAttributeByStep. 434 * updateAttributeByStep.
433 * 435 *
434 * @param {Object} -- renderer which is running steps 436 * @param {Object} -- renderer which is running steps
435 */ 437 */
436 const updateAttributeByStep = ({ results, target, steps }) => { 438 const updateAttributeByStep = ({ results, target, steps }) => {
437 let passNum = results 439 let passNum = results.filter(
438 .filter(r => r.type === 'step' && r.state.match(/success|skip/)) 440 r => r.type === 'step' && r.state.match(/success|skip/),
439 .length 441 ).length;
440 const total = steps.length; 442 const total = steps.length;
441 passNum += `/${total}`; 443 passNum += `/${total}`;
442 if (results.filter(r=>r.type === 'step').length === total) { 444 if (results.filter(r => r.type === 'step').length === total) {
443 passNum += '\u0020' 445 passNum += '\u0020';
444 } 446 }
445 447
446 // FIXME HACK use MutationObserver for animation 448 // FIXME HACK use MutationObserver for animation
@@ -449,7 +451,7 @@ export const generateMaps = (container, { delay, mapCallback }) => {
449 await new Promise(resolve => setTimeout(resolve, 150)); 451 await new Promise(resolve => setTimeout(resolve, 150));
450 target.setAttribute('data-report', passNum); 452 target.setAttribute('data-report', passNum);
451 }); 453 });
452 } 454 };
453 /** 455 /**
454 * config converter for mapclay.renderWith() 456 * config converter for mapclay.renderWith()
455 * 457 *
@@ -465,21 +467,21 @@ export const generateMaps = (container, { delay, mapCallback }) => {
465 ...(config.aliases ?? {}), 467 ...(config.aliases ?? {}),
466 }, 468 },
467 stepCallback: updateAttributeByStep, 469 stepCallback: updateAttributeByStep,
468 }) 470 });
469 const render = renderWith(configConverter) 471 const render = renderWith(configConverter);
470 elementsWithMapConfig.forEach(target => { 472 elementsWithMapConfig.forEach(target => {
471 // Get text in code block starts with markdown text '```map' 473 // Get text in code block starts with markdown text '```map'
472 const configText = target 474 const configText = target
473 .querySelector(".language-map") 475 .querySelector('.language-map')
474 .textContent // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content 476 .textContent // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content
475 // replace it by normal space 477 // replace it by normal space
476 .replace(/\u00A0/g, "\u0020"); 478 .replace(/\u00A0/g, '\u0020');
477 479
478 let configList = []; 480 let configList = [];
479 try { 481 try {
480 configList = parseConfigsFromYaml(configText).map(assignMapId); 482 configList = parseConfigsFromYaml(configText).map(assignMapId);
481 } catch (_) { 483 } catch (_) {
482 console.warn("Fail to parse yaml config for element", target); 484 console.warn('Fail to parse yaml config for element', target);
483 return; 485 return;
484 } 486 }
485 487
@@ -494,9 +496,9 @@ export const generateMaps = (container, { delay, mapCallback }) => {
494 }); 496 });
495 497
496 // trivial: if map cache is applied, do not show yaml text 498 // trivial: if map cache is applied, do not show yaml text
497 if (target.querySelector(".mapclay")) { 499 if (target.querySelector('.mapclay')) {
498 target 500 target
499 .querySelectorAll(":scope > :not([data-render=fulfilled])") 501 .querySelectorAll(':scope > :not([data-render=fulfilled])')
500 .forEach(e => e.remove()); 502 .forEach(e => e.remove());
501 } 503 }
502 504