aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorHsieh Chin Fan <pham@topo.tw>2024-09-29 15:06:59 +0800
committerHsieh Chin Fan <pham@topo.tw>2024-09-29 15:06:59 +0800
commitf598dcbf6283f2c2b81a63d315548c6bc4e943fb (patch)
tree204a86cf47dd2bd2223502d2daaeb4afc6ecce09
parent88201b62764931868f0bc3cae978edb3fd307485 (diff)
style: prettier
-rw-r--r--package.json3
-rw-r--r--src/Layout.mjs88
-rw-r--r--src/MenuItem.mjs34
-rw-r--r--src/css/dumbymap.css4
-rw-r--r--src/css/style.css2
-rw-r--r--src/dumbyUtils.mjs28
-rw-r--r--src/dumbymap.mjs174
-rw-r--r--src/editor.mjs188
-rw-r--r--src/utils.mjs8
9 files changed, 266 insertions, 263 deletions
diff --git a/package.json b/package.json
index 6af2368..1b265d9 100644
--- a/package.json
+++ b/package.json
@@ -70,6 +70,7 @@
70 "semi": false, 70 "semi": false,
71 "singleQuote": true, 71 "singleQuote": true,
72 "tabWidth": 2, 72 "tabWidth": 2,
73 "trailingComma": "es5" 73 "trailingComma": "all",
74 "semi": true
74 } 75 }
75} 76}
diff --git a/src/Layout.mjs b/src/Layout.mjs
index 9cf2ab9..3a835c9 100644
--- a/src/Layout.mjs
+++ b/src/Layout.mjs
@@ -1,9 +1,9 @@
1import PlainDraggable from "plain-draggable"; 1import PlainDraggable from 'plain-draggable';
2import { onRemove, animateRectTransition } from "./utils"; 2import { onRemove, animateRectTransition } from './utils';
3 3
4export class Layout { 4export class Layout {
5 constructor(options = {}) { 5 constructor(options = {}) {
6 if (!options.name) throw Error("Layout name is not given"); 6 if (!options.name) throw Error('Layout name is not given');
7 this.name = options.name; 7 this.name = options.name;
8 this.enterHandler = options.enterHandler; 8 this.enterHandler = options.enterHandler;
9 this.leaveHandler = options.leaveHandler; 9 this.leaveHandler = options.leaveHandler;
@@ -12,63 +12,63 @@ export class Layout {
12} 12}
13 13
14export class SideBySide extends Layout { 14export class SideBySide extends Layout {
15 name = "side-by-side"; 15 name = 'side-by-side';
16 16
17 enterHandler = ({ container, htmlHolder, showcase }) => { 17 enterHandler = ({ container, htmlHolder, showcase }) => {
18 const bar = document.createElement("div"); 18 const bar = document.createElement('div');
19 bar.className = "bar"; 19 bar.className = 'bar';
20 bar.innerHTML = '<div class="bar-handle"></div>'; 20 bar.innerHTML = '<div class="bar-handle"></div>';
21 const handle = bar.querySelector(".bar-handle"); 21 const handle = bar.querySelector('.bar-handle');
22 container.appendChild(bar); 22 container.appendChild(bar);
23 23
24 // Resize views by value 24 // Resize views by value
25 const resizeByLeft = left => { 25 const resizeByLeft = left => {
26 htmlHolder.style.width = left + "px"; 26 htmlHolder.style.width = left + 'px';
27 showcase.style.width = 27 showcase.style.width =
28 parseFloat(getComputedStyle(container).width) - left + "px"; 28 parseFloat(getComputedStyle(container).width) - left + 'px';
29 }; 29 };
30 30
31 const draggable = new PlainDraggable(bar, { 31 const draggable = new PlainDraggable(bar, {
32 handle: handle, 32 handle: handle,
33 containment: { left: "25%", top: 0, right: "75%", height: 0 }, 33 containment: { left: '25%', top: 0, right: '75%', height: 0 },
34 }); 34 });
35 draggable.draggableCursor = "grab"; 35 draggable.draggableCursor = 'grab';
36 36
37 draggable.onDrag = pos => { 37 draggable.onDrag = pos => {
38 handle.style.transform = "unset"; 38 handle.style.transform = 'unset';
39 resizeByLeft(pos.left); 39 resizeByLeft(pos.left);
40 }; 40 };
41 draggable.onDragEnd = _ => { 41 draggable.onDragEnd = _ => {
42 handle.removeAttribute("style"); 42 handle.removeAttribute('style');
43 }; 43 };
44 44
45 onRemove(bar, () => draggable.remove()); 45 onRemove(bar, () => draggable.remove());
46 }; 46 };
47 47
48 leaveHandler = ({ container }) => { 48 leaveHandler = ({ container }) => {
49 container.querySelector(".bar")?.remove(); 49 container.querySelector('.bar')?.remove();
50 }; 50 };
51} 51}
52 52
53export class Overlay extends Layout { 53export class Overlay extends Layout {
54 name = "overlay"; 54 name = 'overlay';
55 55
56 saveLeftTopAsData = element => { 56 saveLeftTopAsData = element => {
57 const { left, top } = element.getBoundingClientRect(); 57 const { left, top } = element.getBoundingClientRect();
58 element.setAttribute("data-left", left); 58 element.setAttribute('data-left', left);
59 element.setAttribute("data-top", top); 59 element.setAttribute('data-top', top);
60 }; 60 };
61 61
62 addDraggable = element => { 62 addDraggable = element => {
63 // Make sure current element always on top 63 // Make sure current element always on top
64 const siblings = Array.from( 64 const siblings = Array.from(
65 element.parentElement?.querySelectorAll(":scope > *") ?? [], 65 element.parentElement?.querySelectorAll(':scope > *') ?? [],
66 ); 66 );
67 let popTimer = null; 67 let popTimer = null;
68 element.onmouseover = () => { 68 element.onmouseover = () => {
69 popTimer = setTimeout(() => { 69 popTimer = setTimeout(() => {
70 siblings.forEach(e => e.style.removeProperty("z-index")); 70 siblings.forEach(e => e.style.removeProperty('z-index'));
71 element.style.zIndex = "9001"; 71 element.style.zIndex = '9001';
72 }, 200); 72 }, 200);
73 }; 73 };
74 element.onmouseout = () => { 74 element.onmouseout = () => {
@@ -76,9 +76,9 @@ export class Overlay extends Layout {
76 }; 76 };
77 77
78 // Add draggable part 78 // Add draggable part
79 const draggablePart = document.createElement("div"); 79 const draggablePart = document.createElement('div');
80 element.appendChild(draggablePart); 80 element.appendChild(draggablePart);
81 draggablePart.className = "draggable-part"; 81 draggablePart.className = 'draggable-part';
82 draggablePart.innerHTML = '<div class="handle">\u2630</div>'; 82 draggablePart.innerHTML = '<div class="handle">\u2630</div>';
83 83
84 // Add draggable instance 84 // Add draggable instance
@@ -91,16 +91,16 @@ export class Overlay extends Layout {
91 }); 91 });
92 92
93 // FIXME use pure CSS to hide utils 93 // FIXME use pure CSS to hide utils
94 const utils = element.querySelector(".utils"); 94 const utils = element.querySelector('.utils');
95 draggable.onDragStart = () => { 95 draggable.onDragStart = () => {
96 utils.style.display = "none"; 96 utils.style.display = 'none';
97 element.classList.add("drag"); 97 element.classList.add('drag');
98 }; 98 };
99 99
100 draggable.onDragEnd = () => { 100 draggable.onDragEnd = () => {
101 utils.style = ""; 101 utils.style = '';
102 element.classList.remove("drag"); 102 element.classList.remove('drag');
103 element.style.zIndex = "9000"; 103 element.style.zIndex = '9000';
104 }; 104 };
105 105
106 // Reposition draggable instance when resized 106 // Reposition draggable instance when resized
@@ -125,12 +125,12 @@ export class Overlay extends Layout {
125 // Create draggable blocks and set each position by previous one 125 // Create draggable blocks and set each position by previous one
126 let [left, top] = [20, 20]; 126 let [left, top] = [20, 20];
127 blocks.forEach(block => { 127 blocks.forEach(block => {
128 const originLeft = Number(block.getAttribute("data-left")); 128 const originLeft = Number(block.getAttribute('data-left'));
129 const originTop = Number(block.getAttribute("data-top")); 129 const originTop = Number(block.getAttribute('data-top'));
130 130
131 // Create draggable block 131 // Create draggable block
132 const wrapper = document.createElement("div"); 132 const wrapper = document.createElement('div');
133 wrapper.classList.add("draggable-block"); 133 wrapper.classList.add('draggable-block');
134 wrapper.innerHTML = ` 134 wrapper.innerHTML = `
135 <div class="utils"> 135 <div class="utils">
136 <div id="close">\u274C</div> 136 <div id="close">\u274C</div>
@@ -138,18 +138,18 @@ export class Overlay extends Layout {
138 <div id="minus-font-size">\u2796</div> 138 <div id="minus-font-size">\u2796</div>
139 </div> 139 </div>
140 `; 140 `;
141 wrapper.title = "Middle-click to hide block"; 141 wrapper.title = 'Middle-click to hide block';
142 wrapper.onmouseup = e => { 142 wrapper.onmouseup = e => {
143 // Hide block with middle click 143 // Hide block with middle click
144 if (e.button === 1) { 144 if (e.button === 1) {
145 wrapper.classList.add("hide"); 145 wrapper.classList.add('hide');
146 } 146 }
147 }; 147 };
148 148
149 // Set DOMRect for wrapper 149 // Set DOMRect for wrapper
150 wrapper.appendChild(block); 150 wrapper.appendChild(block);
151 wrapper.style.left = left + "px"; 151 wrapper.style.left = left + 'px';
152 wrapper.style.top = top + "px"; 152 wrapper.style.top = top + 'px';
153 htmlHolder.appendChild(wrapper); 153 htmlHolder.appendChild(wrapper);
154 const { width } = wrapper.getBoundingClientRect(); 154 const { width } = wrapper.getBoundingClientRect();
155 left += width + 30; 155 left += width + 30;
@@ -168,7 +168,7 @@ export class Overlay extends Layout {
168 // Trivial case: 168 // Trivial case:
169 // This hack make sure utils remains at the same place even when wrapper resized 169 // This hack make sure utils remains at the same place even when wrapper resized
170 // Prevent DOMRect changes when user clicking plus/minus button many times 170 // Prevent DOMRect changes when user clicking plus/minus button many times
171 const utils = wrapper.querySelector(".utils"); 171 const utils = wrapper.querySelector('.utils');
172 utils.onmouseover = () => { 172 utils.onmouseover = () => {
173 const { left, top } = utils.getBoundingClientRect(); 173 const { left, top } = utils.getBoundingClientRect();
174 utils.style.cssText = `visibility: visible; z-index: 9000; position: fixed; transition: unset; left: ${left}px; top: ${top}px;`; 174 utils.style.cssText = `visibility: visible; z-index: 9000; position: fixed; transition: unset; left: ${left}px; top: ${top}px;`;
@@ -176,20 +176,20 @@ export class Overlay extends Layout {
176 }; 176 };
177 utils.onmouseout = () => { 177 utils.onmouseout = () => {
178 wrapper.appendChild(utils); 178 wrapper.appendChild(utils);
179 utils.removeAttribute("style"); 179 utils.removeAttribute('style');
180 }; 180 };
181 181
182 // Close button 182 // Close button
183 wrapper.querySelector("#close").onclick = () => { 183 wrapper.querySelector('#close').onclick = () => {
184 wrapper.classList.add("hide"); 184 wrapper.classList.add('hide');
185 utils.removeAttribute("style"); 185 utils.removeAttribute('style');
186 }; 186 };
187 // Plus/Minus font-size of content 187 // Plus/Minus font-size of content
188 wrapper.querySelector("#plus-font-size").onclick = () => { 188 wrapper.querySelector('#plus-font-size').onclick = () => {
189 const fontSize = parseFloat(getComputedStyle(block).fontSize) / 16; 189 const fontSize = parseFloat(getComputedStyle(block).fontSize) / 16;
190 block.style.fontSize = `${fontSize + 0.2}rem`; 190 block.style.fontSize = `${fontSize + 0.2}rem`;
191 }; 191 };
192 wrapper.querySelector("#minus-font-size").onclick = () => { 192 wrapper.querySelector('#minus-font-size').onclick = () => {
193 const fontSize = parseFloat(getComputedStyle(block).fontSize) / 16; 193 const fontSize = parseFloat(getComputedStyle(block).fontSize) / 16;
194 block.style.fontSize = `${fontSize - 0.2}rem`; 194 block.style.fontSize = `${fontSize - 0.2}rem`;
195 }; 195 };
@@ -198,7 +198,7 @@ export class Overlay extends Layout {
198 198
199 leaveHandler = ({ htmlHolder, blocks }) => { 199 leaveHandler = ({ htmlHolder, blocks }) => {
200 const resumeFromDraggable = block => { 200 const resumeFromDraggable = block => {
201 const draggableContainer = block.closest(".draggable-block"); 201 const draggableContainer = block.closest('.draggable-block');
202 if (!draggableContainer) return; 202 if (!draggableContainer) return;
203 htmlHolder.appendChild(block); 203 htmlHolder.appendChild(block);
204 draggableContainer.remove(); 204 draggableContainer.remove();
diff --git a/src/MenuItem.mjs b/src/MenuItem.mjs
index 2f9aff4..734c313 100644
--- a/src/MenuItem.mjs
+++ b/src/MenuItem.mjs
@@ -1,8 +1,8 @@
1import { createGeoLink } from "./dumbymap"; 1import { createGeoLink } from './dumbymap';
2 2
3export function nextMap() { 3export function nextMap() {
4 const element = document.createElement("div"); 4 const element = document.createElement('div');
5 element.className = "menu-item"; 5 element.className = 'menu-item';
6 element.innerHTML = 'Next Map <span class="info">(Tab)</span>'; 6 element.innerHTML = 'Next Map <span class="info">(Tab)</span>';
7 element.onclick = () => this.utils.focusNextMap(); 7 element.onclick = () => this.utils.focusNextMap();
8 8
@@ -10,8 +10,8 @@ export function nextMap() {
10} 10}
11 11
12export function nextBlock() { 12export function nextBlock() {
13 const element = document.createElement("div"); 13 const element = document.createElement('div');
14 element.className = "menu-item"; 14 element.className = 'menu-item';
15 element.innerHTML = 'Next Block <span class="info">(n)</span>'; 15 element.innerHTML = 'Next Block <span class="info">(n)</span>';
16 element.onclick = () => this.utils.focusNextBlock(); 16 element.onclick = () => this.utils.focusNextBlock();
17 17
@@ -19,8 +19,8 @@ export function nextBlock() {
19} 19}
20 20
21export function nextLayout() { 21export function nextLayout() {
22 const element = document.createElement("div"); 22 const element = document.createElement('div');
23 element.className = "menu-item"; 23 element.className = 'menu-item';
24 element.innerHTML = 'Next Layout <span class="info">(x)</span>'; 24 element.innerHTML = 'Next Layout <span class="info">(x)</span>';
25 element.onclick = () => this.utils.switchToNextLayout(); 25 element.onclick = () => this.utils.switchToNextLayout();
26 26
@@ -33,9 +33,9 @@ export class GeoLink {
33 } 33 }
34 34
35 createElement = () => { 35 createElement = () => {
36 const element = document.createElement("div"); 36 const element = document.createElement('div');
37 element.className = "menu-item"; 37 element.className = 'menu-item';
38 element.innerText = "Add GeoLink"; 38 element.innerText = 'Add GeoLink';
39 element.onclick = this.addGeoLinkbyRange; 39 element.onclick = this.addGeoLinkbyRange;
40 40
41 return element; 41 return element;
@@ -49,7 +49,7 @@ export class GeoLink {
49 if (!match) return false; 49 if (!match) return false;
50 50
51 const [x, y] = match.slice(1); 51 const [x, y] = match.slice(1);
52 const anchor = document.createElement("a"); 52 const anchor = document.createElement('a');
53 anchor.textContent = content; 53 anchor.textContent = content;
54 // FIXME apply WGS84 54 // FIXME apply WGS84
55 anchor.href = `geo:${y},${x}?xy=${x},${y}`; 55 anchor.href = `geo:${y},${x}?xy=${x},${y}`;
@@ -67,21 +67,21 @@ export class Suggestion {
67 } 67 }
68 68
69 createElement(codemirror) { 69 createElement(codemirror) {
70 const option = document.createElement("div"); 70 const option = document.createElement('div');
71 if (this.text.startsWith("<")) { 71 if (this.text.startsWith('<')) {
72 option.innerHTML = this.text; 72 option.innerHTML = this.text;
73 } else { 73 } else {
74 option.innerText = this.text; 74 option.innerText = this.text;
75 } 75 }
76 option.classList.add("container__suggestion"); 76 option.classList.add('container__suggestion');
77 option.onmouseover = () => { 77 option.onmouseover = () => {
78 Array.from(option.parentElement?.children ?? []).forEach(s => 78 Array.from(option.parentElement?.children ?? []).forEach(s =>
79 s.classList.remove("focus"), 79 s.classList.remove('focus'),
80 ); 80 );
81 option.classList.add("focus"); 81 option.classList.add('focus');
82 }; 82 };
83 option.onmouseout = () => { 83 option.onmouseout = () => {
84 option.classList.remove("focus"); 84 option.classList.remove('focus');
85 }; 85 };
86 option.onclick = () => { 86 option.onclick = () => {
87 const anchor = codemirror.getCursor(); 87 const anchor = codemirror.getCursor();
diff --git a/src/css/dumbymap.css b/src/css/dumbymap.css
index c6782fc..f04b1fb 100644
--- a/src/css/dumbymap.css
+++ b/src/css/dumbymap.css
@@ -101,12 +101,12 @@ root {
101 transform: translate(-50%, -50%); 101 transform: translate(-50%, -50%);
102 } 102 }
103 103
104 &[data-render='fulfilled'][data-report$="\20"]::after { 104 &[data-render='fulfilled'][data-report$='\20']::after {
105 content: '\2714 ' attr(data-report); 105 content: '\2714 ' attr(data-report);
106 animation: 1.5s forwards fade-out cubic-bezier(0.44, 0.18, 0.86, -0.21); 106 animation: 1.5s forwards fade-out cubic-bezier(0.44, 0.18, 0.86, -0.21);
107 } 107 }
108 108
109 &[data-render='unfulfilled'][data-report$="\20"]::after { 109 &[data-render='unfulfilled'][data-report$='\20']::after {
110 content: '\2716 ' attr(data-report); 110 content: '\2716 ' attr(data-report);
111 animation: 2.5s forwards fade-out cubic-bezier(0.44, 0.18, 0.86, -0.21); 111 animation: 2.5s forwards fade-out cubic-bezier(0.44, 0.18, 0.86, -0.21);
112 } 112 }
diff --git a/src/css/style.css b/src/css/style.css
index d98e871..ecc7dd8 100644
--- a/src/css/style.css
+++ b/src/css/style.css
@@ -84,7 +84,7 @@ a {
84 color: #007bff; 84 color: #007bff;
85 text-decoration: none; 85 text-decoration: none;
86 86
87 &[href^=http]::after { 87 &[href^='http']::after {
88 content: ''; 88 content: '';
89 display: inline-block; 89 display: inline-block;
90 width: 11px; 90 width: 11px;
diff --git a/src/dumbyUtils.mjs b/src/dumbyUtils.mjs
index d30145c..cfac00b 100644
--- a/src/dumbyUtils.mjs
+++ b/src/dumbyUtils.mjs
@@ -1,16 +1,16 @@
1export function focusNextMap(reverse = false) { 1export function focusNextMap(reverse = false) {
2 const renderedList = Array.from( 2 const renderedList = Array.from(
3 this.htmlHolder.querySelectorAll("[data-render=fulfilled]"), 3 this.htmlHolder.querySelectorAll('[data-render=fulfilled]'),
4 ); 4 );
5 const mapNum = renderedList.length; 5 const mapNum = renderedList.length;
6 if (mapNum === 0) return; 6 if (mapNum === 0) return;
7 7
8 // Get current focused map element 8 // Get current focused map element
9 const currentFocus = this.container.querySelector(".mapclay.focus"); 9 const currentFocus = this.container.querySelector('.mapclay.focus');
10 10
11 // Remove class name of focus for ALL candidates 11 // Remove class name of focus for ALL candidates
12 // This may trigger animation 12 // This may trigger animation
13 renderedList.forEach(ele => ele.classList.remove("focus")); 13 renderedList.forEach(ele => ele.classList.remove('focus'));
14 14
15 // Get next existing map element 15 // Get next existing map element
16 const padding = reverse ? -1 : 1; 16 const padding = reverse ? -1 : 1;
@@ -19,18 +19,18 @@ export function focusNextMap(reverse = false) {
19 : 0; 19 : 0;
20 nextIndex = (nextIndex + mapNum) % mapNum; 20 nextIndex = (nextIndex + mapNum) % mapNum;
21 const nextFocus = renderedList[nextIndex]; 21 const nextFocus = renderedList[nextIndex];
22 nextFocus.classList.add("focus"); 22 nextFocus.classList.add('focus');
23 23
24 return nextFocus; 24 return nextFocus;
25} 25}
26 26
27export function focusDelay() { 27export function focusDelay() {
28 return window.getComputedStyle(this.showcase).display === "none" ? 50 : 300; 28 return window.getComputedStyle(this.showcase).display === 'none' ? 50 : 300;
29} 29}
30 30
31export function switchToNextLayout(reverse = false) { 31export function switchToNextLayout(reverse = false) {
32 const layouts = this.layouts; 32 const layouts = this.layouts;
33 const currentLayoutName = this.container.getAttribute("data-layout"); 33 const currentLayoutName = this.container.getAttribute('data-layout');
34 const currentIndex = layouts.map(l => l.name).indexOf(currentLayoutName); 34 const currentIndex = layouts.map(l => l.name).indexOf(currentLayoutName);
35 const padding = reverse ? -1 : 1; 35 const padding = reverse ? -1 : 1;
36 const nextIndex = 36 const nextIndex =
@@ -38,7 +38,7 @@ export function switchToNextLayout(reverse = false) {
38 ? 0 38 ? 0
39 : (currentIndex + padding + layouts.length) % layouts.length; 39 : (currentIndex + padding + layouts.length) % layouts.length;
40 const nextLayout = layouts[nextIndex]; 40 const nextLayout = layouts[nextIndex];
41 this.container.setAttribute("data-layout", nextLayout.name); 41 this.container.setAttribute('data-layout', nextLayout.name);
42} 42}
43 43
44export function focusNextBlock(reverse = false) { 44export function focusNextBlock(reverse = false) {
@@ -49,7 +49,7 @@ export function focusNextBlock(reverse = false) {
49 visibilityProperty: true, 49 visibilityProperty: true,
50 }), 50 }),
51 ); 51 );
52 const currentBlock = blocks.find(b => b.classList.contains("focus")); 52 const currentBlock = blocks.find(b => b.classList.contains('focus'));
53 const currentIndex = blocks.indexOf(currentBlock); 53 const currentIndex = blocks.indexOf(currentBlock);
54 const padding = reverse ? -1 : 1; 54 const padding = reverse ? -1 : 1;
55 const nextIndex = 55 const nextIndex =
@@ -57,16 +57,16 @@ export function focusNextBlock(reverse = false) {
57 ? 0 57 ? 0
58 : (currentIndex + padding + blocks.length) % blocks.length; 58 : (currentIndex + padding + blocks.length) % blocks.length;
59 const nextBlock = blocks[nextIndex]; 59 const nextBlock = blocks[nextIndex];
60 blocks.forEach(b => b.classList.remove("focus")); 60 blocks.forEach(b => b.classList.remove('focus'));
61 nextBlock?.classList?.add("focus"); 61 nextBlock?.classList?.add('focus');
62 const scrollBlock = 62 const scrollBlock =
63 nextBlock.getBoundingClientRect().height > 63 nextBlock.getBoundingClientRect().height >
64 nextBlock.parentElement.getBoundingClientRect().height * 0.8 64 nextBlock.parentElement.getBoundingClientRect().height * 0.8
65 ? "nearest" 65 ? 'nearest'
66 : "center"; 66 : 'center';
67 nextBlock.scrollIntoView({ behavior: "smooth", block: scrollBlock }); 67 nextBlock.scrollIntoView({ behavior: 'smooth', block: scrollBlock });
68} 68}
69 69
70export function removeBlockFocus() { 70export function removeBlockFocus() {
71 this.blocks.forEach(b => b.classList.remove("focus")); 71 this.blocks.forEach(b => b.classList.remove('focus'));
72} 72}
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
diff --git a/src/editor.mjs b/src/editor.mjs
index 73177b6..f80ff07 100644
--- a/src/editor.mjs
+++ b/src/editor.mjs
@@ -1,22 +1,22 @@
1/*global EasyMDE*/ 1/*global EasyMDE*/
2/*eslint no-undef: "error"*/ 2/*eslint no-undef: "error"*/
3import { markdown2HTML, generateMaps } from "./dumbymap"; 3import { markdown2HTML, generateMaps } from './dumbymap';
4import { defaultAliases, parseConfigsFromYaml } from "mapclay"; 4import { defaultAliases, parseConfigsFromYaml } from 'mapclay';
5import * as menuItem from "./MenuItem"; 5import * as menuItem from './MenuItem';
6 6
7// Set up Containers {{{ 7// Set up Containers {{{
8 8
9const HtmlContainer = document.querySelector(".DumbyMap"); 9const HtmlContainer = document.querySelector('.DumbyMap');
10const textArea = document.querySelector(".editor textarea"); 10const textArea = document.querySelector('.editor textarea');
11let dumbymap; 11let dumbymap;
12 12
13const toggleEditing = () => { 13const toggleEditing = () => {
14 if (document.body.getAttribute("data-mode") === "editing") { 14 if (document.body.getAttribute('data-mode') === 'editing') {
15 document.body.removeAttribute("data-mode"); 15 document.body.removeAttribute('data-mode');
16 } else { 16 } else {
17 document.body.setAttribute("data-mode", "editing"); 17 document.body.setAttribute('data-mode', 'editing');
18 } 18 }
19 HtmlContainer.setAttribute("data-layout", "normal"); 19 HtmlContainer.setAttribute('data-layout', 'normal');
20}; 20};
21// }}} 21// }}}
22// Set up EasyMDE {{{ 22// Set up EasyMDE {{{
@@ -30,37 +30,37 @@ const editor = new EasyMDE({
30 initialValue: defaultContent, 30 initialValue: defaultContent,
31 autosave: { 31 autosave: {
32 enabled: true, 32 enabled: true,
33 uniqueId: "dumbymap", 33 uniqueId: 'dumbymap',
34 }, 34 },
35 indentWithTabs: false, 35 indentWithTabs: false,
36 lineNumbers: true, 36 lineNumbers: true,
37 promptURLs: true, 37 promptURLs: true,
38 uploadImage: true, 38 uploadImage: true,
39 spellChecker: false, 39 spellChecker: false,
40 toolbarButtonClassPrefix: "mde", 40 toolbarButtonClassPrefix: 'mde',
41 status: false, 41 status: false,
42 shortcuts: { 42 shortcuts: {
43 map: "Ctrl-Alt-M", 43 map: 'Ctrl-Alt-M',
44 debug: "Ctrl-Alt-D", 44 debug: 'Ctrl-Alt-D',
45 toggleUnorderedList: null, 45 toggleUnorderedList: null,
46 toggleOrderedList: null, 46 toggleOrderedList: null,
47 }, 47 },
48 toolbar: [ 48 toolbar: [
49 { 49 {
50 name: "map", 50 name: 'map',
51 title: "Toggle Map Generation", 51 title: 'Toggle Map Generation',
52 text: "🌏", 52 text: '🌏',
53 action: () => toggleEditing(), 53 action: () => toggleEditing(),
54 }, 54 },
55 { 55 {
56 name: "debug", 56 name: 'debug',
57 title: "Save content as URL", 57 title: 'Save content as URL',
58 text: "🤔", 58 text: '🤔',
59 action: () => { 59 action: () => {
60 const state = { content: editor.value() }; 60 const state = { content: editor.value() };
61 window.location.hash = encodeURIComponent(JSON.stringify(state)); 61 window.location.hash = encodeURIComponent(JSON.stringify(state));
62 navigator.clipboard.writeText(window.location.href); 62 navigator.clipboard.writeText(window.location.href);
63 alert("URL copied to clipboard"); 63 alert('URL copied to clipboard');
64 }, 64 },
65 }, 65 },
66 "undo", 66 "undo",
@@ -103,7 +103,7 @@ const getContentFromHash = hash => {
103}; 103};
104 104
105const initialState = getStateFromHash(window.location.hash); 105const initialState = getStateFromHash(window.location.hash);
106window.location.hash = ""; 106window.location.hash = '';
107const contentFromHash = initialState.content; 107const contentFromHash = initialState.content;
108 108
109// Seems like autosave would overwrite initialValue, set content from hash here 109// Seems like autosave would overwrite initialValue, set content from hash here
@@ -131,9 +131,9 @@ const addClassToCodeLines = () => {
131 if (line.text.match(/^[\u0060]{3}/)) { 131 if (line.text.match(/^[\u0060]{3}/)) {
132 insideCodeBlock = !insideCodeBlock; 132 insideCodeBlock = !insideCodeBlock;
133 } else if (insideCodeBlock) { 133 } else if (insideCodeBlock) {
134 cm.addLineClass(index, "text", "inside-code-block"); 134 cm.addLineClass(index, 'text', 'inside-code-block');
135 } else { 135 } else {
136 cm.removeLineClass(index, "text", "inside-code-block"); 136 cm.removeLineClass(index, 'text', 'inside-code-block');
137 } 137 }
138 }); 138 });
139}; 139};
@@ -141,29 +141,29 @@ addClassToCodeLines();
141 141
142const completeForCodeBlock = change => { 142const completeForCodeBlock = change => {
143 const line = change.to.line; 143 const line = change.to.line;
144 if (change.origin === "+input") { 144 if (change.origin === '+input') {
145 const text = change.text[0]; 145 const text = change.text[0];
146 146
147 // Completion for YAML doc separator 147 // Completion for YAML doc separator
148 if ( 148 if (
149 text === "-" && 149 text === '-' &&
150 change.to.ch === 0 && 150 change.to.ch === 0 &&
151 insideCodeblockForMap(cm.getCursor()) 151 insideCodeblockForMap(cm.getCursor())
152 ) { 152 ) {
153 cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 1 }); 153 cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 1 });
154 cm.replaceSelection(text.repeat(3) + "\n"); 154 cm.replaceSelection(text.repeat(3) + '\n');
155 } 155 }
156 156
157 // Completion for Code fence 157 // Completion for Code fence
158 if (text === "`" && change.to.ch === 0) { 158 if (text === '`' && change.to.ch === 0) {
159 cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 1 }); 159 cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 1 });
160 cm.replaceSelection(text.repeat(3)); 160 cm.replaceSelection(text.repeat(3));
161 const numberOfFences = cm 161 const numberOfFences = cm
162 .getValue() 162 .getValue()
163 .split("\n") 163 .split('\n')
164 .filter(line => line.match(/[\u0060]{3}/)).length; 164 .filter(line => line.match(/[\u0060]{3}/)).length;
165 if (numberOfFences % 2 === 1) { 165 if (numberOfFences % 2 === 1) {
166 cm.replaceSelection("map\n\n```"); 166 cm.replaceSelection('map\n\n```');
167 cm.setCursor({ line: line + 1 }); 167 cm.setCursor({ line: line + 1 });
168 } 168 }
169 } 169 }
@@ -171,11 +171,11 @@ const completeForCodeBlock = change => {
171 171
172 // For YAML doc separator, <hr> and code fence 172 // For YAML doc separator, <hr> and code fence
173 // Auto delete to start of line 173 // Auto delete to start of line
174 if (change.origin === "+delete") { 174 if (change.origin === '+delete') {
175 const match = change.removed[0].match(/^[-\u0060]$/)?.at(0); 175 const match = change.removed[0].match(/^[-\u0060]$/)?.at(0);
176 if (match && cm.getLine(line) === match.repeat(2) && match) { 176 if (match && cm.getLine(line) === match.repeat(2) && match) {
177 cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 2 }); 177 cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 2 });
178 cm.replaceSelection(""); 178 cm.replaceSelection('');
179 } 179 }
180 } 180 }
181}; 181};
@@ -202,23 +202,23 @@ const updateDumbyMap = () => {
202updateDumbyMap(); 202updateDumbyMap();
203 203
204// Re-render HTML by editor content 204// Re-render HTML by editor content
205cm.on("change", (_, change) => { 205cm.on('change', (_, change) => {
206 updateDumbyMap(); 206 updateDumbyMap();
207 addClassToCodeLines(); 207 addClassToCodeLines();
208 completeForCodeBlock(change); 208 completeForCodeBlock(change);
209}); 209});
210 210
211// Set class for focus 211// Set class for focus
212cm.on("focus", () => { 212cm.on('focus', () => {
213 cm.getWrapperElement().classList.add("focus"); 213 cm.getWrapperElement().classList.add('focus');
214 HtmlContainer.classList.remove("focus"); 214 HtmlContainer.classList.remove('focus');
215}); 215});
216 216
217cm.on("beforeChange", (_, change) => { 217cm.on('beforeChange', (_, change) => {
218 const line = change.to.line; 218 const line = change.to.line;
219 // Don't allow more content after YAML doc separator 219 // Don't allow more content after YAML doc separator
220 if (change.origin.match(/^(\+input|paste)$/)) { 220 if (change.origin.match(/^(\+input|paste)$/)) {
221 if (cm.getLine(line) === "---" && change.text[0] !== "") { 221 if (cm.getLine(line) === '---' && change.text[0] !== '') {
222 change.cancel(); 222 change.cancel();
223 } 223 }
224 } 224 }
@@ -239,9 +239,9 @@ window.onhashchange = () => {
239// }}} 239// }}}
240// Completion in Code Blok {{{ 240// Completion in Code Blok {{{
241// Elements about suggestions {{{ 241// Elements about suggestions {{{
242const menu = document.createElement("div"); 242const menu = document.createElement('div');
243menu.id = "menu"; 243menu.id = 'menu';
244menu.onclick = () => (menu.style.display = "none"); 244menu.onclick = () => (menu.style.display = 'none');
245document.body.append(menu); 245document.body.append(menu);
246 246
247const rendererOptions = {}; 247const rendererOptions = {};
@@ -249,7 +249,7 @@ const rendererOptions = {};
249// }}} 249// }}}
250// Aliases for map options {{{ 250// Aliases for map options {{{
251const aliasesForMapOptions = {}; 251const aliasesForMapOptions = {};
252const defaultApply = "./dist/default.yml"; 252const defaultApply = './dist/default.yml';
253fetch(defaultApply) 253fetch(defaultApply)
254 .then(res => res.text()) 254 .then(res => res.text())
255 .then(rawText => { 255 .then(rawText => {
@@ -269,9 +269,9 @@ const insideCodeblockForMap = anchor => {
269 let line = anchor.line - 1; 269 let line = anchor.line - 1;
270 while (line >= 0) { 270 while (line >= 0) {
271 const content = cm.getLine(line); 271 const content = cm.getLine(line);
272 if (content === "```map") { 272 if (content === '```map') {
273 return true; 273 return true;
274 } else if (content === "```") { 274 } else if (content === '```') {
275 return false; 275 return false;
276 } 276 }
277 line = line - 1; 277 line = line - 1;
@@ -331,7 +331,7 @@ const getSuggestionsForOptions = (optionTyped, validOptions) => {
331 return suggestOptions.map( 331 return suggestOptions.map(
332 o => 332 o =>
333 new menuItem.Suggestion({ 333 new menuItem.Suggestion({
334 text: `<span>${o.valueOf()}</span><span class='info' title="${o.desc ?? ""}">ⓘ</span>`, 334 text: `<span>${o.valueOf()}</span><span class='info' title="${o.desc ?? ''}">ⓘ</span>`,
335 replace: `${o.valueOf()}: `, 335 replace: `${o.valueOf()}: `,
336 }), 336 }),
337 ); 337 );
@@ -347,7 +347,7 @@ const getSuggestionFromMapOption = option => {
347 347
348 return new menuItem.Suggestion({ 348 return new menuItem.Suggestion({
349 text: text, 349 text: text,
350 replace: `${option.valueOf()}: ${option.example ?? ""}`, 350 replace: `${option.valueOf()}: ${option.example ?? ''}`,
351 }); 351 });
352}; 352};
353// }}} 353// }}}
@@ -355,7 +355,7 @@ const getSuggestionFromMapOption = option => {
355const getSuggestionsFromAliases = option => 355const getSuggestionsFromAliases = option =>
356 Object.entries(aliasesForMapOptions[option.valueOf()] ?? {})?.map(record => { 356 Object.entries(aliasesForMapOptions[option.valueOf()] ?? {})?.map(record => {
357 const [alias, value] = record; 357 const [alias, value] = record;
358 const valueString = JSON.stringify(value).replaceAll('"', ""); 358 const valueString = JSON.stringify(value).replaceAll('"', '');
359 return new menuItem.Suggestion({ 359 return new menuItem.Suggestion({
360 text: `<span>${alias}</span><span class="truncate" style="color: gray">${valueString}</span>`, 360 text: `<span>${alias}</span><span class="truncate" style="color: gray">${valueString}</span>`,
361 replace: `${option.valueOf()}: ${valueString}`, 361 replace: `${option.valueOf()}: ${valueString}`,
@@ -391,17 +391,17 @@ const getSuggestions = anchor => {
391 .markText( 391 .markText(
392 { ...anchor, ch: 0 }, 392 { ...anchor, ch: 0 },
393 { ...anchor, ch: text.length }, 393 { ...anchor, ch: text.length },
394 { className: "invalid-input" }, 394 { className: 'invalid-input' },
395 ); 395 );
396 396
397 // Check if "use: <renderer>" is set 397 // Check if "use: <renderer>" is set
398 const lineWithRenderer = getLineWithRenderer(anchor); 398 const lineWithRenderer = getLineWithRenderer(anchor);
399 const renderer = lineWithRenderer 399 const renderer = lineWithRenderer
400 ? cm.getLine(lineWithRenderer).split(" ")[1] 400 ? cm.getLine(lineWithRenderer).split(' ')[1]
401 : null; 401 : null;
402 if (renderer && anchor.line !== lineWithRenderer) { 402 if (renderer && anchor.line !== lineWithRenderer) {
403 // Do not check properties 403 // Do not check properties
404 if (text.startsWith(" ")) return []; 404 if (text.startsWith(' ')) return [];
405 405
406 // If no valid options for current used renderer, go get it! 406 // If no valid options for current used renderer, go get it!
407 const validOptions = rendererOptions[renderer]; 407 const validOptions = rendererOptions[renderer];
@@ -426,7 +426,7 @@ const getSuggestions = anchor => {
426 } 426 }
427 427
428 // If input is "key:value" (no space left after colon), then it is invalid 428 // If input is "key:value" (no space left after colon), then it is invalid
429 const isKeyFinished = text.includes(":"); 429 const isKeyFinished = text.includes(':');
430 const isValidKeyValue = text.match(/^[^:]+:\s+/); 430 const isValidKeyValue = text.match(/^[^:]+:\s+/);
431 if (isKeyFinished && !isValidKeyValue) { 431 if (isKeyFinished && !isValidKeyValue) {
432 markInputIsInvalid(); 432 markInputIsInvalid();
@@ -434,7 +434,7 @@ const getSuggestions = anchor => {
434 } 434 }
435 435
436 // If user is typing option 436 // If user is typing option
437 const keyTyped = text.split(":")[0].trim(); 437 const keyTyped = text.split(':')[0].trim();
438 if (!isKeyFinished) { 438 if (!isKeyFinished) {
439 markInputIsInvalid(); 439 markInputIsInvalid();
440 return getSuggestionsForOptions(keyTyped, validOptions); 440 return getSuggestionsForOptions(keyTyped, validOptions);
@@ -447,7 +447,7 @@ const getSuggestions = anchor => {
447 } 447 }
448 448
449 if (isKeyFinished && matchedOption) { 449 if (isKeyFinished && matchedOption) {
450 const valueTyped = text.substring(text.indexOf(":") + 1).trim(); 450 const valueTyped = text.substring(text.indexOf(':') + 1).trim();
451 const isValidValue = matchedOption.isValid(valueTyped); 451 const isValidValue = matchedOption.isValid(valueTyped);
452 if (!valueTyped) { 452 if (!valueTyped) {
453 return [ 453 return [
@@ -465,8 +465,8 @@ const getSuggestions = anchor => {
465 const rendererSuggestions = Object.entries(defaultAliases.use) 465 const rendererSuggestions = Object.entries(defaultAliases.use)
466 .filter(([renderer]) => { 466 .filter(([renderer]) => {
467 const suggestion = `use: ${renderer}`; 467 const suggestion = `use: ${renderer}`;
468 const suggestionPattern = suggestion.replace(" ", "").toLowerCase(); 468 const suggestionPattern = suggestion.replace(' ', '').toLowerCase();
469 const textPattern = text.replace(" ", "").toLowerCase(); 469 const textPattern = text.replace(' ', '').toLowerCase();
470 return suggestion !== text && suggestionPattern.includes(textPattern); 470 return suggestion !== text && suggestionPattern.includes(textPattern);
471 }) 471 })
472 .map( 472 .map(
@@ -484,55 +484,55 @@ const getSuggestions = anchor => {
484// {{{ FUNCTION: Show element about suggestions 484// {{{ FUNCTION: Show element about suggestions
485const addSuggestions = (anchor, suggestions) => { 485const addSuggestions = (anchor, suggestions) => {
486 if (suggestions.length === 0) { 486 if (suggestions.length === 0) {
487 menu.style.display = "none"; 487 menu.style.display = 'none';
488 return; 488 return;
489 } else { 489 } else {
490 menu.style.display = "block"; 490 menu.style.display = 'block';
491 } 491 }
492 492
493 menu.innerHTML = ""; 493 menu.innerHTML = '';
494 suggestions 494 suggestions
495 .map(s => s.createElement(cm)) 495 .map(s => s.createElement(cm))
496 .forEach(option => menu.appendChild(option)); 496 .forEach(option => menu.appendChild(option));
497 497
498 const widgetAnchor = document.createElement("div"); 498 const widgetAnchor = document.createElement('div');
499 cm.addWidget(anchor, widgetAnchor, true); 499 cm.addWidget(anchor, widgetAnchor, true);
500 const rect = widgetAnchor.getBoundingClientRect(); 500 const rect = widgetAnchor.getBoundingClientRect();
501 menu.style.left = `calc(${rect.left}px + 2rem)`; 501 menu.style.left = `calc(${rect.left}px + 2rem)`;
502 menu.style.top = `calc(${rect.bottom}px + 1rem)`; 502 menu.style.top = `calc(${rect.bottom}px + 1rem)`;
503 menu.style.maxWidth = `calc(${window.innerWidth}px - ${rect.x}px - 3rem)`; 503 menu.style.maxWidth = `calc(${window.innerWidth}px - ${rect.x}px - 3rem)`;
504 menu.style.display = "block"; 504 menu.style.display = 'block';
505}; 505};
506// }}} 506// }}}
507// EVENT: Suggests for current selection {{{ 507// EVENT: Suggests for current selection {{{
508// FIXME Dont show suggestion when selecting multiple chars 508// FIXME Dont show suggestion when selecting multiple chars
509cm.on("cursorActivity", _ => { 509cm.on('cursorActivity', _ => {
510 menu.style.display = "none"; 510 menu.style.display = 'none';
511 const anchor = cm.getCursor(); 511 const anchor = cm.getCursor();
512 512
513 if (insideCodeblockForMap(anchor)) { 513 if (insideCodeblockForMap(anchor)) {
514 handleTypingInCodeBlock(anchor); 514 handleTypingInCodeBlock(anchor);
515 } 515 }
516}); 516});
517cm.on("blur", () => { 517cm.on('blur', () => {
518 menu.style.display = "none"; 518 menu.style.display = 'none';
519 cm.getWrapperElement().classList.remove("focus"); 519 cm.getWrapperElement().classList.remove('focus');
520 HtmlContainer.classList.add("focus"); 520 HtmlContainer.classList.add('focus');
521}); 521});
522// }}} 522// }}}
523// EVENT: keydown for suggestions {{{ 523// EVENT: keydown for suggestions {{{
524const keyForSuggestions = ["Tab", "Enter", "Escape"]; 524const keyForSuggestions = ['Tab', 'Enter', 'Escape'];
525cm.on("keydown", (_, e) => { 525cm.on('keydown', (_, e) => {
526 if ( 526 if (
527 !cm.hasFocus || 527 !cm.hasFocus ||
528 !keyForSuggestions.includes(e.key) || 528 !keyForSuggestions.includes(e.key) ||
529 menu.style.display === "none" 529 menu.style.display === 'none'
530 ) 530 )
531 return; 531 return;
532 532
533 // Directly add a newline when no suggestion is selected 533 // Directly add a newline when no suggestion is selected
534 const currentSuggestion = menu.querySelector(".container__suggestion.focus"); 534 const currentSuggestion = menu.querySelector('.container__suggestion.focus');
535 if (!currentSuggestion && e.key === "Enter") return; 535 if (!currentSuggestion && e.key === 'Enter') return;
536 536
537 // Override default behavior 537 // Override default behavior
538 e.preventDefault(); 538 e.preventDefault();
@@ -540,25 +540,25 @@ cm.on("keydown", (_, e) => {
540 // Suggestion when pressing Tab or Shift + Tab 540 // Suggestion when pressing Tab or Shift + Tab
541 const nextSuggestion = 541 const nextSuggestion =
542 currentSuggestion?.nextSibling ?? 542 currentSuggestion?.nextSibling ??
543 menu.querySelector(".container__suggestion:first-child"); 543 menu.querySelector('.container__suggestion:first-child');
544 const previousSuggestion = 544 const previousSuggestion =
545 currentSuggestion?.previousSibling ?? 545 currentSuggestion?.previousSibling ??
546 menu.querySelector(".container__suggestion:last-child"); 546 menu.querySelector('.container__suggestion:last-child');
547 const focusSuggestion = e.shiftKey ? previousSuggestion : nextSuggestion; 547 const focusSuggestion = e.shiftKey ? previousSuggestion : nextSuggestion;
548 548
549 // Current editor selection state 549 // Current editor selection state
550 const anchor = cm.getCursor(); 550 const anchor = cm.getCursor();
551 switch (e.key) { 551 switch (e.key) {
552 case "Tab": 552 case 'Tab':
553 Array.from(menu.children).forEach(s => s.classList.remove("focus")); 553 Array.from(menu.children).forEach(s => s.classList.remove('focus'));
554 focusSuggestion.classList.add("focus"); 554 focusSuggestion.classList.add('focus');
555 focusSuggestion.scrollIntoView({ behavior: "smooth", block: "nearest" }); 555 focusSuggestion.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
556 break; 556 break;
557 case "Enter": 557 case 'Enter':
558 currentSuggestion.onclick(); 558 currentSuggestion.onclick();
559 break; 559 break;
560 case "Escape": 560 case 'Escape':
561 menu.style.display = "none"; 561 menu.style.display = 'none';
562 // Focus editor again 562 // Focus editor again
563 setTimeout(() => cm.focus() && cm.setCursor(anchor), 100); 563 setTimeout(() => cm.focus() && cm.setCursor(anchor), 100);
564 break; 564 break;
@@ -566,34 +566,34 @@ cm.on("keydown", (_, e) => {
566}); 566});
567 567
568document.onkeydown = e => { 568document.onkeydown = e => {
569 if (e.altKey && e.ctrlKey && e.key === "m") { 569 if (e.altKey && e.ctrlKey && e.key === 'm') {
570 toggleEditing(); 570 toggleEditing();
571 e.preventDefault(); 571 e.preventDefault();
572 return null; 572 return null;
573 } 573 }
574 574
575 if (!cm.hasFocus()) { 575 if (!cm.hasFocus()) {
576 if (e.key === "F1") { 576 if (e.key === 'F1') {
577 e.preventDefault(); 577 e.preventDefault();
578 cm.focus(); 578 cm.focus();
579 } 579 }
580 if (e.key === "Tab") { 580 if (e.key === 'Tab') {
581 e.preventDefault(); 581 e.preventDefault();
582 dumbymap.utils.focusNextMap(e.shiftKey); 582 dumbymap.utils.focusNextMap(e.shiftKey);
583 } 583 }
584 if (e.key === "x" || e.key === "X") { 584 if (e.key === 'x' || e.key === 'X') {
585 e.preventDefault(); 585 e.preventDefault();
586 dumbymap.utils.switchToNextLayout(e.shiftKey); 586 dumbymap.utils.switchToNextLayout(e.shiftKey);
587 } 587 }
588 if (e.key === "n") { 588 if (e.key === 'n') {
589 e.preventDefault(); 589 e.preventDefault();
590 dumbymap.utils.focusNextBlock(); 590 dumbymap.utils.focusNextBlock();
591 } 591 }
592 if (e.key === "p") { 592 if (e.key === 'p') {
593 e.preventDefault(); 593 e.preventDefault();
594 dumbymap.utils.focusNextBlock(true); 594 dumbymap.utils.focusNextBlock(true);
595 } 595 }
596 if (e.key === "Escape") { 596 if (e.key === 'Escape') {
597 e.preventDefault(); 597 e.preventDefault();
598 dumbymap.utils.removeBlockFocus(); 598 dumbymap.utils.removeBlockFocus();
599 } 599 }
@@ -604,15 +604,15 @@ document.onkeydown = e => {
604// }}} 604// }}}
605// Layout Switch {{{ 605// Layout Switch {{{
606const layoutObserver = new MutationObserver(() => { 606const layoutObserver = new MutationObserver(() => {
607 const layout = HtmlContainer.getAttribute("data-layout"); 607 const layout = HtmlContainer.getAttribute('data-layout');
608 if (layout !== "normal") { 608 if (layout !== 'normal') {
609 document.body.removeAttribute("data-mode"); 609 document.body.removeAttribute('data-mode');
610 } 610 }
611}); 611});
612 612
613layoutObserver.observe(HtmlContainer, { 613layoutObserver.observe(HtmlContainer, {
614 attributes: true, 614 attributes: true,
615 attributeFilter: ["data-layout"], 615 attributeFilter: ['data-layout'],
616 attributeOldValue: true, 616 attributeOldValue: true,
617}); 617});
618// }}} 618// }}}
@@ -624,7 +624,7 @@ document.oncontextmenu = e => {
624 const range = selection.getRangeAt(0); 624 const range = selection.getRangeAt(0);
625 if (selection) { 625 if (selection) {
626 e.preventDefault(); 626 e.preventDefault();
627 menu.innerHTML = ""; 627 menu.innerHTML = '';
628 menu.style.cssText = `display: block; left: ${e.clientX + 10}px; top: ${e.clientY + 5}px;`; 628 menu.style.cssText = `display: block; left: ${e.clientX + 10}px; top: ${e.clientY + 5}px;`;
629 const addGeoLink = new menuItem.GeoLink({ range }); 629 const addGeoLink = new menuItem.GeoLink({ range });
630 menu.appendChild(addGeoLink.createElement()); 630 menu.appendChild(addGeoLink.createElement());
@@ -635,7 +635,7 @@ document.oncontextmenu = e => {
635}; 635};
636 636
637const actionOutsideMenu = e => { 637const actionOutsideMenu = e => {
638 if (menu.style.display === "none" || cm.hasFocus()) return; 638 if (menu.style.display === 'none' || cm.hasFocus()) return;
639 const rect = menu.getBoundingClientRect(); 639 const rect = menu.getBoundingClientRect();
640 if ( 640 if (
641 e.clientX < rect.left || 641 e.clientX < rect.left ||
@@ -643,11 +643,11 @@ const actionOutsideMenu = e => {
643 e.clientY < rect.top || 643 e.clientY < rect.top ||
644 e.clientY > rect.top + rect.height 644 e.clientY > rect.top + rect.height
645 ) { 645 ) {
646 menu.style.display = "none"; 646 menu.style.display = 'none';
647 } 647 }
648}; 648};
649 649
650document.addEventListener("click", actionOutsideMenu); 650document.addEventListener('click', actionOutsideMenu);
651 651
652// }}} 652// }}}
653 653
diff --git a/src/utils.mjs b/src/utils.mjs
index ad9131e..3824672 100644
--- a/src/utils.mjs
+++ b/src/utils.mjs
@@ -6,7 +6,7 @@
6 */ 6 */
7export const onRemove = (element, callback) => { 7export const onRemove = (element, callback) => {
8 const parent = element.parentNode; 8 const parent = element.parentNode;
9 if (!parent) throw new Error("The node must already be attached"); 9 if (!parent) throw new Error('The node must already be attached');
10 10
11 const obs = new MutationObserver(mutations => { 11 const obs = new MutationObserver(mutations => {
12 for (const mutation of mutations) { 12 for (const mutation of mutations) {
@@ -33,7 +33,7 @@ export const onRemove = (element, callback) => {
33 */ 33 */
34export const animateRectTransition = (element, rect, options = {}) => { 34export const animateRectTransition = (element, rect, options = {}) => {
35 if (!element.parentElement) 35 if (!element.parentElement)
36 throw new Error("The node must already be attached"); 36 throw new Error('The node must already be attached');
37 37
38 const { width: w1, height: h1, left: x1, top: y1 } = rect; 38 const { width: w1, height: h1, left: x1, top: y1 } = rect;
39 const { 39 const {
@@ -62,7 +62,7 @@ export const animateRectTransition = (element, rect, options = {}) => {
62 62
63 return element.animate(keyframes, { 63 return element.animate(keyframes, {
64 duration: options.duration ?? 500, 64 duration: options.duration ?? 500,
65 easing: "ease-in-out", 65 easing: 'ease-in-out',
66 }); 66 });
67}; 67};
68 68
@@ -82,7 +82,7 @@ export function throttle(func, delay) {
82 82
83 timerFlag = setTimeout( 83 timerFlag = setTimeout(
84 () => (timerFlag = null), 84 () => (timerFlag = null),
85 typeof delay === "function" ? delay.call(context) : delay, 85 typeof delay === 'function' ? delay.call(context) : delay,
86 ); 86 );
87 87
88 return func.call(context, ...args); 88 return func.call(context, ...args);