diff options
author | Hsieh Chin Fan <pham@topo.tw> | 2024-09-28 17:47:52 +0800 |
---|---|---|
committer | Hsieh Chin Fan <pham@topo.tw> | 2024-09-28 17:47:52 +0800 |
commit | 933eba7dc3bdc979fefadd47388b20b8360e1d6b (patch) | |
tree | 9b88908d88a025aaaa1d70306afde8e49fedab27 | |
parent | 5a64dfc8bb8169e72b7c1164f18c6912e9e3576e (diff) |
style: prettier
-rw-r--r-- | package.json | 3 | ||||
-rw-r--r-- | src/Layout.mjs | 240 | ||||
-rw-r--r-- | src/MenuItem.mjs | 102 | ||||
-rw-r--r-- | src/css/dumbymap.css | 83 | ||||
-rw-r--r-- | src/css/index.css | 4 | ||||
-rw-r--r-- | src/css/style.css | 36 | ||||
-rw-r--r-- | src/dumbyUtils.mjs | 86 | ||||
-rw-r--r-- | src/dumbymap.mjs | 591 | ||||
-rw-r--r-- | src/editor.mjs | 655 | ||||
-rw-r--r-- | src/utils.mjs | 52 |
10 files changed, 983 insertions, 869 deletions
diff --git a/package.json b/package.json index e55d489..55a032b 100644 --- a/package.json +++ b/package.json | |||
@@ -63,5 +63,8 @@ | |||
63 | "repository": { | 63 | "repository": { |
64 | "type": "git", | 64 | "type": "git", |
65 | "url": "https://github.com/outdoorsafetylab/dumbymap" | 65 | "url": "https://github.com/outdoorsafetylab/dumbymap" |
66 | }, | ||
67 | "prettier": { | ||
68 | "arrowParens": "avoid" | ||
66 | } | 69 | } |
67 | } | 70 | } |
diff --git a/src/Layout.mjs b/src/Layout.mjs index abefd4d..9cf2ab9 100644 --- a/src/Layout.mjs +++ b/src/Layout.mjs | |||
@@ -1,208 +1,208 @@ | |||
1 | import PlainDraggable from 'plain-draggable' | 1 | import PlainDraggable from "plain-draggable"; |
2 | import { onRemove, animateRectTransition } from './utils' | 2 | import { onRemove, animateRectTransition } from "./utils"; |
3 | 3 | ||
4 | export class Layout { | 4 | export 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; |
10 | } | 10 | } |
11 | valueOf = () => this.name | 11 | valueOf = () => this.name; |
12 | } | 12 | } |
13 | 13 | ||
14 | export class SideBySide extends Layout { | 14 | export 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 = (parseFloat(getComputedStyle(container).width) - left) + "px" | 27 | showcase.style.width = |
28 | } | 28 | parseFloat(getComputedStyle(container).width) - left + "px"; |
29 | }; | ||
29 | 30 | ||
30 | const draggable = new PlainDraggable(bar, { | 31 | const draggable = new PlainDraggable(bar, { |
31 | handle: handle, | 32 | handle: handle, |
32 | containment: { left: '25%', top: 0, right: '75%', height: 0 }, | 33 | containment: { left: "25%", top: 0, right: "75%", height: 0 }, |
33 | }) | 34 | }); |
34 | draggable.draggableCursor = "grab" | 35 | draggable.draggableCursor = "grab"; |
35 | 36 | ||
36 | draggable.onDrag = (pos) => { | 37 | draggable.onDrag = pos => { |
37 | handle.style.transform = 'unset' | 38 | handle.style.transform = "unset"; |
38 | resizeByLeft(pos.left) | 39 | resizeByLeft(pos.left); |
39 | } | 40 | }; |
40 | draggable.onDragEnd = (_) => { | 41 | draggable.onDragEnd = _ => { |
41 | handle.removeAttribute('style') | 42 | handle.removeAttribute("style"); |
42 | } | 43 | }; |
43 | 44 | ||
44 | onRemove(bar, () => draggable.remove()) | 45 | onRemove(bar, () => draggable.remove()); |
45 | } | 46 | }; |
46 | 47 | ||
47 | leaveHandler = ({ container }) => { | 48 | leaveHandler = ({ container }) => { |
48 | container.querySelector('.bar')?.remove() | 49 | container.querySelector(".bar")?.remove(); |
49 | } | 50 | }; |
50 | } | 51 | } |
51 | 52 | ||
52 | export class Overlay extends Layout { | 53 | export class Overlay extends Layout { |
53 | name = "overlay" | 54 | name = "overlay"; |
54 | 55 | ||
55 | saveLeftTopAsData = (element) => { | 56 | saveLeftTopAsData = element => { |
56 | const { left, top } = element.getBoundingClientRect() | 57 | const { left, top } = element.getBoundingClientRect(); |
57 | element.setAttribute('data-left', left) | 58 | element.setAttribute("data-left", left); |
58 | element.setAttribute('data-top', top) | 59 | element.setAttribute("data-top", top); |
59 | } | 60 | }; |
60 | 61 | ||
61 | addDraggable = (element) => { | 62 | addDraggable = element => { |
62 | // Make sure current element always on top | 63 | // Make sure current element always on top |
63 | const siblings = Array.from(element.parentElement?.querySelectorAll(':scope > *') ?? []) | 64 | const siblings = Array.from( |
64 | let popTimer = null | 65 | element.parentElement?.querySelectorAll(":scope > *") ?? [], |
66 | ); | ||
67 | let popTimer = null; | ||
65 | element.onmouseover = () => { | 68 | element.onmouseover = () => { |
66 | popTimer = setTimeout(() => { | 69 | popTimer = setTimeout(() => { |
67 | siblings.forEach(e => e.style.removeProperty('z-index')) | 70 | siblings.forEach(e => e.style.removeProperty("z-index")); |
68 | element.style.zIndex = '9001' | 71 | element.style.zIndex = "9001"; |
69 | }, 200) | 72 | }, 200); |
70 | } | 73 | }; |
71 | element.onmouseout = () => { | 74 | element.onmouseout = () => { |
72 | clearTimeout(popTimer) | 75 | clearTimeout(popTimer); |
73 | } | 76 | }; |
74 | 77 | ||
75 | // Add draggable part | 78 | // Add draggable part |
76 | const draggablePart = document.createElement('div') | 79 | const draggablePart = document.createElement("div"); |
77 | element.appendChild(draggablePart) | 80 | element.appendChild(draggablePart); |
78 | draggablePart.className = 'draggable-part' | 81 | draggablePart.className = "draggable-part"; |
79 | draggablePart.innerHTML = '<div class="handle">\u2630</div>' | 82 | draggablePart.innerHTML = '<div class="handle">\u2630</div>'; |
80 | 83 | ||
81 | // Add draggable instance | 84 | // Add draggable instance |
82 | const { left, top } = element.getBoundingClientRect() | 85 | const { left, top } = element.getBoundingClientRect(); |
83 | const draggable = new PlainDraggable(element, { | 86 | const draggable = new PlainDraggable(element, { |
84 | top: top, | 87 | top: top, |
85 | left: left, | 88 | left: left, |
86 | handle: draggablePart, | 89 | handle: draggablePart, |
87 | snap: { x: { step: 20 }, y: { step: 20 } }, | 90 | snap: { x: { step: 20 }, y: { step: 20 } }, |
88 | }) | 91 | }); |
89 | 92 | ||
90 | // FIXME use pure CSS to hide utils | 93 | // FIXME use pure CSS to hide utils |
91 | const utils = element.querySelector('.utils') | 94 | const utils = element.querySelector(".utils"); |
92 | draggable.onDragStart = () => { | 95 | draggable.onDragStart = () => { |
93 | utils.style.display = 'none' | 96 | utils.style.display = "none"; |
94 | element.classList.add('drag') | 97 | element.classList.add("drag"); |
95 | } | 98 | }; |
96 | 99 | ||
97 | draggable.onDragEnd = () => { | 100 | draggable.onDragEnd = () => { |
98 | utils.style = '' | 101 | utils.style = ""; |
99 | element.classList.remove('drag') | 102 | element.classList.remove("drag"); |
100 | element.style.zIndex = '9000' | 103 | element.style.zIndex = "9000"; |
101 | } | 104 | }; |
102 | 105 | ||
103 | // Reposition draggable instance when resized | 106 | // Reposition draggable instance when resized |
104 | new ResizeObserver(() => { | 107 | new ResizeObserver(() => { |
105 | try { | 108 | try { |
106 | draggable.position(); | 109 | draggable.position(); |
107 | } catch (_) { | 110 | } catch (_) { |
108 | null | 111 | null; |
109 | } | 112 | } |
110 | }).observe(element); | 113 | }).observe(element); |
111 | 114 | ||
112 | // Callback for remove | 115 | // Callback for remove |
113 | onRemove(element, () => { | 116 | onRemove(element, () => { |
114 | draggable.remove() | 117 | draggable.remove(); |
115 | }) | 118 | }); |
116 | } | 119 | }; |
117 | 120 | ||
118 | enterHandler = ({ htmlHolder, blocks }) => { | 121 | enterHandler = ({ htmlHolder, blocks }) => { |
119 | // FIXME It is weird rect from this method and this scope are different... | 122 | // FIXME It is weird rect from this method and this scope are different... |
120 | blocks.forEach(this.saveLeftTopAsData) | 123 | blocks.forEach(this.saveLeftTopAsData); |
121 | 124 | ||
122 | // Create draggable blocks and set each position by previous one | 125 | // Create draggable blocks and set each position by previous one |
123 | let [left, top] = [20, 20] | 126 | let [left, top] = [20, 20]; |
124 | blocks.forEach(block => { | 127 | blocks.forEach(block => { |
125 | const originLeft = Number(block.getAttribute('data-left')) | 128 | const originLeft = Number(block.getAttribute("data-left")); |
126 | const originTop = Number(block.getAttribute('data-top')) | 129 | const originTop = Number(block.getAttribute("data-top")); |
127 | 130 | ||
128 | // Create draggable block | 131 | // Create draggable block |
129 | const wrapper = document.createElement('div') | 132 | const wrapper = document.createElement("div"); |
130 | wrapper.classList.add('draggable-block') | 133 | wrapper.classList.add("draggable-block"); |
131 | wrapper.innerHTML = ` | 134 | wrapper.innerHTML = ` |
132 | <div class="utils"> | 135 | <div class="utils"> |
133 | <div id="close">\u274C</div> | 136 | <div id="close">\u274C</div> |
134 | <div id="plus-font-size" ">\u2795</div> | 137 | <div id="plus-font-size" ">\u2795</div> |
135 | <div id="minus-font-size">\u2796</div> | 138 | <div id="minus-font-size">\u2796</div> |
136 | </div> | 139 | </div> |
137 | ` | 140 | `; |
138 | wrapper.title = 'Middle-click to hide block' | 141 | wrapper.title = "Middle-click to hide block"; |
139 | wrapper.onmouseup = (e) => { | 142 | wrapper.onmouseup = e => { |
140 | // Hide block with middle click | 143 | // Hide block with middle click |
141 | if (e.button === 1) { | 144 | if (e.button === 1) { |
142 | wrapper.classList.add('hide') | 145 | wrapper.classList.add("hide"); |
143 | } | 146 | } |
144 | } | 147 | }; |
145 | 148 | ||
146 | // Set DOMRect for wrapper | 149 | // Set DOMRect for wrapper |
147 | wrapper.appendChild(block) | 150 | wrapper.appendChild(block); |
148 | wrapper.style.left = left + "px" | 151 | wrapper.style.left = left + "px"; |
149 | wrapper.style.top = top + "px" | 152 | wrapper.style.top = top + "px"; |
150 | htmlHolder.appendChild(wrapper) | 153 | htmlHolder.appendChild(wrapper); |
151 | const { width } = wrapper.getBoundingClientRect() | 154 | const { width } = wrapper.getBoundingClientRect(); |
152 | left += width + 30 | 155 | left += width + 30; |
153 | if (left > window.innerWidth) { | 156 | if (left > window.innerWidth) { |
154 | top += 200 | 157 | top += 200; |
155 | left = left % window.innerWidth | 158 | left = left % window.innerWidth; |
156 | } | 159 | } |
157 | 160 | ||
158 | // Animation for DOMRect | 161 | // Animation for DOMRect |
159 | animateRectTransition( | 162 | animateRectTransition( |
160 | wrapper, | 163 | wrapper, |
161 | { left: originLeft, top: originTop }, | 164 | { left: originLeft, top: originTop }, |
162 | { resume: true, duration: 300 } | 165 | { resume: true, duration: 300 }, |
163 | ) | 166 | ).finished.finally(() => this.addDraggable(wrapper)); |
164 | .finished | ||
165 | .finally(() => this.addDraggable(wrapper)) | ||
166 | 167 | ||
167 | // Trivial case: | 168 | // Trivial case: |
168 | // 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 |
169 | // Prevent DOMRect changes when user clicking plus/minus button many times | 170 | // Prevent DOMRect changes when user clicking plus/minus button many times |
170 | const utils = wrapper.querySelector('.utils') | 171 | const utils = wrapper.querySelector(".utils"); |
171 | utils.onmouseover = () => { | 172 | utils.onmouseover = () => { |
172 | const { left, top } = utils.getBoundingClientRect() | 173 | const { left, top } = utils.getBoundingClientRect(); |
173 | 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;`; |
174 | document.body.appendChild(utils) | 175 | document.body.appendChild(utils); |
175 | } | 176 | }; |
176 | utils.onmouseout = () => { | 177 | utils.onmouseout = () => { |
177 | wrapper.appendChild(utils) | 178 | wrapper.appendChild(utils); |
178 | utils.removeAttribute('style') | 179 | utils.removeAttribute("style"); |
179 | } | 180 | }; |
180 | 181 | ||
181 | // Close button | 182 | // Close button |
182 | wrapper.querySelector('#close').onclick = () => { | 183 | wrapper.querySelector("#close").onclick = () => { |
183 | wrapper.classList.add('hide') | 184 | wrapper.classList.add("hide"); |
184 | utils.removeAttribute('style') | 185 | utils.removeAttribute("style"); |
185 | } | 186 | }; |
186 | // Plus/Minus font-size of content | 187 | // Plus/Minus font-size of content |
187 | wrapper.querySelector('#plus-font-size').onclick = () => { | 188 | wrapper.querySelector("#plus-font-size").onclick = () => { |
188 | const fontSize = parseFloat(getComputedStyle(block).fontSize) / 16 | 189 | const fontSize = parseFloat(getComputedStyle(block).fontSize) / 16; |
189 | block.style.fontSize = `${fontSize + 0.2}rem` | 190 | block.style.fontSize = `${fontSize + 0.2}rem`; |
190 | } | 191 | }; |
191 | wrapper.querySelector('#minus-font-size').onclick = () => { | 192 | wrapper.querySelector("#minus-font-size").onclick = () => { |
192 | const fontSize = parseFloat(getComputedStyle(block).fontSize) / 16 | 193 | const fontSize = parseFloat(getComputedStyle(block).fontSize) / 16; |
193 | block.style.fontSize = `${fontSize - 0.2}rem` | 194 | block.style.fontSize = `${fontSize - 0.2}rem`; |
194 | } | 195 | }; |
195 | }) | 196 | }); |
196 | } | 197 | }; |
197 | 198 | ||
198 | leaveHandler = ({ htmlHolder, blocks }) => { | 199 | leaveHandler = ({ htmlHolder, blocks }) => { |
199 | const resumeFromDraggable = (block) => { | 200 | const resumeFromDraggable = block => { |
200 | const draggableContainer = block.closest('.draggable-block') | 201 | const draggableContainer = block.closest(".draggable-block"); |
201 | if (!draggableContainer) return | 202 | if (!draggableContainer) return; |
202 | htmlHolder.appendChild(block) | 203 | htmlHolder.appendChild(block); |
203 | draggableContainer.remove() | 204 | draggableContainer.remove(); |
204 | } | 205 | }; |
205 | blocks.forEach(resumeFromDraggable) | 206 | blocks.forEach(resumeFromDraggable); |
206 | } | 207 | }; |
207 | } | 208 | } |
208 | |||
diff --git a/src/MenuItem.mjs b/src/MenuItem.mjs index 85ab1a2..2f9aff4 100644 --- a/src/MenuItem.mjs +++ b/src/MenuItem.mjs | |||
@@ -1,97 +1,97 @@ | |||
1 | import { createGeoLink } from './dumbymap' | 1 | import { createGeoLink } from "./dumbymap"; |
2 | 2 | ||
3 | export function nextMap() { | 3 | export 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 | ||
9 | return element | 9 | return element; |
10 | } | 10 | } |
11 | 11 | ||
12 | export function nextBlock() { | 12 | export 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 | ||
18 | return element | 18 | return element; |
19 | } | 19 | } |
20 | 20 | ||
21 | export function nextLayout() { | 21 | export 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 | ||
27 | return element | 27 | return element; |
28 | } | 28 | } |
29 | 29 | ||
30 | export class GeoLink { | 30 | export class GeoLink { |
31 | |||
32 | constructor({ range }) { | 31 | constructor({ range }) { |
33 | this.range = range | 32 | this.range = range; |
34 | } | 33 | } |
35 | 34 | ||
36 | createElement = () => { | 35 | createElement = () => { |
37 | const element = document.createElement('div') | 36 | const element = document.createElement("div"); |
38 | element.className = 'menu-item' | 37 | element.className = "menu-item"; |
39 | element.innerText = "Add GeoLink" | 38 | element.innerText = "Add GeoLink"; |
40 | element.onclick = this.addGeoLinkbyRange | 39 | element.onclick = this.addGeoLinkbyRange; |
41 | 40 | ||
42 | return element | 41 | return element; |
43 | } | 42 | }; |
44 | 43 | ||
45 | addGeoLinkbyRange = () => { | 44 | addGeoLinkbyRange = () => { |
46 | const range = this.range | 45 | const range = this.range; |
47 | const content = range.toString() | 46 | const content = range.toString(); |
48 | // FIXME Apply geolink only on matching sub-range | 47 | // FIXME Apply geolink only on matching sub-range |
49 | const match = content.match(/(^\D*[\d.]+)\D+([\d.]+)\D*$/) | 48 | const match = content.match(/(^\D*[\d.]+)\D+([\d.]+)\D*$/); |
50 | if (!match) return false | 49 | if (!match) return false; |
51 | 50 | ||
52 | const [x, y] = match.slice(1) | 51 | const [x, y] = match.slice(1); |
53 | const anchor = document.createElement('a') | 52 | const anchor = document.createElement("a"); |
54 | anchor.textContent = content | 53 | anchor.textContent = content; |
55 | // FIXME apply WGS84 | 54 | // FIXME apply WGS84 |
56 | anchor.href = `geo:${y},${x}?xy=${x},${y}` | 55 | anchor.href = `geo:${y},${x}?xy=${x},${y}`; |
57 | 56 | ||
58 | if (createGeoLink(anchor)) { | 57 | if (createGeoLink(anchor)) { |
59 | range.deleteContents() | 58 | range.deleteContents(); |
60 | range.insertNode(anchor) | 59 | range.insertNode(anchor); |
61 | } | 60 | } |
62 | } | 61 | }; |
63 | } | 62 | } |
64 | export class Suggestion { | 63 | export class Suggestion { |
65 | constructor({ text, replace }) { | 64 | constructor({ text, replace }) { |
66 | this.text = text | 65 | this.text = text; |
67 | this.replace = replace | 66 | this.replace = replace; |
68 | } | 67 | } |
69 | 68 | ||
70 | createElement(codemirror) { | 69 | createElement(codemirror) { |
71 | const option = document.createElement('div'); | 70 | const option = document.createElement("div"); |
72 | if (this.text.startsWith('<')) { | 71 | if (this.text.startsWith("<")) { |
73 | option.innerHTML = this.text; | 72 | option.innerHTML = this.text; |
74 | } else { | 73 | } else { |
75 | option.innerText = this.text; | 74 | option.innerText = this.text; |
76 | } | 75 | } |
77 | option.classList.add('container__suggestion'); | 76 | option.classList.add("container__suggestion"); |
78 | option.onmouseover = () => { | 77 | option.onmouseover = () => { |
79 | Array.from(option.parentElement?.children ?? []) | 78 | Array.from(option.parentElement?.children ?? []).forEach(s => |
80 | .forEach(s => s.classList.remove('focus')) | 79 | s.classList.remove("focus"), |
81 | option.classList.add('focus') | 80 | ); |
82 | } | 81 | option.classList.add("focus"); |
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(); |
88 | codemirror.setSelection(anchor, { ...anchor, ch: 0 }) | 88 | codemirror.setSelection(anchor, { ...anchor, ch: 0 }); |
89 | codemirror.replaceSelection(this.replace) | 89 | codemirror.replaceSelection(this.replace); |
90 | codemirror.focus(); | 90 | codemirror.focus(); |
91 | const newAnchor = { ...anchor, ch: this.replace.length } | 91 | const newAnchor = { ...anchor, ch: this.replace.length }; |
92 | codemirror.setCursor(newAnchor); | 92 | codemirror.setCursor(newAnchor); |
93 | }; | 93 | }; |
94 | 94 | ||
95 | return option | 95 | return option; |
96 | } | 96 | } |
97 | } | 97 | } |
diff --git a/src/css/dumbymap.css b/src/css/dumbymap.css index 5a67575..4826b5b 100644 --- a/src/css/dumbymap.css +++ b/src/css/dumbymap.css | |||
@@ -8,12 +8,18 @@ root { | |||
8 | } | 8 | } |
9 | 9 | ||
10 | @keyframes fade-in { | 10 | @keyframes fade-in { |
11 | from { opacity: 0; } | 11 | from { |
12 | to { opacity: 1; } | 12 | opacity: 0; |
13 | } | ||
14 | to { | ||
15 | opacity: 1; | ||
16 | } | ||
13 | } | 17 | } |
14 | 18 | ||
15 | @keyframes fade-out { | 19 | @keyframes fade-out { |
16 | from { opacity: 1; } | 20 | from { |
21 | opacity: 1; | ||
22 | } | ||
17 | 23 | ||
18 | to { | 24 | to { |
19 | visibility: hidden; | 25 | visibility: hidden; |
@@ -22,13 +28,21 @@ root { | |||
22 | } | 28 | } |
23 | 29 | ||
24 | @keyframes map-fade-in { | 30 | @keyframes map-fade-in { |
25 | from { opacity: 0.3; } | 31 | from { |
26 | to { opacity: 1; } | 32 | opacity: 0.3; |
33 | } | ||
34 | to { | ||
35 | opacity: 1; | ||
36 | } | ||
27 | } | 37 | } |
28 | 38 | ||
29 | @keyframes map-fade-out { | 39 | @keyframes map-fade-out { |
30 | from { opacity: 1; } | 40 | from { |
31 | to { opacity: 0.3; } | 41 | opacity: 1; |
42 | } | ||
43 | to { | ||
44 | opacity: 0.3; | ||
45 | } | ||
32 | } | 46 | } |
33 | 47 | ||
34 | .mapclay { | 48 | .mapclay { |
@@ -37,7 +51,7 @@ root { | |||
37 | 51 | ||
38 | &.focus::after { | 52 | &.focus::after { |
39 | content: "Map-ID: " attr(id); | 53 | content: "Map-ID: " attr(id); |
40 | padding: .3rem .5rem; | 54 | padding: 0.3rem 0.5rem; |
41 | 55 | ||
42 | position: absolute; | 56 | position: absolute; |
43 | left: 50%; | 57 | left: 50%; |
@@ -56,7 +70,7 @@ root { | |||
56 | 70 | ||
57 | transform: translate(-50%, -50%); | 71 | transform: translate(-50%, -50%); |
58 | opacity: 0; | 72 | opacity: 0; |
59 | animation: 1.5s forwards fade-out cubic-bezier(.44,.18,.86,-0.21); | 73 | animation: 1.5s forwards fade-out cubic-bezier(0.44, 0.18, 0.86, -0.21); |
60 | } | 74 | } |
61 | } | 75 | } |
62 | 76 | ||
@@ -89,7 +103,6 @@ root { | |||
89 | } | 103 | } |
90 | 104 | ||
91 | *:has(> nav) { | 105 | *:has(> nav) { |
92 | |||
93 | /* FIXME Hide nav at proper time */ | 106 | /* FIXME Hide nav at proper time */ |
94 | display: none; | 107 | display: none; |
95 | padding: 0.5rem; | 108 | padding: 0.5rem; |
@@ -131,11 +144,10 @@ root { | |||
131 | 144 | ||
132 | border-left: 0.5em solid #e0e0e0; | 145 | border-left: 0.5em solid #e0e0e0; |
133 | 146 | ||
134 | transition: border-color .5s linear; | 147 | transition: border-color 0.5s linear; |
135 | } | 148 | } |
136 | 149 | ||
137 | &.focus { | 150 | &.focus { |
138 | |||
139 | /* Dress up when block is focus */ | 151 | /* Dress up when block is focus */ |
140 | 152 | ||
141 | /* background: var(--block-focus-color); */ | 153 | /* background: var(--block-focus-color); */ |
@@ -145,7 +157,7 @@ root { | |||
145 | 157 | ||
146 | &::after { | 158 | &::after { |
147 | content: counter(block) "/" attr(data-total); | 159 | content: counter(block) "/" attr(data-total); |
148 | padding: .3rem .5rem; | 160 | padding: 0.3rem 0.5rem; |
149 | 161 | ||
150 | position: absolute; | 162 | position: absolute; |
151 | left: 50%; | 163 | left: 50%; |
@@ -163,7 +175,7 @@ root { | |||
163 | 175 | ||
164 | transform: translate(-50%, -50%); | 176 | transform: translate(-50%, -50%); |
165 | opacity: 0; | 177 | opacity: 0; |
166 | animation: 1.5s forwards fade-out cubic-bezier(.44,.18,.86,-0.21); | 178 | animation: 1.5s forwards fade-out cubic-bezier(0.44, 0.18, 0.86, -0.21); |
167 | } | 179 | } |
168 | } | 180 | } |
169 | } | 181 | } |
@@ -194,7 +206,7 @@ root { | |||
194 | } | 206 | } |
195 | } | 207 | } |
196 | 208 | ||
197 | :has( > .mapclay, > [data-placeholder]) { | 209 | :has(> .mapclay, > [data-placeholder]) { |
198 | display: flex; | 210 | display: flex; |
199 | gap: 0.5em; | 211 | gap: 0.5em; |
200 | 212 | ||
@@ -225,7 +237,7 @@ root { | |||
225 | .Showcase { | 237 | .Showcase { |
226 | display: none; | 238 | display: none; |
227 | overflow: visible; | 239 | overflow: visible; |
228 | width: 100%; | 240 | width: 100%; |
229 | height: 100%; | 241 | height: 100%; |
230 | 242 | ||
231 | position: relative; | 243 | position: relative; |
@@ -246,7 +258,7 @@ root { | |||
246 | 258 | ||
247 | &::after { | 259 | &::after { |
248 | content: "Layout: " attr(data-layout); | 260 | content: "Layout: " attr(data-layout); |
249 | padding: .3rem .5rem; | 261 | padding: 0.3rem 0.5rem; |
250 | 262 | ||
251 | position: absolute; | 263 | position: absolute; |
252 | left: 50%; | 264 | left: 50%; |
@@ -272,13 +284,13 @@ root { | |||
272 | max-width: 60em; | 284 | max-width: 60em; |
273 | 285 | ||
274 | &::after { | 286 | &::after { |
275 | animation: 1.5s forwards fade-out cubic-bezier(.44,.18,.86,-0.21); | 287 | animation: 1.5s forwards fade-out cubic-bezier(0.44, 0.18, 0.86, -0.21); |
276 | } | 288 | } |
277 | } | 289 | } |
278 | 290 | ||
279 | .Dumby[data-layout="side-by-side"] { | 291 | .Dumby[data-layout="side-by-side"] { |
280 | &::after { | 292 | &::after { |
281 | animation: 1.5s forwards fade-out cubic-bezier(.44,.18,.86,-0.21); | 293 | animation: 1.5s forwards fade-out cubic-bezier(0.44, 0.18, 0.86, -0.21); |
282 | } | 294 | } |
283 | 295 | ||
284 | .SemanticHtml, | 296 | .SemanticHtml, |
@@ -336,13 +348,12 @@ root { | |||
336 | transform: unset; | 348 | transform: unset; |
337 | } | 349 | } |
338 | } | 350 | } |
339 | |||
340 | } | 351 | } |
341 | } | 352 | } |
342 | 353 | ||
343 | .Dumby[data-layout="overlay"] { | 354 | .Dumby[data-layout="overlay"] { |
344 | &::after { | 355 | &::after { |
345 | animation: 1.5s forwards fade-out cubic-bezier(.44,.18,.86,-0.21); | 356 | animation: 1.5s forwards fade-out cubic-bezier(0.44, 0.18, 0.86, -0.21); |
346 | } | 357 | } |
347 | 358 | ||
348 | .SemanticHtml, | 359 | .SemanticHtml, |
@@ -384,7 +395,6 @@ root { | |||
384 | padding-inline: 1rem; | 395 | padding-inline: 1rem; |
385 | padding-block: 1rem; | 396 | padding-block: 1rem; |
386 | 397 | ||
387 | |||
388 | &:hover { | 398 | &:hover { |
389 | visibility: visible; | 399 | visibility: visible; |
390 | opacity: 1; | 400 | opacity: 1; |
@@ -402,16 +412,16 @@ root { | |||
402 | } | 412 | } |
403 | 413 | ||
404 | #close { | 414 | #close { |
405 | cursor: pointer; | 415 | cursor: pointer; |
406 | } | 416 | } |
407 | 417 | ||
408 | #plus-font-size { | 418 | #plus-font-size { |
409 | cursor: zoom-in; | 419 | cursor: zoom-in; |
410 | } | 420 | } |
411 | 421 | ||
412 | #minus-font-size { | 422 | #minus-font-size { |
413 | cursor: zoom-out; | 423 | cursor: zoom-out; |
414 | } | 424 | } |
415 | } | 425 | } |
416 | 426 | ||
417 | .draggable-block { | 427 | .draggable-block { |
@@ -431,7 +441,7 @@ root { | |||
431 | &:has(.dumby-block.focus) { | 441 | &:has(.dumby-block.focus) { |
432 | z-index: 9001; | 442 | z-index: 9001; |
433 | outline: solid #505050 1px; | 443 | outline: solid #505050 1px; |
434 | border-color: #505050 | 444 | border-color: #505050; |
435 | } | 445 | } |
436 | 446 | ||
437 | &:has(pre:only-child [data-placeholder]:only-child) { | 447 | &:has(pre:only-child [data-placeholder]:only-child) { |
@@ -440,7 +450,7 @@ root { | |||
440 | 450 | ||
441 | /* Fade out top of contents scrolling */ | 451 | /* Fade out top of contents scrolling */ |
442 | &::before { | 452 | &::before { |
443 | content: ''; | 453 | content: ""; |
444 | display: block; | 454 | display: block; |
445 | height: 1.5rem; | 455 | height: 1.5rem; |
446 | 456 | ||
@@ -504,7 +514,7 @@ root { | |||
504 | border-top-right-radius: 0.3rem; | 514 | border-top-right-radius: 0.3rem; |
505 | } | 515 | } |
506 | 516 | ||
507 | .handle { | 517 | .handle { |
508 | font-size: 1.1rem; | 518 | font-size: 1.1rem; |
509 | text-align: center; | 519 | text-align: center; |
510 | 520 | ||
@@ -525,7 +535,8 @@ root { | |||
525 | } | 535 | } |
526 | } | 536 | } |
527 | 537 | ||
528 | &.drag, &:has(.draggable-part:hover) { | 538 | &.drag, |
539 | &:has(.draggable-part:hover) { | ||
529 | .handle { | 540 | .handle { |
530 | background: #e1e1e1; | 541 | background: #e1e1e1; |
531 | 542 | ||
@@ -534,7 +545,7 @@ root { | |||
534 | 545 | ||
535 | .utils { | 546 | .utils { |
536 | visibility: visible; | 547 | visibility: visible; |
537 | animation: fade-in .5s; | 548 | animation: fade-in 0.5s; |
538 | } | 549 | } |
539 | 550 | ||
540 | > *:not(.draggable-part, .utils) { | 551 | > *:not(.draggable-part, .utils) { |
@@ -550,8 +561,8 @@ root { | |||
550 | 561 | ||
551 | [data-placeholder] { | 562 | [data-placeholder] { |
552 | height: 50px !important; | 563 | height: 50px !important; |
553 | scale: .9; | 564 | scale: 0.9; |
554 | transition-duration: .4s; | 565 | transition-duration: 0.4s; |
555 | transition-timing-function: ease-out; | 566 | transition-timing-function: ease-out; |
556 | } | 567 | } |
557 | } | 568 | } |
diff --git a/src/css/index.css b/src/css/index.css index 194ca9c..a17ecce 100644 --- a/src/css/index.css +++ b/src/css/index.css | |||
@@ -65,7 +65,7 @@ body { | |||
65 | padding-inline: 0; | 65 | padding-inline: 0; |
66 | 66 | ||
67 | font-family: monospace; | 67 | font-family: monospace; |
68 | font-size: 1.0rem; | 68 | font-size: 1rem; |
69 | 69 | ||
70 | &.focus { | 70 | &.focus { |
71 | border: var(--content-focus-border); | 71 | border: var(--content-focus-border); |
@@ -158,7 +158,7 @@ body { | |||
158 | text-overflow: ellipsis; | 158 | text-overflow: ellipsis; |
159 | 159 | ||
160 | ::before { | 160 | ::before { |
161 | width: 2rem | 161 | width: 2rem; |
162 | } | 162 | } |
163 | } | 163 | } |
164 | } | 164 | } |
diff --git a/src/css/style.css b/src/css/style.css index 14a2e96..1d39cc0 100644 --- a/src/css/style.css +++ b/src/css/style.css | |||
@@ -1,5 +1,7 @@ | |||
1 | /* CSS Reset */ | 1 | /* CSS Reset */ |
2 | *, *::before, *::after { | 2 | *, |
3 | *::before, | ||
4 | *::after { | ||
3 | box-sizing: border-box; | 5 | box-sizing: border-box; |
4 | } | 6 | } |
5 | 7 | ||
@@ -9,15 +11,28 @@ | |||
9 | line-height: calc(1em + 0.5rem); | 11 | line-height: calc(1em + 0.5rem); |
10 | } | 12 | } |
11 | 13 | ||
12 | img, picture, video, canvas, svg { | 14 | img, |
15 | picture, | ||
16 | video, | ||
17 | canvas, | ||
18 | svg { | ||
13 | display: block; | 19 | display: block; |
14 | } | 20 | } |
15 | 21 | ||
16 | input, button, textarea, select { | 22 | input, |
23 | button, | ||
24 | textarea, | ||
25 | select { | ||
17 | font: inherit; | 26 | font: inherit; |
18 | } | 27 | } |
19 | 28 | ||
20 | p, h1, h2, h3, h4, h5, h6 { | 29 | p, |
30 | h1, | ||
31 | h2, | ||
32 | h3, | ||
33 | h4, | ||
34 | h5, | ||
35 | h6 { | ||
21 | overflow-wrap: break-word; | 36 | overflow-wrap: break-word; |
22 | hyphens: auto; | 37 | hyphens: auto; |
23 | } | 38 | } |
@@ -29,7 +44,12 @@ body { | |||
29 | line-height: 1.6; | 44 | line-height: 1.6; |
30 | } | 45 | } |
31 | 46 | ||
32 | h1, h2, h3, h4, h5, h6 { | 47 | h1, |
48 | h2, | ||
49 | h3, | ||
50 | h4, | ||
51 | h5, | ||
52 | h6 { | ||
33 | font-weight: bold; | 53 | font-weight: bold; |
34 | margin-bottom: 0.5em; | 54 | margin-bottom: 0.5em; |
35 | } | 55 | } |
@@ -102,7 +122,8 @@ pre:has(code) { | |||
102 | } | 122 | } |
103 | } | 123 | } |
104 | 124 | ||
105 | ul, ol { | 125 | ul, |
126 | ol { | ||
106 | margin-top: 1em; | 127 | margin-top: 1em; |
107 | margin-bottom: 1em; | 128 | margin-bottom: 1em; |
108 | padding-left: 20px; | 129 | padding-left: 20px; |
@@ -118,7 +139,8 @@ table { | |||
118 | width: 100%; | 139 | width: 100%; |
119 | } | 140 | } |
120 | 141 | ||
121 | th, td { | 142 | th, |
143 | td { | ||
122 | padding: 8px; | 144 | padding: 8px; |
123 | 145 | ||
124 | border: 1px solid #ddd; | 146 | border: 1px solid #ddd; |
diff --git a/src/dumbyUtils.mjs b/src/dumbyUtils.mjs index c878e48..d30145c 100644 --- a/src/dumbyUtils.mjs +++ b/src/dumbyUtils.mjs | |||
@@ -1,58 +1,72 @@ | |||
1 | export function focusNextMap(reverse = false) { | 1 | export function focusNextMap(reverse = false) { |
2 | const renderedList = Array.from(this.htmlHolder.querySelectorAll('[data-render=fulfilled]')) | 2 | const renderedList = Array.from( |
3 | const mapNum = renderedList.length | 3 | this.htmlHolder.querySelectorAll("[data-render=fulfilled]"), |
4 | if (mapNum === 0) return | 4 | ); |
5 | const mapNum = renderedList.length; | ||
6 | if (mapNum === 0) return; | ||
5 | 7 | ||
6 | // Get current focused map element | 8 | // Get current focused map element |
7 | const currentFocus = this.container.querySelector('.mapclay.focus') | 9 | const currentFocus = this.container.querySelector(".mapclay.focus"); |
8 | 10 | ||
9 | // Remove class name of focus for ALL candidates | 11 | // Remove class name of focus for ALL candidates |
10 | // This may trigger animation | 12 | // This may trigger animation |
11 | renderedList.forEach(ele => ele.classList.remove('focus')) | 13 | renderedList.forEach(ele => ele.classList.remove("focus")); |
12 | 14 | ||
13 | // Get next existing map element | 15 | // Get next existing map element |
14 | const padding = reverse ? -1 : 1 | 16 | const padding = reverse ? -1 : 1; |
15 | let nextIndex = currentFocus ? renderedList.indexOf(currentFocus) + padding : 0 | 17 | let nextIndex = currentFocus |
16 | nextIndex = (nextIndex + mapNum) % mapNum | 18 | ? renderedList.indexOf(currentFocus) + padding |
17 | const nextFocus = renderedList[nextIndex] | 19 | : 0; |
18 | nextFocus.classList.add("focus") | 20 | nextIndex = (nextIndex + mapNum) % mapNum; |
21 | const nextFocus = renderedList[nextIndex]; | ||
22 | nextFocus.classList.add("focus"); | ||
19 | 23 | ||
20 | return nextFocus | 24 | return nextFocus; |
21 | } | 25 | } |
22 | 26 | ||
23 | export function focusDelay() { | 27 | export function focusDelay() { |
24 | return window.getComputedStyle(this.showcase).display === 'none' ? 50 : 300 | 28 | return window.getComputedStyle(this.showcase).display === "none" ? 50 : 300; |
25 | } | 29 | } |
26 | 30 | ||
27 | export function switchToNextLayout(reverse = false) { | 31 | export function switchToNextLayout(reverse = false) { |
28 | const layouts = this.layouts | 32 | const layouts = this.layouts; |
29 | const currentLayoutName = this.container.getAttribute('data-layout') | 33 | const currentLayoutName = this.container.getAttribute("data-layout"); |
30 | const currentIndex = layouts.map(l => l.name).indexOf(currentLayoutName) | 34 | const currentIndex = layouts.map(l => l.name).indexOf(currentLayoutName); |
31 | const padding = reverse ? -1 : 1 | 35 | const padding = reverse ? -1 : 1; |
32 | const nextIndex = currentIndex === -1 ? 0 : (currentIndex + padding + layouts.length) % layouts.length | 36 | const nextIndex = |
33 | const nextLayout = layouts[nextIndex] | 37 | currentIndex === -1 |
34 | this.container.setAttribute("data-layout", nextLayout.name) | 38 | ? 0 |
39 | : (currentIndex + padding + layouts.length) % layouts.length; | ||
40 | const nextLayout = layouts[nextIndex]; | ||
41 | this.container.setAttribute("data-layout", nextLayout.name); | ||
35 | } | 42 | } |
36 | 43 | ||
37 | export function focusNextBlock(reverse = false) { | 44 | export function focusNextBlock(reverse = false) { |
38 | const blocks = this.blocks.filter(b => b.checkVisibility({ | 45 | const blocks = this.blocks.filter(b => |
39 | contentVisibilityAuto: true, | 46 | b.checkVisibility({ |
40 | opacityProperty: true, | 47 | contentVisibilityAuto: true, |
41 | visibilityProperty: true, | 48 | opacityProperty: true, |
42 | })) | 49 | visibilityProperty: true, |
43 | const currentBlock = blocks.find(b => b.classList.contains('focus')) | 50 | }), |
44 | const currentIndex = blocks.indexOf(currentBlock) | 51 | ); |
45 | const padding = reverse ? -1 : 1 | 52 | const currentBlock = blocks.find(b => b.classList.contains("focus")); |
46 | const nextIndex = currentIndex === -1 ? 0 : (currentIndex + padding + blocks.length) % blocks.length | 53 | const currentIndex = blocks.indexOf(currentBlock); |
47 | const nextBlock = blocks[nextIndex] | 54 | const padding = reverse ? -1 : 1; |
48 | blocks.forEach(b => b.classList.remove('focus')) | 55 | const nextIndex = |
49 | nextBlock?.classList?.add('focus') | 56 | currentIndex === -1 |
50 | const scrollBlock = nextBlock.getBoundingClientRect().height > nextBlock.parentElement.getBoundingClientRect().height * 0.8 | 57 | ? 0 |
51 | ? 'nearest' | 58 | : (currentIndex + padding + blocks.length) % blocks.length; |
52 | : 'center' | 59 | const nextBlock = blocks[nextIndex]; |
53 | nextBlock.scrollIntoView({ behavior: 'smooth', block: scrollBlock }) | 60 | blocks.forEach(b => b.classList.remove("focus")); |
61 | nextBlock?.classList?.add("focus"); | ||
62 | const scrollBlock = | ||
63 | nextBlock.getBoundingClientRect().height > | ||
64 | nextBlock.parentElement.getBoundingClientRect().height * 0.8 | ||
65 | ? "nearest" | ||
66 | : "center"; | ||
67 | nextBlock.scrollIntoView({ behavior: "smooth", block: scrollBlock }); | ||
54 | } | 68 | } |
55 | 69 | ||
56 | export function removeBlockFocus() { | 70 | export function removeBlockFocus() { |
57 | this.blocks.forEach(b => b.classList.remove('focus')) | 71 | this.blocks.forEach(b => b.classList.remove("focus")); |
58 | } | 72 | } |
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs index 56375f7..cb029de 100644 --- a/src/dumbymap.mjs +++ b/src/dumbymap.mjs | |||
@@ -1,23 +1,23 @@ | |||
1 | import MarkdownIt from 'markdown-it' | 1 | import MarkdownIt from "markdown-it"; |
2 | import MarkdownItAnchor from 'markdown-it-anchor' | 2 | import MarkdownItAnchor from "markdown-it-anchor"; |
3 | import MarkdownItFootnote from 'markdown-it-footnote' | 3 | import MarkdownItFootnote from "markdown-it-footnote"; |
4 | import MarkdownItFrontMatter from 'markdown-it-front-matter' | 4 | import MarkdownItFrontMatter from "markdown-it-front-matter"; |
5 | import MarkdownItTocDoneRight from 'markdown-it-toc-done-right' | 5 | import MarkdownItTocDoneRight from "markdown-it-toc-done-right"; |
6 | import LeaderLine from 'leader-line' | 6 | import LeaderLine from "leader-line"; |
7 | import { renderWith, defaultAliases, parseConfigsFromYaml } from 'mapclay' | 7 | import { renderWith, defaultAliases, parseConfigsFromYaml } from "mapclay"; |
8 | import { onRemove, animateRectTransition, throttle } from './utils' | 8 | import { onRemove, animateRectTransition, throttle } from "./utils"; |
9 | import { Layout, SideBySide, Overlay } from './Layout' | 9 | import { Layout, SideBySide, Overlay } from "./Layout"; |
10 | import * as utils from './dumbyUtils' | 10 | import * as utils from "./dumbyUtils"; |
11 | 11 | ||
12 | const docLinkSelector = 'a[href^="#"][title^="=>"]' | 12 | const docLinkSelector = 'a[href^="#"][title^="=>"]'; |
13 | const geoLinkSelector = 'a[href^="geo:"]' | 13 | const geoLinkSelector = 'a[href^="geo:"]'; |
14 | 14 | ||
15 | const layouts = [ | 15 | const layouts = [ |
16 | new Layout({ name: "normal" }), | 16 | new Layout({ name: "normal" }), |
17 | new SideBySide({ name: "side-by-side" }), | 17 | new SideBySide({ name: "side-by-side" }), |
18 | new Overlay({ name: "overlay" }), | 18 | new Overlay({ name: "overlay" }), |
19 | ] | 19 | ]; |
20 | const mapCache = {} | 20 | const mapCache = {}; |
21 | 21 | ||
22 | // FUNCTION: Get DocLinks from special anchor element {{{ | 22 | // FUNCTION: Get DocLinks from special anchor element {{{ |
23 | /** | 23 | /** |
@@ -25,34 +25,34 @@ const mapCache = {} | |||
25 | * | 25 | * |
26 | * @param {HTMLElement} Elements contains anchor elements for doclinks | 26 | * @param {HTMLElement} Elements contains anchor elements for doclinks |
27 | */ | 27 | */ |
28 | export const createDocLink = (link) => { | 28 | export const createDocLink = link => { |
29 | link.classList.add('with-leader-line', 'doclink') | 29 | link.classList.add("with-leader-line", "doclink"); |
30 | link.lines = [] | 30 | link.lines = []; |
31 | 31 | ||
32 | link.onmouseover = () => { | 32 | link.onmouseover = () => { |
33 | const label = decodeURIComponent(link.href.split('#')[1]) | 33 | const label = decodeURIComponent(link.href.split("#")[1]); |
34 | const selector = link.title.split('=>')[1] ?? '#' + label | 34 | const selector = link.title.split("=>")[1] ?? "#" + label; |
35 | const target = document.querySelector(selector) | 35 | const target = document.querySelector(selector); |
36 | if (!target?.checkVisibility()) return | 36 | if (!target?.checkVisibility()) return; |
37 | 37 | ||
38 | const line = new LeaderLine({ | 38 | const line = new LeaderLine({ |
39 | start: link, | 39 | start: link, |
40 | end: target, | 40 | end: target, |
41 | middleLabel: LeaderLine.pathLabel({ | 41 | middleLabel: LeaderLine.pathLabel({ |
42 | text: label, | 42 | text: label, |
43 | fontWeight: 'bold', | 43 | fontWeight: "bold", |
44 | }), | 44 | }), |
45 | hide: true, | 45 | hide: true, |
46 | path: "magnet" | 46 | path: "magnet", |
47 | }) | 47 | }); |
48 | link.lines.push(line) | 48 | link.lines.push(line); |
49 | line.show('draw', { duration: 300, }) | 49 | line.show("draw", { duration: 300 }); |
50 | } | 50 | }; |
51 | link.onmouseout = () => { | 51 | link.onmouseout = () => { |
52 | link.lines.forEach(line => line.remove()) | 52 | link.lines.forEach(line => line.remove()); |
53 | link.lines.length = 0 | 53 | link.lines.length = 0; |
54 | } | 54 | }; |
55 | } | 55 | }; |
56 | // }}} | 56 | // }}} |
57 | // FUNCTION: Get GeoLinks from special anchor element {{{ | 57 | // FUNCTION: Get GeoLinks from special anchor element {{{ |
58 | /** | 58 | /** |
@@ -62,92 +62,98 @@ export const createDocLink = (link) => { | |||
62 | * @returns {Boolean} ture is link is created, false if coordinates are invalid | 62 | * @returns {Boolean} ture is link is created, false if coordinates are invalid |
63 | */ | 63 | */ |
64 | export const createGeoLink = (link, callback = null) => { | 64 | export const createGeoLink = (link, callback = null) => { |
65 | const url = new URL(link.href) | 65 | const url = new URL(link.href); |
66 | const xyInParams = url.searchParams.get('xy') | 66 | const xyInParams = url.searchParams.get("xy"); |
67 | const xy = xyInParams | 67 | const xy = xyInParams |
68 | ? xyInParams.split(',')?.map(Number) | 68 | ? xyInParams.split(",")?.map(Number) |
69 | : url?.href?.match(/^geo:([0-9.,]+)/)?.at(1)?.split(',')?.reverse()?.map(Number) | 69 | : url?.href |
70 | ?.match(/^geo:([0-9.,]+)/) | ||
71 | ?.at(1) | ||
72 | ?.split(",") | ||
73 | ?.reverse() | ||
74 | ?.map(Number); | ||
70 | 75 | ||
71 | if (!xy || isNaN(xy[0]) || isNaN(xy[1])) return false | 76 | if (!xy || isNaN(xy[0]) || isNaN(xy[1])) return false; |
72 | 77 | ||
73 | // Geo information in link | 78 | // Geo information in link |
74 | link.url = url | 79 | link.url = url; |
75 | link.xy = xy | 80 | link.xy = xy; |
76 | link.classList.add('with-leader-line', 'geolink') | 81 | link.classList.add("with-leader-line", "geolink"); |
77 | link.targets = link.url.searchParams.get('id')?.split(',') ?? null | 82 | link.targets = link.url.searchParams.get("id")?.split(",") ?? null; |
78 | 83 | ||
79 | // LeaderLine | 84 | // LeaderLine |
80 | link.lines = [] | 85 | link.lines = []; |
81 | callback?.call(this, link) | 86 | callback?.call(this, link); |
82 | 87 | ||
83 | return true | 88 | return true; |
84 | } | 89 | }; |
85 | // }}} | 90 | // }}} |
86 | 91 | ||
87 | export const markdown2HTML = (container, mdContent) => { | 92 | export const markdown2HTML = (container, mdContent) => { |
88 | // Render: Markdown -> HTML {{{ | 93 | // Render: Markdown -> HTML {{{ |
89 | Array.from(container.children).map(e => e.remove()) | 94 | Array.from(container.children).map(e => e.remove()); |
90 | 95 | ||
91 | 96 | container.innerHTML = '<div class="SemanticHtml"></div>'; | |
92 | container.innerHTML = '<div class="SemanticHtml"></div>' | 97 | const htmlHolder = container.querySelector(".SemanticHtml"); |
93 | const htmlHolder = container.querySelector('.SemanticHtml') | ||
94 | 98 | ||
95 | const md = MarkdownIt({ | 99 | const md = MarkdownIt({ |
96 | html: true, | 100 | html: true, |
97 | breaks: true, | 101 | breaks: true, |
98 | }) | 102 | }) |
99 | .use(MarkdownItAnchor, { | 103 | .use(MarkdownItAnchor, { |
100 | permalink: MarkdownItAnchor.permalink.linkInsideHeader({ placement: 'before' }) | 104 | permalink: MarkdownItAnchor.permalink.linkInsideHeader({ |
105 | placement: "before", | ||
106 | }), | ||
101 | }) | 107 | }) |
102 | .use(MarkdownItFootnote) | 108 | .use(MarkdownItFootnote) |
103 | .use(MarkdownItFrontMatter) | 109 | .use(MarkdownItFrontMatter) |
104 | .use(MarkdownItTocDoneRight) | 110 | .use(MarkdownItTocDoneRight); |
105 | 111 | ||
106 | // FIXME A better way to generate blocks | 112 | // FIXME A better way to generate blocks |
107 | md.renderer.rules.dumby_block_open = () => '<div>' | 113 | md.renderer.rules.dumby_block_open = () => "<div>"; |
108 | md.renderer.rules.dumby_block_close = () => '</div>' | 114 | md.renderer.rules.dumby_block_close = () => "</div>"; |
109 | 115 | ||
110 | md.core.ruler.before('block', 'dumby_block', (state) => { | 116 | md.core.ruler.before("block", "dumby_block", state => { |
111 | state.tokens.push(new state.Token('dumby_block_open', '', 1)) | 117 | state.tokens.push(new state.Token("dumby_block_open", "", 1)); |
112 | }) | 118 | }); |
113 | 119 | ||
114 | // Add close tag for block with more than 2 empty lines | 120 | // Add close tag for block with more than 2 empty lines |
115 | md.block.ruler.before('table', 'dumby_block', (state, startLine) => { | 121 | md.block.ruler.before("table", "dumby_block", (state, startLine) => { |
116 | if ( | 122 | if ( |
117 | state.src[state.bMarks[startLine - 1]] === '\n' && | 123 | state.src[state.bMarks[startLine - 1]] === "\n" && |
118 | state.src[state.bMarks[startLine - 2]] === '\n' && | 124 | state.src[state.bMarks[startLine - 2]] === "\n" && |
119 | state.tokens.at(-1).type !== 'list_item_open' // Quick hack for not adding tag after "::marker" for <li> | 125 | state.tokens.at(-1).type !== "list_item_open" // Quick hack for not adding tag after "::marker" for <li> |
120 | ) { | 126 | ) { |
121 | state.push('dumby_block_close', '', -1); | 127 | state.push("dumby_block_close", "", -1); |
122 | state.push('dumby_block_open', '', 1); | 128 | state.push("dumby_block_open", "", 1); |
123 | } | 129 | } |
124 | }) | 130 | }); |
125 | 131 | ||
126 | md.core.ruler.after('block', 'dumby_block', (state) => { | 132 | md.core.ruler.after("block", "dumby_block", state => { |
127 | state.tokens.push(new state.Token('dumby_block_close', '', -1)) | 133 | state.tokens.push(new state.Token("dumby_block_close", "", -1)); |
128 | }) | 134 | }); |
129 | 135 | ||
130 | const contentWithToc = '${toc}\n\n\n' + mdContent | 136 | const contentWithToc = "${toc}\n\n\n" + mdContent; |
131 | htmlHolder.innerHTML = md.render(contentWithToc); | 137 | htmlHolder.innerHTML = md.render(contentWithToc); |
132 | 138 | ||
133 | // TODO Do this in markdown-it | 139 | // TODO Do this in markdown-it |
134 | const blocks = htmlHolder.querySelectorAll(':scope > div:not(:has(nav))') | 140 | const blocks = htmlHolder.querySelectorAll(":scope > div:not(:has(nav))"); |
135 | blocks.forEach(b => { | 141 | blocks.forEach(b => { |
136 | b.classList.add('dumby-block') | 142 | b.classList.add("dumby-block"); |
137 | b.setAttribute('data-total', blocks.length) | 143 | b.setAttribute("data-total", blocks.length); |
138 | }) | 144 | }); |
139 | 145 | ||
140 | return container | 146 | return container; |
141 | //}}} | 147 | //}}} |
142 | } | 148 | }; |
143 | export const generateMaps = (container, {delay, mapCallback}) => { | 149 | export const generateMaps = (container, { delay, mapCallback }) => { |
144 | container.classList.add('Dumby') | 150 | container.classList.add("Dumby"); |
145 | const htmlHolder = container.querySelector('.SemanticHtml') ?? container | 151 | const htmlHolder = container.querySelector(".SemanticHtml") ?? container; |
146 | const blocks = Array.from(htmlHolder.querySelectorAll('.dumby-block')) | 152 | const blocks = Array.from(htmlHolder.querySelectorAll(".dumby-block")); |
147 | const showcase = document.createElement('div') | 153 | const showcase = document.createElement("div"); |
148 | container.appendChild(showcase) | 154 | container.appendChild(showcase); |
149 | showcase.classList.add('Showcase') | 155 | showcase.classList.add("Showcase"); |
150 | const renderMaps = [] | 156 | const renderMaps = []; |
151 | 157 | ||
152 | const dumbymap = { | 158 | const dumbymap = { |
153 | layouts, | 159 | layouts, |
@@ -161,310 +167,325 @@ export const generateMaps = (container, {delay, mapCallback}) => { | |||
161 | switchToNextLayout: throttle(utils.switchToNextLayout, 300), | 167 | switchToNextLayout: throttle(utils.switchToNextLayout, 300), |
162 | focusNextBlock: utils.focusNextBlock, | 168 | focusNextBlock: utils.focusNextBlock, |
163 | removeBlockFocus: utils.removeBlockFocus, | 169 | removeBlockFocus: utils.removeBlockFocus, |
164 | } | 170 | }, |
165 | } | 171 | }; |
166 | Object.entries(dumbymap.utils) | 172 | Object.entries(dumbymap.utils).forEach(([util, func]) => { |
167 | .forEach(([util, func]) => { | 173 | dumbymap.utils[util] = func.bind(dumbymap); |
168 | dumbymap.utils[util] = func.bind(dumbymap) | 174 | }); |
169 | }) | ||
170 | 175 | ||
171 | // LeaderLine {{{ | 176 | // LeaderLine {{{ |
172 | 177 | ||
173 | Array.from(container.querySelectorAll(docLinkSelector)) | 178 | Array.from(container.querySelectorAll(docLinkSelector)).filter(createDocLink); |
174 | .filter(createDocLink) | ||
175 | 179 | ||
176 | // Get anchors with "geo:" scheme | 180 | // Get anchors with "geo:" scheme |
177 | htmlHolder.anchors = [] | 181 | htmlHolder.anchors = []; |
178 | const geoLinkCallback = (link) => { | 182 | const geoLinkCallback = link => { |
179 | link.onmouseover = () => addLeaderLines(link) | 183 | link.onmouseover = () => addLeaderLines(link); |
180 | link.onmouseout = () => removeLeaderLines(link) | 184 | link.onmouseout = () => removeLeaderLines(link); |
181 | link.onclick = (event) => { | 185 | link.onclick = event => { |
182 | event.preventDefault() | 186 | event.preventDefault(); |
183 | htmlHolder.anchors | 187 | htmlHolder.anchors |
184 | .filter(isAnchorPointedBy(link)) | 188 | .filter(isAnchorPointedBy(link)) |
185 | .forEach(updateMapByMarker(link.xy)) | 189 | .forEach(updateMapByMarker(link.xy)); |
186 | // TODO Just hide leader line and show it again | 190 | // TODO Just hide leader line and show it again |
187 | removeLeaderLines(link) | 191 | removeLeaderLines(link); |
188 | } | 192 | }; |
189 | } | 193 | }; |
190 | const geoLinks = Array.from(container.querySelectorAll(geoLinkSelector)) | 194 | const geoLinks = Array.from( |
191 | .filter(l => createGeoLink(l, geoLinkCallback)) | 195 | container.querySelectorAll(geoLinkSelector), |
192 | 196 | ).filter(l => createGeoLink(l, geoLinkCallback)); | |
193 | const isAnchorPointedBy = (link) => (anchor) => { | 197 | |
194 | const mapContainer = anchor.closest('.mapclay') | 198 | const isAnchorPointedBy = link => anchor => { |
195 | const isTarget = !link.targets || link.targets.includes(mapContainer.id) | 199 | const mapContainer = anchor.closest(".mapclay"); |
196 | return anchor.title === link.url.pathname && isTarget | 200 | const isTarget = !link.targets || link.targets.includes(mapContainer.id); |
197 | } | 201 | return anchor.title === link.url.pathname && isTarget; |
198 | 202 | }; | |
199 | const isAnchorVisible = (anchor) => { | 203 | |
200 | const mapContainer = anchor.closest('.mapclay') | 204 | const isAnchorVisible = anchor => { |
201 | return insideWindow(anchor) && insideParent(anchor, mapContainer) | 205 | const mapContainer = anchor.closest(".mapclay"); |
202 | } | 206 | return insideWindow(anchor) && insideParent(anchor, mapContainer); |
203 | 207 | }; | |
204 | const drawLeaderLine = (link) => (anchor) => { | 208 | |
209 | const drawLeaderLine = link => anchor => { | ||
205 | const line = new LeaderLine({ | 210 | const line = new LeaderLine({ |
206 | start: link, | 211 | start: link, |
207 | end: anchor, | 212 | end: anchor, |
208 | hide: true, | 213 | hide: true, |
209 | middleLabel: link.url.searchParams.get('text'), | 214 | middleLabel: link.url.searchParams.get("text"), |
210 | path: "magnet", | 215 | path: "magnet", |
211 | }) | 216 | }); |
212 | line.show('draw', { duration: 300, }) | 217 | line.show("draw", { duration: 300 }); |
213 | return line | 218 | return line; |
214 | } | 219 | }; |
215 | 220 | ||
216 | const addLeaderLines = (link) => { | 221 | const addLeaderLines = link => { |
217 | link.lines = htmlHolder.anchors | 222 | link.lines = htmlHolder.anchors |
218 | .filter(isAnchorPointedBy(link)) | 223 | .filter(isAnchorPointedBy(link)) |
219 | .filter(isAnchorVisible) | 224 | .filter(isAnchorVisible) |
220 | .map(drawLeaderLine(link)) | 225 | .map(drawLeaderLine(link)); |
221 | } | 226 | }; |
222 | 227 | ||
223 | const removeLeaderLines = (link) => { | 228 | const removeLeaderLines = link => { |
224 | if (!link.lines) return | 229 | if (!link.lines) return; |
225 | link.lines.forEach(line => line.remove()) | 230 | link.lines.forEach(line => line.remove()); |
226 | link.lines = [] | 231 | link.lines = []; |
227 | } | 232 | }; |
228 | 233 | ||
229 | const updateMapByMarker = (xy) => (marker) => { | 234 | const updateMapByMarker = xy => marker => { |
230 | const renderer = marker.closest('.mapclay')?.renderer | 235 | const renderer = marker.closest(".mapclay")?.renderer; |
231 | renderer.updateCamera({ center: xy }, true) | 236 | renderer.updateCamera({ center: xy }, true); |
232 | } | 237 | }; |
233 | 238 | ||
234 | const insideWindow = (element) => { | 239 | const insideWindow = element => { |
235 | const rect = element.getBoundingClientRect() | 240 | const rect = element.getBoundingClientRect(); |
236 | return rect.left > 0 && | 241 | return ( |
242 | rect.left > 0 && | ||
237 | rect.right < window.innerWidth + rect.width && | 243 | rect.right < window.innerWidth + rect.width && |
238 | rect.top > 0 && | 244 | rect.top > 0 && |
239 | rect.bottom < window.innerHeight + rect.height | 245 | rect.bottom < window.innerHeight + rect.height |
240 | } | 246 | ); |
247 | }; | ||
241 | 248 | ||
242 | const insideParent = (childElement, parentElement) => { | 249 | const insideParent = (childElement, parentElement) => { |
243 | const childRect = childElement.getBoundingClientRect(); | 250 | const childRect = childElement.getBoundingClientRect(); |
244 | const parentRect = parentElement.getBoundingClientRect(); | 251 | const parentRect = parentElement.getBoundingClientRect(); |
245 | const offset = 20 | 252 | const offset = 20; |
246 | 253 | ||
247 | return childRect.left > parentRect.left + offset && | 254 | return ( |
255 | childRect.left > parentRect.left + offset && | ||
248 | childRect.right < parentRect.right - offset && | 256 | childRect.right < parentRect.right - offset && |
249 | childRect.top > parentRect.top + offset && | 257 | childRect.top > parentRect.top + offset && |
250 | childRect.bottom < parentRect.bottom - offset | 258 | childRect.bottom < parentRect.bottom - offset |
251 | } | 259 | ); |
260 | }; | ||
252 | //}}} | 261 | //}}} |
253 | // Draggable Blocks {{{ | 262 | // Draggable Blocks {{{ |
254 | // Add draggable part for blocks | 263 | // Add draggable part for blocks |
255 | 264 | ||
256 | |||
257 | // }}} | 265 | // }}} |
258 | // CSS observer {{{ | 266 | // CSS observer {{{ |
259 | // Focus Map {{{ | 267 | // Focus Map {{{ |
260 | // Set focusArea | 268 | // Set focusArea |
261 | 269 | ||
262 | const mapFocusObserver = () => new MutationObserver((mutations) => { | 270 | const mapFocusObserver = () => |
263 | const mutation = mutations.at(-1) | 271 | new MutationObserver(mutations => { |
264 | const target = mutation.target | 272 | const mutation = mutations.at(-1); |
265 | const focus = target.getAttribute(mutation.attributeName).includes('focus') | 273 | const target = mutation.target; |
266 | const shouldBeInShowcase = focus && showcase.checkVisibility({ | 274 | const focus = target |
267 | contentVisibilityAuto: true, | 275 | .getAttribute(mutation.attributeName) |
268 | opacityProperty: true, | 276 | .includes("focus"); |
269 | visibilityProperty: true, | 277 | const shouldBeInShowcase = |
270 | }) | 278 | focus && |
271 | 279 | showcase.checkVisibility({ | |
272 | if (shouldBeInShowcase) { | 280 | contentVisibilityAuto: true, |
273 | if (showcase.contains(target)) return | 281 | opacityProperty: true, |
274 | 282 | visibilityProperty: true, | |
275 | // Placeholder for map in Showcase, it should has the same DOMRect | 283 | }); |
276 | const placeholder = target.cloneNode(true) | 284 | |
277 | placeholder.removeAttribute('id') | 285 | if (shouldBeInShowcase) { |
278 | placeholder.classList.remove('mapclay', 'focus') | 286 | if (showcase.contains(target)) return; |
279 | target.parentElement.replaceChild(placeholder, target) | 287 | |
280 | 288 | // Placeholder for map in Showcase, it should has the same DOMRect | |
281 | // FIXME Maybe use @start-style for CSS | 289 | const placeholder = target.cloneNode(true); |
282 | // Trigger CSS transition, if placeholde is the olny chil element in block, | 290 | placeholder.removeAttribute("id"); |
283 | // reduce its height to zero. | 291 | placeholder.classList.remove("mapclay", "focus"); |
284 | // To make sure the original height of placeholder is applied, DOM changes seems needed | 292 | target.parentElement.replaceChild(placeholder, target); |
285 | // then set data-attribute for CSS selector to change height to 0 | 293 | |
286 | placeholder.getBoundingClientRect() | 294 | // FIXME Maybe use @start-style for CSS |
287 | placeholder.setAttribute('data-placeholder', target.id) | 295 | // Trigger CSS transition, if placeholde is the olny chil element in block, |
288 | 296 | // reduce its height to zero. | |
289 | // To fit showcase, remove all inline style | 297 | // To make sure the original height of placeholder is applied, DOM changes seems needed |
290 | target.removeAttribute('style') | 298 | // then set data-attribute for CSS selector to change height to 0 |
291 | showcase.appendChild(target) | 299 | placeholder.getBoundingClientRect(); |
292 | 300 | placeholder.setAttribute("data-placeholder", target.id); | |
293 | // Resume rect from Semantic HTML to Showcase, with animation | 301 | |
294 | animateRectTransition(target, placeholder.getBoundingClientRect(), { | 302 | // To fit showcase, remove all inline style |
295 | duration: 300, | 303 | target.removeAttribute("style"); |
296 | resume: true | 304 | showcase.appendChild(target); |
297 | }) | 305 | |
298 | } else if (showcase.contains(target)) { | 306 | // Resume rect from Semantic HTML to Showcase, with animation |
299 | // Check placeholder is inside Semantic HTML | 307 | animateRectTransition(target, placeholder.getBoundingClientRect(), { |
300 | const placeholder = htmlHolder.querySelector(`[data-placeholder="${target.id}"]`) | 308 | duration: 300, |
301 | if (!placeholder) throw Error(`Cannot fine placeholder for map "${target.id}"`) | 309 | resume: true, |
302 | 310 | }); | |
303 | // Consider animation may fail, write callback | 311 | } else if (showcase.contains(target)) { |
304 | const afterAnimation = () => { | 312 | // Check placeholder is inside Semantic HTML |
305 | placeholder.parentElement.replaceChild(target, placeholder) | 313 | const placeholder = htmlHolder.querySelector( |
306 | target.style = placeholder.style.cssText | 314 | `[data-placeholder="${target.id}"]`, |
307 | placeholder.remove() | 315 | ); |
316 | if (!placeholder) | ||
317 | throw Error(`Cannot fine placeholder for map "${target.id}"`); | ||
318 | |||
319 | // Consider animation may fail, write callback | ||
320 | const afterAnimation = () => { | ||
321 | placeholder.parentElement.replaceChild(target, placeholder); | ||
322 | target.style = placeholder.style.cssText; | ||
323 | placeholder.remove(); | ||
324 | }; | ||
325 | |||
326 | // animation from Showcase to placeholder | ||
327 | animateRectTransition(target, placeholder.getBoundingClientRect(), { | ||
328 | duration: 300, | ||
329 | }).finished.finally(afterAnimation); | ||
308 | } | 330 | } |
309 | 331 | }); | |
310 | // animation from Showcase to placeholder | ||
311 | animateRectTransition(target, placeholder.getBoundingClientRect(), { duration: 300 }) | ||
312 | .finished | ||
313 | .finally(afterAnimation) | ||
314 | } | ||
315 | }) | ||
316 | // }}} | 332 | // }}} |
317 | // Layout {{{ | 333 | // Layout {{{ |
318 | // press key to switch layout | 334 | // press key to switch layout |
319 | const defaultLayout = layouts[0] | 335 | const defaultLayout = layouts[0]; |
320 | container.setAttribute("data-layout", defaultLayout.name) | 336 | container.setAttribute("data-layout", defaultLayout.name); |
321 | 337 | ||
322 | // observe layout change | 338 | // observe layout change |
323 | const layoutObserver = new MutationObserver((mutations) => { | 339 | const layoutObserver = new MutationObserver(mutations => { |
324 | const mutation = mutations.at(-1) | 340 | const mutation = mutations.at(-1); |
325 | const oldLayout = mutation.oldValue | 341 | const oldLayout = mutation.oldValue; |
326 | const newLayout = container.getAttribute(mutation.attributeName) | 342 | const newLayout = container.getAttribute(mutation.attributeName); |
327 | 343 | ||
328 | // Apply handler for leaving/entering layouts | 344 | // Apply handler for leaving/entering layouts |
329 | if (oldLayout) { | 345 | if (oldLayout) { |
330 | layouts.find(l => l.name === oldLayout) | 346 | layouts |
331 | ?.leaveHandler | 347 | .find(l => l.name === oldLayout) |
332 | ?.call(this, dumbymap) | 348 | ?.leaveHandler?.call(this, dumbymap); |
333 | } | 349 | } |
334 | 350 | ||
335 | Object.values(dumbymap) | 351 | Object.values(dumbymap) |
336 | .flat() | 352 | .flat() |
337 | .filter(ele => ele instanceof HTMLElement) | 353 | .filter(ele => ele instanceof HTMLElement) |
338 | .forEach(ele => ele.removeAttribute('style')) | 354 | .forEach(ele => ele.removeAttribute("style")); |
339 | 355 | ||
340 | if (newLayout) { | 356 | if (newLayout) { |
341 | layouts.find(l => l.name === newLayout) | 357 | layouts |
342 | ?.enterHandler | 358 | .find(l => l.name === newLayout) |
343 | ?.call(this, dumbymap) | 359 | ?.enterHandler?.call(this, dumbymap); |
344 | } | 360 | } |
345 | 361 | ||
346 | // Since layout change may show/hide showcase, the current focused map should do something | 362 | // Since layout change may show/hide showcase, the current focused map should do something |
347 | // Reset attribute triggers MutationObserver which is observing it | 363 | // Reset attribute triggers MutationObserver which is observing it |
348 | const focusMap = container.querySelector('.mapclay.focus') | 364 | const focusMap = |
349 | ?? container.querySelector('.mapclay') | 365 | container.querySelector(".mapclay.focus") ?? |
350 | focusMap?.classList?.add('focus') | 366 | container.querySelector(".mapclay"); |
367 | focusMap?.classList?.add("focus"); | ||
351 | }); | 368 | }); |
352 | layoutObserver.observe(container, { | 369 | layoutObserver.observe(container, { |
353 | attributes: true, | 370 | attributes: true, |
354 | attributeFilter: ["data-layout"], | 371 | attributeFilter: ["data-layout"], |
355 | attributeOldValue: true, | 372 | attributeOldValue: true, |
356 | characterDataOldValue: true | 373 | characterDataOldValue: true, |
357 | }); | 374 | }); |
358 | 375 | ||
359 | onRemove(htmlHolder, () => layoutObserver.disconnect()) | 376 | onRemove(htmlHolder, () => layoutObserver.disconnect()); |
360 | //}}} | 377 | //}}} |
361 | //}}} | 378 | //}}} |
362 | // Render Maps {{{ | 379 | // Render Maps {{{ |
363 | 380 | ||
364 | const afterMapRendered = (renderer) => { | 381 | const afterMapRendered = renderer => { |
365 | const mapElement = renderer.target | 382 | const mapElement = renderer.target; |
366 | mapElement.setAttribute('tabindex', "-1") | 383 | mapElement.setAttribute("tabindex", "-1"); |
367 | if (mapElement.getAttribute('data-render') === 'fulfilled') { | 384 | if (mapElement.getAttribute("data-render") === "fulfilled") { |
368 | mapCache[mapElement.id] = renderer | 385 | mapCache[mapElement.id] = renderer; |
369 | } | 386 | } |
370 | 387 | ||
371 | // Execute callback from caller | 388 | // Execute callback from caller |
372 | mapCallback?.call(this, mapElement) | 389 | mapCallback?.call(this, mapElement); |
373 | const markers = geoLinks | 390 | const markers = geoLinks |
374 | .filter(link => !link.targets || link.targets.includes(mapElement.id)) | 391 | .filter(link => !link.targets || link.targets.includes(mapElement.id)) |
375 | .map(link => ({ xy: link.xy, title: link.url.pathname })) | 392 | .map(link => ({ xy: link.xy, title: link.url.pathname })); |
376 | 393 | ||
377 | // Add markers with Geolinks | 394 | // Add markers with Geolinks |
378 | renderer.addMarkers(markers) | 395 | renderer.addMarkers(markers); |
379 | mapElement.querySelectorAll('.marker') | 396 | mapElement |
380 | .forEach(marker => htmlHolder.anchors.push(marker)) | 397 | .querySelectorAll(".marker") |
398 | .forEach(marker => htmlHolder.anchors.push(marker)); | ||
381 | 399 | ||
382 | // Work with Mutation Observer | 400 | // Work with Mutation Observer |
383 | const observer = mapFocusObserver() | 401 | const observer = mapFocusObserver(); |
384 | mapFocusObserver().observe(mapElement, { | 402 | mapFocusObserver().observe(mapElement, { |
385 | attributes: true, | 403 | attributes: true, |
386 | attributeFilter: ["class"], | 404 | attributeFilter: ["class"], |
387 | attributeOldValue: true | 405 | attributeOldValue: true, |
388 | }); | 406 | }); |
389 | onRemove(mapElement, () => observer.disconnect()) | 407 | onRemove(mapElement, () => observer.disconnect()); |
390 | } | 408 | }; |
391 | 409 | ||
392 | // Set unique ID for map container | 410 | // Set unique ID for map container |
393 | const mapIdList = [] | 411 | const mapIdList = []; |
394 | const assignMapId = (config) => { | 412 | const assignMapId = config => { |
395 | let mapId = config.id | 413 | let mapId = config.id; |
396 | if (!mapId) { | 414 | if (!mapId) { |
397 | mapId = config.use?.split('/')?.at(-1) | 415 | mapId = config.use?.split("/")?.at(-1); |
398 | let counter = 1 | 416 | let counter = 1; |
399 | while (!mapId || mapIdList.includes(mapId)) { | 417 | while (!mapId || mapIdList.includes(mapId)) { |
400 | mapId = `${config.use ?? "unnamed"}-${counter}` | 418 | mapId = `${config.use ?? "unnamed"}-${counter}`; |
401 | counter++ | 419 | counter++; |
402 | } | 420 | } |
403 | config.id = mapId | 421 | config.id = mapId; |
404 | } | 422 | } |
405 | mapIdList.push(mapId) | 423 | mapIdList.push(mapId); |
406 | return config | 424 | return config; |
407 | } | 425 | }; |
408 | 426 | ||
409 | // Render each code block with "language-map" class | 427 | // Render each code block with "language-map" class |
410 | const elementsWithMapConfig = Array.from(container.querySelectorAll('pre:has(.language-map)') ?? []) | 428 | const elementsWithMapConfig = Array.from( |
429 | container.querySelectorAll("pre:has(.language-map)") ?? [], | ||
430 | ); | ||
411 | // Add default aliases into each config | 431 | // Add default aliases into each config |
412 | const configConverter = (config => ({ | 432 | const configConverter = config => ({ |
413 | use: config.use ?? 'Leaflet', | 433 | use: config.use ?? "Leaflet", |
414 | width: "100%", | 434 | width: "100%", |
415 | ...config, | 435 | ...config, |
416 | aliases: { | 436 | aliases: { |
417 | ...defaultAliases, | 437 | ...defaultAliases, |
418 | ...config.aliases ?? {} | 438 | ...(config.aliases ?? {}), |
419 | }, | 439 | }, |
420 | })) | 440 | }); |
421 | const render = renderWith(configConverter) | 441 | const render = renderWith(configConverter); |
422 | elementsWithMapConfig | 442 | elementsWithMapConfig.forEach(target => { |
423 | .forEach(target => { | 443 | // Get text in code block starts with markdown text '```map' |
424 | // Get text in code block starts with markdown text '```map' | 444 | const configText = target |
425 | const configText = target.querySelector('.language-map') | 445 | .querySelector(".language-map") |
426 | .textContent | 446 | .textContent // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content |
427 | // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content | 447 | // replace it by normal space |
428 | // replace it by normal space | 448 | .replace(/\u00A0/g, "\u0020"); |
429 | .replace(/\u00A0/g, '\u0020') | 449 | |
430 | 450 | let configList = []; | |
431 | let configList = [] | 451 | try { |
432 | try { | 452 | configList = parseConfigsFromYaml(configText).map(assignMapId); |
433 | configList = parseConfigsFromYaml(configText).map(assignMapId) | 453 | } catch (_) { |
434 | } catch (_) { | 454 | console.warn("Fail to parse yaml config for element", target); |
435 | console.warn('Fail to parse yaml config for element', target) | 455 | return; |
436 | return | 456 | } |
437 | } | ||
438 | 457 | ||
439 | // If map in cache has the same ID, just put it into target | 458 | // If map in cache has the same ID, just put it into target |
440 | // So user won't feel anything changes when editing markdown | 459 | // So user won't feel anything changes when editing markdown |
441 | configList.forEach(config => { | 460 | configList.forEach(config => { |
442 | const cache = mapCache[config.id] | 461 | const cache = mapCache[config.id]; |
443 | if (!cache) return; | 462 | if (!cache) return; |
444 | 463 | ||
445 | target.appendChild(cache.target) | 464 | target.appendChild(cache.target); |
446 | config.target = cache.target | 465 | config.target = cache.target; |
447 | }) | 466 | }); |
448 | 467 | ||
449 | // trivial: if map cache is applied, do not show yaml text | 468 | // trivial: if map cache is applied, do not show yaml text |
450 | if (target.querySelector('.mapclay')) { | 469 | if (target.querySelector(".mapclay")) { |
451 | target.querySelectorAll(':scope > :not([data-render=fulfilled])') | 470 | target |
452 | .forEach(e => e.remove()) | 471 | .querySelectorAll(":scope > :not([data-render=fulfilled])") |
453 | } | 472 | .forEach(e => e.remove()); |
473 | } | ||
454 | 474 | ||
455 | // Render maps with delay | 475 | // Render maps with delay |
456 | const timer = setTimeout(() => | 476 | const timer = setTimeout( |
477 | () => | ||
457 | render(target, configList).forEach(renderMap => { | 478 | render(target, configList).forEach(renderMap => { |
458 | renderMaps.push(renderMap) | 479 | renderMaps.push(renderMap); |
459 | renderMap.then(afterMapRendered) | 480 | renderMap.then(afterMapRendered); |
460 | }), | 481 | }), |
461 | delay ?? 1000 | 482 | delay ?? 1000, |
462 | ) | 483 | ); |
463 | onRemove(htmlHolder, () => { | 484 | onRemove(htmlHolder, () => { |
464 | clearTimeout(timer) | 485 | clearTimeout(timer); |
465 | }) | 486 | }); |
466 | }) | 487 | }); |
467 | 488 | ||
468 | //}}} | 489 | //}}} |
469 | return Object.seal(dumbymap) | 490 | return Object.seal(dumbymap); |
470 | } | 491 | }; |
diff --git a/src/editor.mjs b/src/editor.mjs index c04ba24..73177b6 100644 --- a/src/editor.mjs +++ b/src/editor.mjs | |||
@@ -1,143 +1,170 @@ | |||
1 | /*global EasyMDE*/ | 1 | /*global EasyMDE*/ |
2 | /*eslint no-undef: "error"*/ | 2 | /*eslint no-undef: "error"*/ |
3 | import { markdown2HTML, generateMaps } from './dumbymap' | 3 | import { markdown2HTML, generateMaps } from "./dumbymap"; |
4 | import { defaultAliases, parseConfigsFromYaml } from 'mapclay' | 4 | import { defaultAliases, parseConfigsFromYaml } from "mapclay"; |
5 | import * as menuItem from './MenuItem' | 5 | import * as menuItem from "./MenuItem"; |
6 | 6 | ||
7 | // Set up Containers {{{ | 7 | // Set up Containers {{{ |
8 | 8 | ||
9 | const HtmlContainer = document.querySelector(".DumbyMap") | 9 | const HtmlContainer = document.querySelector(".DumbyMap"); |
10 | const textArea = document.querySelector(".editor textarea") | 10 | const textArea = document.querySelector(".editor textarea"); |
11 | let dumbymap | 11 | let dumbymap; |
12 | 12 | ||
13 | const toggleEditing = () => { | 13 | const 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 {{{ |
23 | 23 | ||
24 | // Content values for editor | 24 | // Content values for editor |
25 | 25 | ||
26 | const defaultContent = '## Links\n\n- [Go to marker](geo:24,121?id=foo,leaflet&text=normal "Link Test")\n\n```map\nid: foo\nuse: Maplibre\n```\n' | 26 | const defaultContent = |
27 | '## Links\n\n- [Go to marker](geo:24,121?id=foo,leaflet&text=normal "Link Test")\n\n```map\nid: foo\nuse: Maplibre\n```\n'; | ||
27 | const editor = new EasyMDE({ | 28 | const editor = new EasyMDE({ |
28 | element: textArea, | 29 | element: textArea, |
29 | initialValue: defaultContent, | 30 | initialValue: defaultContent, |
30 | autosave: { | 31 | autosave: { |
31 | enabled: true, | 32 | enabled: true, |
32 | uniqueId: 'dumbymap', | 33 | uniqueId: "dumbymap", |
33 | }, | 34 | }, |
34 | indentWithTabs: false, | 35 | indentWithTabs: false, |
35 | lineNumbers: true, | 36 | lineNumbers: true, |
36 | promptURLs: true, | 37 | promptURLs: true, |
37 | uploadImage: true, | 38 | uploadImage: true, |
38 | spellChecker: false, | 39 | spellChecker: false, |
39 | toolbarButtonClassPrefix: 'mde', | 40 | toolbarButtonClassPrefix: "mde", |
40 | status: false, | 41 | status: false, |
41 | shortcuts: { | 42 | shortcuts: { |
42 | "map": "Ctrl-Alt-M", | 43 | map: "Ctrl-Alt-M", |
43 | "debug": "Ctrl-Alt-D", | 44 | debug: "Ctrl-Alt-D", |
44 | "toggleUnorderedList": null, | 45 | toggleUnorderedList: null, |
45 | "toggleOrderedList": null, | 46 | toggleOrderedList: null, |
46 | }, | 47 | }, |
47 | toolbar: [ | 48 | toolbar: [ |
48 | { | 49 | { |
49 | name: 'map', | 50 | name: "map", |
50 | title: 'Toggle Map Generation', | 51 | title: "Toggle Map Generation", |
51 | text: "🌏", | 52 | text: "🌏", |
52 | action: () => toggleEditing(), | 53 | action: () => toggleEditing(), |
53 | }, | 54 | }, |
54 | { | 55 | { |
55 | name: 'debug', | 56 | name: "debug", |
56 | title: 'Save content as URL', | 57 | title: "Save content as URL", |
57 | text: "🤔", | 58 | text: "🤔", |
58 | action: () => { | 59 | action: () => { |
59 | const state = { content: editor.value() } | 60 | const state = { content: editor.value() }; |
60 | window.location.hash = encodeURIComponent(JSON.stringify(state)) | 61 | window.location.hash = encodeURIComponent(JSON.stringify(state)); |
61 | navigator.clipboard.writeText(window.location.href) | 62 | navigator.clipboard.writeText(window.location.href); |
62 | alert('URL copied to clipboard') | 63 | alert("URL copied to clipboard"); |
63 | }, | 64 | }, |
64 | }, 'undo', 'redo', '|', 'heading-1', 'heading-2', '|', 'link', 'image', '|', 'bold', 'italic', 'strikethrough', 'code', 'clean-block', '|', 'unordered-list', 'ordered-list', 'quote', 'table' | 65 | }, |
66 | "undo", | ||
67 | "redo", | ||
68 | "|", | ||
69 | "heading-1", | ||
70 | "heading-2", | ||
71 | "|", | ||
72 | "link", | ||
73 | "image", | ||
74 | "|", | ||
75 | "bold", | ||
76 | "italic", | ||
77 | "strikethrough", | ||
78 | "code", | ||
79 | "clean-block", | ||
80 | "|", | ||
81 | "unordered-list", | ||
82 | "ordered-list", | ||
83 | "quote", | ||
84 | "table", | ||
65 | ], | 85 | ], |
66 | }); | 86 | }); |
67 | 87 | ||
68 | const cm = editor.codemirror | 88 | const cm = editor.codemirror; |
69 | 89 | ||
70 | const getStateFromHash = (hash) => { | 90 | const getStateFromHash = hash => { |
71 | const hashValue = hash.substring(1); | 91 | const hashValue = hash.substring(1); |
72 | const stateString = decodeURIComponent(hashValue) | 92 | const stateString = decodeURIComponent(hashValue); |
73 | try { return JSON.parse(stateString) ?? {} } | 93 | try { |
74 | catch (_) { return {} } | 94 | return JSON.parse(stateString) ?? {}; |
75 | } | 95 | } catch (_) { |
96 | return {}; | ||
97 | } | ||
98 | }; | ||
76 | 99 | ||
77 | const getContentFromHash = (hash) => { | 100 | const getContentFromHash = hash => { |
78 | const state = getStateFromHash(hash) | 101 | const state = getStateFromHash(hash); |
79 | return state.content | 102 | return state.content; |
80 | } | 103 | }; |
81 | 104 | ||
82 | const initialState = getStateFromHash(window.location.hash) | 105 | const initialState = getStateFromHash(window.location.hash); |
83 | window.location.hash = '' | 106 | window.location.hash = ""; |
84 | const contentFromHash = initialState.content | 107 | const contentFromHash = initialState.content; |
85 | 108 | ||
86 | // Seems like autosave would overwrite initialValue, set content from hash here | 109 | // Seems like autosave would overwrite initialValue, set content from hash here |
87 | if (contentFromHash) { | 110 | if (contentFromHash) { |
88 | editor.cleanup() | 111 | editor.cleanup(); |
89 | editor.value(contentFromHash) | 112 | editor.value(contentFromHash); |
90 | } | 113 | } |
91 | // }}} | 114 | // }}} |
92 | // Set up logic about editor content {{{ | 115 | // Set up logic about editor content {{{ |
93 | const afterMapRendered = (_) => { | 116 | const afterMapRendered = _ => { |
94 | // mapHolder.oncontextmenu = (event) => { | 117 | // mapHolder.oncontextmenu = (event) => { |
95 | // event.preventDefault() | 118 | // event.preventDefault() |
96 | // const lonLat = mapHolder.renderer.unproject([event.x, event.y]) | 119 | // const lonLat = mapHolder.renderer.unproject([event.x, event.y]) |
97 | // // TODO... | 120 | // // TODO... |
98 | // } | 121 | // } |
99 | } | 122 | }; |
100 | markdown2HTML(HtmlContainer, editor.value()) | 123 | markdown2HTML(HtmlContainer, editor.value()); |
101 | dumbymap = generateMaps(HtmlContainer, afterMapRendered) | 124 | dumbymap = generateMaps(HtmlContainer, afterMapRendered); |
102 | 125 | ||
103 | // Quick hack to style lines inside code block | 126 | // Quick hack to style lines inside code block |
104 | const addClassToCodeLines = () => { | 127 | const addClassToCodeLines = () => { |
105 | const lines = cm.getLineHandle(0).parent.lines | 128 | const lines = cm.getLineHandle(0).parent.lines; |
106 | let insideCodeBlock = false | 129 | let insideCodeBlock = false; |
107 | lines.forEach((line, index) => { | 130 | lines.forEach((line, index) => { |
108 | if (line.text.match(/^[\u0060]{3}/)) { | 131 | if (line.text.match(/^[\u0060]{3}/)) { |
109 | insideCodeBlock = !insideCodeBlock | 132 | insideCodeBlock = !insideCodeBlock; |
110 | } else if (insideCodeBlock) { | 133 | } else if (insideCodeBlock) { |
111 | cm.addLineClass(index, "text", "inside-code-block") | 134 | cm.addLineClass(index, "text", "inside-code-block"); |
112 | } else { | 135 | } else { |
113 | cm.removeLineClass(index, "text", "inside-code-block") | 136 | cm.removeLineClass(index, "text", "inside-code-block"); |
114 | } | 137 | } |
115 | }) | 138 | }); |
116 | } | 139 | }; |
117 | addClassToCodeLines() | 140 | addClassToCodeLines(); |
118 | 141 | ||
119 | const completeForCodeBlock = (change) => { | 142 | const completeForCodeBlock = change => { |
120 | const line = change.to.line | 143 | const line = change.to.line; |
121 | if (change.origin === "+input") { | 144 | if (change.origin === "+input") { |
122 | const text = change.text[0] | 145 | const text = change.text[0]; |
123 | 146 | ||
124 | // Completion for YAML doc separator | 147 | // Completion for YAML doc separator |
125 | if (text === "-" && change.to.ch === 0 && insideCodeblockForMap(cm.getCursor())) { | 148 | if ( |
126 | cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 1 }) | 149 | text === "-" && |
127 | cm.replaceSelection(text.repeat(3) + '\n') | 150 | change.to.ch === 0 && |
151 | insideCodeblockForMap(cm.getCursor()) | ||
152 | ) { | ||
153 | cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 1 }); | ||
154 | cm.replaceSelection(text.repeat(3) + "\n"); | ||
128 | } | 155 | } |
129 | 156 | ||
130 | // Completion for Code fence | 157 | // Completion for Code fence |
131 | if (text === "`" && change.to.ch === 0) { | 158 | if (text === "`" && change.to.ch === 0) { |
132 | cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 1 }) | 159 | cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 1 }); |
133 | cm.replaceSelection(text.repeat(3)) | 160 | cm.replaceSelection(text.repeat(3)); |
134 | const numberOfFences = cm.getValue() | 161 | const numberOfFences = cm |
135 | .split('\n') | 162 | .getValue() |
136 | .filter(line => line.match(/[\u0060]{3}/)) | 163 | .split("\n") |
137 | .length | 164 | .filter(line => line.match(/[\u0060]{3}/)).length; |
138 | if (numberOfFences % 2 === 1) { | 165 | if (numberOfFences % 2 === 1) { |
139 | cm.replaceSelection('map\n\n```') | 166 | cm.replaceSelection("map\n\n```"); |
140 | cm.setCursor({ line: line + 1 }) | 167 | cm.setCursor({ line: line + 1 }); |
141 | } | 168 | } |
142 | } | 169 | } |
143 | } | 170 | } |
@@ -145,63 +172,63 @@ const completeForCodeBlock = (change) => { | |||
145 | // For YAML doc separator, <hr> and code fence | 172 | // For YAML doc separator, <hr> and code fence |
146 | // Auto delete to start of line | 173 | // Auto delete to start of line |
147 | if (change.origin === "+delete") { | 174 | if (change.origin === "+delete") { |
148 | const match = change.removed[0].match(/^[-\u0060]$/)?.at(0) | 175 | const match = change.removed[0].match(/^[-\u0060]$/)?.at(0); |
149 | if (match && cm.getLine(line) === match.repeat(2) && match) { | 176 | if (match && cm.getLine(line) === match.repeat(2) && match) { |
150 | cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 2 }) | 177 | cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 2 }); |
151 | cm.replaceSelection('') | 178 | cm.replaceSelection(""); |
152 | } | 179 | } |
153 | } | 180 | } |
154 | } | 181 | }; |
155 | 182 | ||
156 | const debounceForMap = (() => { | 183 | const debounceForMap = (() => { |
157 | let timer = null; | 184 | let timer = null; |
158 | 185 | ||
159 | return function(...args) { | 186 | return function (...args) { |
160 | dumbymap = generateMaps.apply(this, args) | 187 | dumbymap = generateMaps.apply(this, args); |
161 | // clearTimeout(timer); | 188 | // clearTimeout(timer); |
162 | // timer = setTimeout(() => { | 189 | // timer = setTimeout(() => { |
163 | // dumbymap = generateMaps.apply(this, args) | 190 | // dumbymap = generateMaps.apply(this, args) |
164 | // }, 10); | 191 | // }, 10); |
165 | } | 192 | }; |
166 | })() | 193 | })(); |
167 | 194 | ||
168 | const updateDumbyMap = () => { | 195 | const updateDumbyMap = () => { |
169 | markdown2HTML(HtmlContainer, editor.value()) | 196 | markdown2HTML(HtmlContainer, editor.value()); |
170 | // TODO Test if generate maps intantly is OK with map cache | 197 | // TODO Test if generate maps intantly is OK with map cache |
171 | // debounceForMap(HtmlContainer, afterMapRendered) | 198 | // debounceForMap(HtmlContainer, afterMapRendered) |
172 | dumbymap = generateMaps(HtmlContainer, afterMapRendered) | 199 | dumbymap = generateMaps(HtmlContainer, afterMapRendered); |
173 | } | 200 | }; |
174 | 201 | ||
175 | updateDumbyMap() | 202 | updateDumbyMap(); |
176 | 203 | ||
177 | // Re-render HTML by editor content | 204 | // Re-render HTML by editor content |
178 | cm.on("change", (_, change) => { | 205 | cm.on("change", (_, change) => { |
179 | updateDumbyMap() | 206 | updateDumbyMap(); |
180 | addClassToCodeLines() | 207 | addClassToCodeLines(); |
181 | completeForCodeBlock(change) | 208 | completeForCodeBlock(change); |
182 | }) | 209 | }); |
183 | 210 | ||
184 | // Set class for focus | 211 | // Set class for focus |
185 | cm.on("focus", () => { | 212 | cm.on("focus", () => { |
186 | cm.getWrapperElement().classList.add('focus') | 213 | cm.getWrapperElement().classList.add("focus"); |
187 | HtmlContainer.classList.remove('focus') | 214 | HtmlContainer.classList.remove("focus"); |
188 | }) | 215 | }); |
189 | 216 | ||
190 | cm.on("beforeChange", (_, change) => { | 217 | cm.on("beforeChange", (_, change) => { |
191 | const line = change.to.line | 218 | const line = change.to.line; |
192 | // Don't allow more content after YAML doc separator | 219 | // Don't allow more content after YAML doc separator |
193 | if (change.origin.match(/^(\+input|paste)$/)) { | 220 | if (change.origin.match(/^(\+input|paste)$/)) { |
194 | if (cm.getLine(line) === "---" && change.text[0] !== "") { | 221 | if (cm.getLine(line) === "---" && change.text[0] !== "") { |
195 | change.cancel() | 222 | change.cancel(); |
196 | } | 223 | } |
197 | } | 224 | } |
198 | }) | 225 | }); |
199 | 226 | ||
200 | // Reload editor content by hash value | 227 | // Reload editor content by hash value |
201 | window.onhashchange = () => { | 228 | window.onhashchange = () => { |
202 | const content = getContentFromHash(window.location.hash) | 229 | const content = getContentFromHash(window.location.hash); |
203 | if (content) editor.value(content) | 230 | if (content) editor.value(content); |
204 | } | 231 | }; |
205 | 232 | ||
206 | // FIXME DEBUGONLY | 233 | // FIXME DEBUGONLY |
207 | // generateMaps(HtmlContainer) | 234 | // generateMaps(HtmlContainer) |
@@ -212,401 +239,415 @@ window.onhashchange = () => { | |||
212 | // }}} | 239 | // }}} |
213 | // Completion in Code Blok {{{ | 240 | // Completion in Code Blok {{{ |
214 | // Elements about suggestions {{{ | 241 | // Elements about suggestions {{{ |
215 | const menu = document.createElement('div') | 242 | const menu = document.createElement("div"); |
216 | menu.id = 'menu' | 243 | menu.id = "menu"; |
217 | menu.onclick = () => menu.style.display = 'none' | 244 | menu.onclick = () => (menu.style.display = "none"); |
218 | document.body.append(menu) | 245 | document.body.append(menu); |
219 | 246 | ||
220 | const rendererOptions = {} | 247 | const rendererOptions = {}; |
221 | 248 | ||
222 | // }}} | 249 | // }}} |
223 | // Aliases for map options {{{ | 250 | // Aliases for map options {{{ |
224 | const aliasesForMapOptions = {} | 251 | const aliasesForMapOptions = {}; |
225 | const defaultApply = './dist/default.yml' | 252 | const defaultApply = "./dist/default.yml"; |
226 | fetch(defaultApply) | 253 | fetch(defaultApply) |
227 | .then(res => res.text()) | 254 | .then(res => res.text()) |
228 | .then(rawText => { | 255 | .then(rawText => { |
229 | const config = parseConfigsFromYaml(rawText)?.at(0) | 256 | const config = parseConfigsFromYaml(rawText)?.at(0); |
230 | Object.assign(aliasesForMapOptions, config.aliases ?? {}) | 257 | Object.assign(aliasesForMapOptions, config.aliases ?? {}); |
231 | }) | 258 | }) |
232 | .catch(err => console.warn(`Fail to get aliases from ${defaultApply}`, err)) | 259 | .catch(err => console.warn(`Fail to get aliases from ${defaultApply}`, err)); |
233 | // }}} | 260 | // }}} |
234 | // FUNCTION: Check if current token is inside code block {{{ | 261 | // FUNCTION: Check if current token is inside code block {{{ |
235 | const insideCodeblockForMap = (anchor) => { | 262 | const insideCodeblockForMap = anchor => { |
236 | const token = cm.getTokenAt(anchor) | 263 | const token = cm.getTokenAt(anchor); |
237 | const insideCodeBlock = token.state.overlay.codeBlock && !cm.getLine(anchor.line).match(/^[\u0060]{3}/) | 264 | const insideCodeBlock = |
238 | if (!insideCodeBlock) return false | 265 | token.state.overlay.codeBlock && |
239 | 266 | !cm.getLine(anchor.line).match(/^[\u0060]{3}/); | |
240 | let line = anchor.line - 1 | 267 | if (!insideCodeBlock) return false; |
268 | |||
269 | let line = anchor.line - 1; | ||
241 | while (line >= 0) { | 270 | while (line >= 0) { |
242 | const content = cm.getLine(line) | 271 | const content = cm.getLine(line); |
243 | if (content === '```map') { | 272 | if (content === "```map") { |
244 | return true | 273 | return true; |
245 | } else if (content === '```') { | 274 | } else if (content === "```") { |
246 | return false | 275 | return false; |
247 | } | 276 | } |
248 | line = line - 1 | 277 | line = line - 1; |
249 | } | 278 | } |
250 | return false | 279 | return false; |
251 | } | 280 | }; |
252 | // }}} | 281 | // }}} |
253 | // FUNCTION: Get Renderer by cursor position in code block {{{ | 282 | // FUNCTION: Get Renderer by cursor position in code block {{{ |
254 | const getLineWithRenderer = (anchor) => { | 283 | const getLineWithRenderer = anchor => { |
255 | const currentLine = anchor.line | 284 | const currentLine = anchor.line; |
256 | if (!cm.getLine) return null | 285 | if (!cm.getLine) return null; |
257 | 286 | ||
258 | const match = (line) => cm.getLine(line).match(/^use: /) | 287 | const match = line => cm.getLine(line).match(/^use: /); |
259 | |||
260 | if (match(currentLine)) return currentLine | ||
261 | 288 | ||
289 | if (match(currentLine)) return currentLine; | ||
262 | 290 | ||
263 | // Look backward/forward for pattern of used renderer: /use: .+/ | 291 | // Look backward/forward for pattern of used renderer: /use: .+/ |
264 | let pl = currentLine - 1 | 292 | let pl = currentLine - 1; |
265 | while (pl > 0 && insideCodeblockForMap(anchor)) { | 293 | while (pl > 0 && insideCodeblockForMap(anchor)) { |
266 | const text = cm.getLine(pl) | 294 | const text = cm.getLine(pl); |
267 | if (match(pl)) { | 295 | if (match(pl)) { |
268 | return pl | 296 | return pl; |
269 | } else if (text.match(/^---|^[\u0060]{3}/)) { | 297 | } else if (text.match(/^---|^[\u0060]{3}/)) { |
270 | break | 298 | break; |
271 | } | 299 | } |
272 | pl = pl - 1 | 300 | pl = pl - 1; |
273 | } | 301 | } |
274 | 302 | ||
275 | let nl = currentLine + 1 | 303 | let nl = currentLine + 1; |
276 | while (insideCodeblockForMap(anchor)) { | 304 | while (insideCodeblockForMap(anchor)) { |
277 | const text = cm.getLine(nl) | 305 | const text = cm.getLine(nl); |
278 | if (match(nl)) { | 306 | if (match(nl)) { |
279 | return nl | 307 | return nl; |
280 | } else if (text.match(/^---|^[\u0060]{3}/)) { | 308 | } else if (text.match(/^---|^[\u0060]{3}/)) { |
281 | return null | 309 | return null; |
282 | } | 310 | } |
283 | nl = nl + 1 | 311 | nl = nl + 1; |
284 | } | 312 | } |
285 | 313 | ||
286 | return null | 314 | return null; |
287 | } | 315 | }; |
288 | // }}} | 316 | // }}} |
289 | // FUNCTION: Return suggestions for valid options {{{ | 317 | // FUNCTION: Return suggestions for valid options {{{ |
290 | const getSuggestionsForOptions = (optionTyped, validOptions) => { | 318 | const getSuggestionsForOptions = (optionTyped, validOptions) => { |
291 | let suggestOptions = [] | 319 | let suggestOptions = []; |
292 | 320 | ||
293 | const matchedOptions = validOptions | 321 | const matchedOptions = validOptions.filter(o => |
294 | .filter(o => o.valueOf().toLowerCase().includes(optionTyped.toLowerCase())) | 322 | o.valueOf().toLowerCase().includes(optionTyped.toLowerCase()), |
323 | ); | ||
295 | 324 | ||
296 | if (matchedOptions.length > 0) { | 325 | if (matchedOptions.length > 0) { |
297 | suggestOptions = matchedOptions | 326 | suggestOptions = matchedOptions; |
298 | } else { | 327 | } else { |
299 | suggestOptions = validOptions | 328 | suggestOptions = validOptions; |
300 | } | 329 | } |
301 | 330 | ||
302 | return suggestOptions | 331 | return suggestOptions.map( |
303 | .map(o => new menuItem.Suggestion({ | 332 | o => |
304 | text: `<span>${o.valueOf()}</span><span class='info' title="${o.desc ?? ''}">ⓘ</span>`, | 333 | new menuItem.Suggestion({ |
305 | replace: `${o.valueOf()}: `, | 334 | text: `<span>${o.valueOf()}</span><span class='info' title="${o.desc ?? ""}">ⓘ</span>`, |
306 | })) | 335 | replace: `${o.valueOf()}: `, |
307 | } | 336 | }), |
337 | ); | ||
338 | }; | ||
308 | // }}} | 339 | // }}} |
309 | // FUNCTION: Return suggestion for example of option value {{{ | 340 | // FUNCTION: Return suggestion for example of option value {{{ |
310 | const getSuggestionFromMapOption = (option) => { | 341 | const getSuggestionFromMapOption = option => { |
311 | if (!option.example) return null | 342 | if (!option.example) return null; |
312 | 343 | ||
313 | const text = option.example_desc | 344 | const text = option.example_desc |
314 | ? `<span>${option.example_desc}</span><span class="truncate"style="color: gray">${option.example}</span>` | 345 | ? `<span>${option.example_desc}</span><span class="truncate"style="color: gray">${option.example}</span>` |
315 | : `<span>${option.example}</span>` | 346 | : `<span>${option.example}</span>`; |
316 | 347 | ||
317 | return new menuItem.Suggestion({ | 348 | return new menuItem.Suggestion({ |
318 | text: text, | 349 | text: text, |
319 | replace: `${option.valueOf()}: ${option.example ?? ""}`, | 350 | replace: `${option.valueOf()}: ${option.example ?? ""}`, |
320 | }) | 351 | }); |
321 | } | 352 | }; |
322 | // }}} | 353 | // }}} |
323 | // FUNCTION: Return suggestions from aliases {{{ | 354 | // FUNCTION: Return suggestions from aliases {{{ |
324 | const getSuggestionsFromAliases = (option) => Object.entries(aliasesForMapOptions[option.valueOf()] ?? {}) | 355 | const getSuggestionsFromAliases = option => |
325 | ?.map(record => { | 356 | Object.entries(aliasesForMapOptions[option.valueOf()] ?? {})?.map(record => { |
326 | const [alias, value] = record | 357 | const [alias, value] = record; |
327 | const valueString = JSON.stringify(value).replaceAll('"', '') | 358 | const valueString = JSON.stringify(value).replaceAll('"', ""); |
328 | return new menuItem.Suggestion({ | 359 | return new menuItem.Suggestion({ |
329 | 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>`, |
330 | replace: `${option.valueOf()}: ${valueString}`, | 361 | replace: `${option.valueOf()}: ${valueString}`, |
331 | }) | 362 | }); |
332 | }) | 363 | }) ?? []; |
333 | ?? [] | ||
334 | // }}} | 364 | // }}} |
335 | // FUCNTION: Handler for map codeblock {{{ | 365 | // FUCNTION: Handler for map codeblock {{{ |
336 | const handleTypingInCodeBlock = (anchor) => { | 366 | const handleTypingInCodeBlock = anchor => { |
337 | const text = cm.getLine(anchor.line) | 367 | const text = cm.getLine(anchor.line); |
338 | if (text.match(/^\s\+$/) && text.length % 2 !== 0) { | 368 | if (text.match(/^\s\+$/) && text.length % 2 !== 0) { |
339 | // TODO Completion for even number of spaces | 369 | // TODO Completion for even number of spaces |
340 | } else if (text.match(/^-/)) { | 370 | } else if (text.match(/^-/)) { |
341 | // TODO Completion for YAML doc separator | 371 | // TODO Completion for YAML doc separator |
342 | } else { | 372 | } else { |
343 | const suggestions = getSuggestions(anchor) | 373 | const suggestions = getSuggestions(anchor); |
344 | addSuggestions(anchor, suggestions) | 374 | addSuggestions(anchor, suggestions); |
345 | } | 375 | } |
346 | } | 376 | }; |
347 | // }}} | 377 | // }}} |
348 | // FUNCTION: get suggestions by current input {{{ | 378 | // FUNCTION: get suggestions by current input {{{ |
349 | const getSuggestions = (anchor) => { | 379 | const getSuggestions = anchor => { |
350 | const text = cm.getLine(anchor.line) | 380 | const text = cm.getLine(anchor.line); |
351 | 381 | ||
352 | // Clear marks on text | 382 | // Clear marks on text |
353 | cm.findMarks( | 383 | cm.findMarks({ ...anchor, ch: 0 }, { ...anchor, ch: text.length }).forEach( |
354 | { ...anchor, ch: 0 }, | 384 | m => m.clear(), |
355 | { ...anchor, ch: text.length } | 385 | ); |
356 | ).forEach(m => m.clear()) | ||
357 | 386 | ||
358 | // Mark user input invalid by case | 387 | // Mark user input invalid by case |
359 | const markInputIsInvalid = () => | 388 | const markInputIsInvalid = () => |
360 | cm.getDoc().markText( | 389 | cm |
361 | { ...anchor, ch: 0 }, | 390 | .getDoc() |
362 | { ...anchor, ch: text.length }, | 391 | .markText( |
363 | { className: 'invalid-input' }, | 392 | { ...anchor, ch: 0 }, |
364 | ) | 393 | { ...anchor, ch: text.length }, |
394 | { className: "invalid-input" }, | ||
395 | ); | ||
365 | 396 | ||
366 | // Check if "use: <renderer>" is set | 397 | // Check if "use: <renderer>" is set |
367 | const lineWithRenderer = getLineWithRenderer(anchor) | 398 | const lineWithRenderer = getLineWithRenderer(anchor); |
368 | const renderer = lineWithRenderer | 399 | const renderer = lineWithRenderer |
369 | ? cm.getLine(lineWithRenderer).split(' ')[1] | 400 | ? cm.getLine(lineWithRenderer).split(" ")[1] |
370 | : null | 401 | : null; |
371 | if (renderer && anchor.line !== lineWithRenderer) { | 402 | if (renderer && anchor.line !== lineWithRenderer) { |
372 | // Do not check properties | 403 | // Do not check properties |
373 | if (text.startsWith(' ')) return [] | 404 | if (text.startsWith(" ")) return []; |
374 | 405 | ||
375 | // If no valid options for current used renderer, go get it! | 406 | // If no valid options for current used renderer, go get it! |
376 | const validOptions = rendererOptions[renderer] | 407 | const validOptions = rendererOptions[renderer]; |
377 | if (!validOptions) { | 408 | if (!validOptions) { |
378 | // Get list of valid options for current renderer | 409 | // Get list of valid options for current renderer |
379 | const rendererUrl = defaultAliases.use[renderer]?.value | 410 | const rendererUrl = defaultAliases.use[renderer]?.value; |
380 | import(rendererUrl) | 411 | import(rendererUrl) |
381 | .then(rendererModule => { | 412 | .then(rendererModule => { |
382 | rendererOptions[renderer] = rendererModule.default.validOptions | 413 | rendererOptions[renderer] = rendererModule.default.validOptions; |
383 | const currentAnchor = cm.getCursor() | 414 | const currentAnchor = cm.getCursor(); |
384 | if (insideCodeblockForMap(currentAnchor)) { | 415 | if (insideCodeblockForMap(currentAnchor)) { |
385 | handleTypingInCodeBlock(currentAnchor) | 416 | handleTypingInCodeBlock(currentAnchor); |
386 | } | 417 | } |
387 | }) | 418 | }) |
388 | .catch(_ => { | 419 | .catch(_ => { |
389 | markInputIsInvalid(lineWithRenderer) | 420 | markInputIsInvalid(lineWithRenderer); |
390 | console.warn(`Fail to get valid options from Renderer typed: ${renderer}`) | 421 | console.warn( |
391 | }) | 422 | `Fail to get valid options from Renderer typed: ${renderer}`, |
392 | return [] | 423 | ); |
424 | }); | ||
425 | return []; | ||
393 | } | 426 | } |
394 | 427 | ||
395 | // 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 |
396 | const isKeyFinished = text.includes(':'); | 429 | const isKeyFinished = text.includes(":"); |
397 | const isValidKeyValue = text.match(/^[^:]+:\s+/) | 430 | const isValidKeyValue = text.match(/^[^:]+:\s+/); |
398 | if (isKeyFinished && !isValidKeyValue) { | 431 | if (isKeyFinished && !isValidKeyValue) { |
399 | markInputIsInvalid() | 432 | markInputIsInvalid(); |
400 | return [] | 433 | return []; |
401 | } | 434 | } |
402 | 435 | ||
403 | // If user is typing option | 436 | // If user is typing option |
404 | const keyTyped = text.split(':')[0].trim() | 437 | const keyTyped = text.split(":")[0].trim(); |
405 | if (!isKeyFinished) { | 438 | if (!isKeyFinished) { |
406 | markInputIsInvalid() | 439 | markInputIsInvalid(); |
407 | return getSuggestionsForOptions(keyTyped, validOptions) | 440 | return getSuggestionsForOptions(keyTyped, validOptions); |
408 | } | 441 | } |
409 | 442 | ||
410 | // If user is typing value | 443 | // If user is typing value |
411 | const matchedOption = validOptions.find(o => o.name === keyTyped) | 444 | const matchedOption = validOptions.find(o => o.name === keyTyped); |
412 | if (isKeyFinished && !matchedOption) { | 445 | if (isKeyFinished && !matchedOption) { |
413 | markInputIsInvalid() | 446 | markInputIsInvalid(); |
414 | } | 447 | } |
415 | 448 | ||
416 | if (isKeyFinished && matchedOption) { | 449 | if (isKeyFinished && matchedOption) { |
417 | const valueTyped = text.substring(text.indexOf(':') + 1).trim() | 450 | const valueTyped = text.substring(text.indexOf(":") + 1).trim(); |
418 | const isValidValue = matchedOption.isValid(valueTyped) | 451 | const isValidValue = matchedOption.isValid(valueTyped); |
419 | if (!valueTyped) { | 452 | if (!valueTyped) { |
420 | return [ | 453 | return [ |
421 | getSuggestionFromMapOption(matchedOption), | 454 | getSuggestionFromMapOption(matchedOption), |
422 | ...getSuggestionsFromAliases(matchedOption) | 455 | ...getSuggestionsFromAliases(matchedOption), |
423 | ].filter(s => s instanceof menuItem.Suggestion) | 456 | ].filter(s => s instanceof menuItem.Suggestion); |
424 | } | 457 | } |
425 | if (valueTyped && !isValidValue) { | 458 | if (valueTyped && !isValidValue) { |
426 | markInputIsInvalid() | 459 | markInputIsInvalid(); |
427 | return [] | 460 | return []; |
428 | } | 461 | } |
429 | } | 462 | } |
430 | |||
431 | } else { | 463 | } else { |
432 | // Suggestion for "use" | 464 | // Suggestion for "use" |
433 | const rendererSuggestions = Object.entries(defaultAliases.use) | 465 | const rendererSuggestions = Object.entries(defaultAliases.use) |
434 | .filter(([renderer,]) => { | 466 | .filter(([renderer]) => { |
435 | const suggestion = `use: ${renderer}` | 467 | const suggestion = `use: ${renderer}`; |
436 | const suggestionPattern = suggestion.replace(' ', '').toLowerCase() | 468 | const suggestionPattern = suggestion.replace(" ", "").toLowerCase(); |
437 | const textPattern = text.replace(' ', '').toLowerCase() | 469 | const textPattern = text.replace(" ", "").toLowerCase(); |
438 | return suggestion !== text && | 470 | return suggestion !== text && suggestionPattern.includes(textPattern); |
439 | (suggestionPattern.includes(textPattern)) | ||
440 | }) | 471 | }) |
441 | .map(([renderer, info]) => | 472 | .map( |
442 | new menuItem.Suggestion({ | 473 | ([renderer, info]) => |
443 | text: `<span>use: ${renderer}</span><span class='info' title="${info.desc}">ⓘ</span>`, | 474 | new menuItem.Suggestion({ |
444 | replace: `use: ${renderer}`, | 475 | text: `<span>use: ${renderer}</span><span class='info' title="${info.desc}">ⓘ</span>`, |
445 | }) | 476 | replace: `use: ${renderer}`, |
446 | ) | 477 | }), |
447 | return rendererSuggestions.length > 0 ? rendererSuggestions : [] | 478 | ); |
479 | return rendererSuggestions.length > 0 ? rendererSuggestions : []; | ||
448 | } | 480 | } |
449 | return [] | 481 | return []; |
450 | } | 482 | }; |
451 | // }}} | 483 | // }}} |
452 | // {{{ FUNCTION: Show element about suggestions | 484 | // {{{ FUNCTION: Show element about suggestions |
453 | const addSuggestions = (anchor, suggestions) => { | 485 | const addSuggestions = (anchor, suggestions) => { |
454 | |||
455 | if (suggestions.length === 0) { | 486 | if (suggestions.length === 0) { |
456 | menu.style.display = 'none'; | 487 | menu.style.display = "none"; |
457 | return | 488 | return; |
458 | } else { | 489 | } else { |
459 | menu.style.display = 'block'; | 490 | menu.style.display = "block"; |
460 | } | 491 | } |
461 | 492 | ||
462 | menu.innerHTML = '' | 493 | menu.innerHTML = ""; |
463 | suggestions | 494 | suggestions |
464 | .map(s => s.createElement(cm)) | 495 | .map(s => s.createElement(cm)) |
465 | .forEach(option => menu.appendChild(option)) | 496 | .forEach(option => menu.appendChild(option)); |
466 | 497 | ||
467 | const widgetAnchor = document.createElement('div') | 498 | const widgetAnchor = document.createElement("div"); |
468 | cm.addWidget(anchor, widgetAnchor, true) | 499 | cm.addWidget(anchor, widgetAnchor, true); |
469 | const rect = widgetAnchor.getBoundingClientRect() | 500 | const rect = widgetAnchor.getBoundingClientRect(); |
470 | menu.style.left = `calc(${rect.left}px + 2rem)`; | 501 | menu.style.left = `calc(${rect.left}px + 2rem)`; |
471 | menu.style.top = `calc(${rect.bottom}px + 1rem)`; | 502 | menu.style.top = `calc(${rect.bottom}px + 1rem)`; |
472 | menu.style.maxWidth = `calc(${window.innerWidth}px - ${rect.x}px - 3rem)`; | 503 | menu.style.maxWidth = `calc(${window.innerWidth}px - ${rect.x}px - 3rem)`; |
473 | menu.style.display = 'block' | 504 | menu.style.display = "block"; |
474 | } | 505 | }; |
475 | // }}} | 506 | // }}} |
476 | // EVENT: Suggests for current selection {{{ | 507 | // EVENT: Suggests for current selection {{{ |
477 | // FIXME Dont show suggestion when selecting multiple chars | 508 | // FIXME Dont show suggestion when selecting multiple chars |
478 | cm.on("cursorActivity", (_) => { | 509 | cm.on("cursorActivity", _ => { |
479 | menu.style.display = 'none' | 510 | menu.style.display = "none"; |
480 | const anchor = cm.getCursor() | 511 | const anchor = cm.getCursor(); |
481 | 512 | ||
482 | if (insideCodeblockForMap(anchor)) { | 513 | if (insideCodeblockForMap(anchor)) { |
483 | handleTypingInCodeBlock(anchor) | 514 | handleTypingInCodeBlock(anchor); |
484 | } | 515 | } |
485 | }); | 516 | }); |
486 | cm.on("blur", () => { | 517 | cm.on("blur", () => { |
487 | menu.style.display = 'none' | 518 | menu.style.display = "none"; |
488 | cm.getWrapperElement().classList.remove('focus') | 519 | cm.getWrapperElement().classList.remove("focus"); |
489 | HtmlContainer.classList.add('focus') | 520 | HtmlContainer.classList.add("focus"); |
490 | }) | 521 | }); |
491 | // }}} | 522 | // }}} |
492 | // EVENT: keydown for suggestions {{{ | 523 | // EVENT: keydown for suggestions {{{ |
493 | const keyForSuggestions = ['Tab', 'Enter', 'Escape'] | 524 | const keyForSuggestions = ["Tab", "Enter", "Escape"]; |
494 | cm.on('keydown', (_, e) => { | 525 | cm.on("keydown", (_, e) => { |
495 | if (!cm.hasFocus || !keyForSuggestions.includes(e.key) || menu.style.display === 'none') return; | 526 | if ( |
527 | !cm.hasFocus || | ||
528 | !keyForSuggestions.includes(e.key) || | ||
529 | menu.style.display === "none" | ||
530 | ) | ||
531 | return; | ||
496 | 532 | ||
497 | // Directly add a newline when no suggestion is selected | 533 | // Directly add a newline when no suggestion is selected |
498 | const currentSuggestion = menu.querySelector('.container__suggestion.focus') | 534 | const currentSuggestion = menu.querySelector(".container__suggestion.focus"); |
499 | if (!currentSuggestion && e.key === 'Enter') return | 535 | if (!currentSuggestion && e.key === "Enter") return; |
500 | 536 | ||
501 | // Override default behavior | 537 | // Override default behavior |
502 | e.preventDefault(); | 538 | e.preventDefault(); |
503 | 539 | ||
504 | // Suggestion when pressing Tab or Shift + Tab | 540 | // Suggestion when pressing Tab or Shift + Tab |
505 | const nextSuggestion = currentSuggestion?.nextSibling ?? menu.querySelector('.container__suggestion:first-child') | 541 | const nextSuggestion = |
506 | const previousSuggestion = currentSuggestion?.previousSibling ?? menu.querySelector('.container__suggestion:last-child') | 542 | currentSuggestion?.nextSibling ?? |
507 | const focusSuggestion = e.shiftKey ? previousSuggestion : nextSuggestion | 543 | menu.querySelector(".container__suggestion:first-child"); |
544 | const previousSuggestion = | ||
545 | currentSuggestion?.previousSibling ?? | ||
546 | menu.querySelector(".container__suggestion:last-child"); | ||
547 | const focusSuggestion = e.shiftKey ? previousSuggestion : nextSuggestion; | ||
508 | 548 | ||
509 | // Current editor selection state | 549 | // Current editor selection state |
510 | const anchor = cm.getCursor() | 550 | const anchor = cm.getCursor(); |
511 | switch (e.key) { | 551 | switch (e.key) { |
512 | case 'Tab': | 552 | case "Tab": |
513 | Array.from(menu.children).forEach(s => s.classList.remove('focus')) | 553 | Array.from(menu.children).forEach(s => s.classList.remove("focus")); |
514 | focusSuggestion.classList.add('focus') | 554 | focusSuggestion.classList.add("focus"); |
515 | focusSuggestion.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) | 555 | focusSuggestion.scrollIntoView({ behavior: "smooth", block: "nearest" }); |
516 | break; | 556 | break; |
517 | case 'Enter': | 557 | case "Enter": |
518 | currentSuggestion.onclick() | 558 | currentSuggestion.onclick(); |
519 | break; | 559 | break; |
520 | case 'Escape': | 560 | case "Escape": |
521 | menu.style.display = 'none'; | 561 | menu.style.display = "none"; |
522 | // Focus editor again | 562 | // Focus editor again |
523 | setTimeout(() => cm.focus() && cm.setCursor(anchor), 100) | 563 | setTimeout(() => cm.focus() && cm.setCursor(anchor), 100); |
524 | break; | 564 | break; |
525 | } | 565 | } |
526 | }); | 566 | }); |
527 | 567 | ||
528 | document.onkeydown = (e) => { | 568 | document.onkeydown = e => { |
529 | if (e.altKey && e.ctrlKey && e.key === 'm') { | 569 | if (e.altKey && e.ctrlKey && e.key === "m") { |
530 | toggleEditing() | 570 | toggleEditing(); |
531 | e.preventDefault() | 571 | e.preventDefault(); |
532 | return null | 572 | return null; |
533 | } | 573 | } |
534 | 574 | ||
535 | if (!cm.hasFocus()) { | 575 | if (!cm.hasFocus()) { |
536 | if (e.key === 'F1') { | 576 | if (e.key === "F1") { |
537 | e.preventDefault() | 577 | e.preventDefault(); |
538 | cm.focus() | 578 | cm.focus(); |
539 | } | 579 | } |
540 | if (e.key === 'Tab') { | 580 | if (e.key === "Tab") { |
541 | e.preventDefault() | 581 | e.preventDefault(); |
542 | dumbymap.utils.focusNextMap(e.shiftKey) | 582 | dumbymap.utils.focusNextMap(e.shiftKey); |
543 | } | 583 | } |
544 | if (e.key === 'x' || e.key === 'X') { | 584 | if (e.key === "x" || e.key === "X") { |
545 | e.preventDefault() | 585 | e.preventDefault(); |
546 | dumbymap.utils.switchToNextLayout(e.shiftKey) | 586 | dumbymap.utils.switchToNextLayout(e.shiftKey); |
547 | } | 587 | } |
548 | if (e.key === 'n') { | 588 | if (e.key === "n") { |
549 | e.preventDefault() | 589 | e.preventDefault(); |
550 | dumbymap.utils.focusNextBlock() | 590 | dumbymap.utils.focusNextBlock(); |
551 | } | 591 | } |
552 | if (e.key === 'p') { | 592 | if (e.key === "p") { |
553 | e.preventDefault() | 593 | e.preventDefault(); |
554 | dumbymap.utils.focusNextBlock(true) | 594 | dumbymap.utils.focusNextBlock(true); |
555 | } | 595 | } |
556 | if (e.key === 'Escape') { | 596 | if (e.key === "Escape") { |
557 | e.preventDefault() | 597 | e.preventDefault(); |
558 | dumbymap.utils.removeBlockFocus() | 598 | dumbymap.utils.removeBlockFocus(); |
559 | } | 599 | } |
560 | } | 600 | } |
561 | } | 601 | }; |
562 | 602 | ||
563 | // }}} | 603 | // }}} |
564 | // }}} | 604 | // }}} |
565 | // Layout Switch {{{ | 605 | // Layout Switch {{{ |
566 | const layoutObserver = new MutationObserver(() => { | 606 | const layoutObserver = new MutationObserver(() => { |
567 | const layout = HtmlContainer.getAttribute('data-layout') | 607 | const layout = HtmlContainer.getAttribute("data-layout"); |
568 | if (layout !== 'normal') { | 608 | if (layout !== "normal") { |
569 | document.body.removeAttribute('data-mode') | 609 | document.body.removeAttribute("data-mode"); |
570 | } | 610 | } |
571 | }) | 611 | }); |
572 | 612 | ||
573 | layoutObserver.observe(HtmlContainer, { | 613 | layoutObserver.observe(HtmlContainer, { |
574 | attributes: true, | 614 | attributes: true, |
575 | attributeFilter: ["data-layout"], | 615 | attributeFilter: ["data-layout"], |
576 | attributeOldValue: true | 616 | attributeOldValue: true, |
577 | }); | 617 | }); |
578 | // }}} | 618 | // }}} |
579 | // ContextMenu {{{ | 619 | // ContextMenu {{{ |
580 | document.oncontextmenu = (e) => { | 620 | document.oncontextmenu = e => { |
581 | if (cm.hasFocus()) return | 621 | if (cm.hasFocus()) return; |
582 | 622 | ||
583 | const selection = document.getSelection() | 623 | const selection = document.getSelection(); |
584 | const range = selection.getRangeAt(0) | 624 | const range = selection.getRangeAt(0); |
585 | if (selection) { | 625 | if (selection) { |
586 | e.preventDefault() | 626 | e.preventDefault(); |
587 | menu.innerHTML = '' | 627 | menu.innerHTML = ""; |
588 | 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;`; |
589 | const addGeoLink = new menuItem.GeoLink({ range }) | 629 | const addGeoLink = new menuItem.GeoLink({ range }); |
590 | menu.appendChild(addGeoLink.createElement()) | 630 | menu.appendChild(addGeoLink.createElement()); |
591 | } | 631 | } |
592 | menu.appendChild(menuItem.nextMap.bind(dumbymap)()) | 632 | menu.appendChild(menuItem.nextMap.bind(dumbymap)()); |
593 | menu.appendChild(menuItem.nextBlock.bind(dumbymap)()) | 633 | menu.appendChild(menuItem.nextBlock.bind(dumbymap)()); |
594 | menu.appendChild(menuItem.nextLayout.bind(dumbymap)()) | 634 | menu.appendChild(menuItem.nextLayout.bind(dumbymap)()); |
595 | } | 635 | }; |
596 | 636 | ||
597 | const actionOutsideMenu = (e) => { | 637 | const actionOutsideMenu = e => { |
598 | if (menu.style.display === 'none' || cm.hasFocus()) return | 638 | if (menu.style.display === "none" || cm.hasFocus()) return; |
599 | const rect = menu.getBoundingClientRect() | 639 | const rect = menu.getBoundingClientRect(); |
600 | if (e.clientX < rect.left | 640 | if ( |
601 | || e.clientX > rect.left + rect.width | 641 | e.clientX < rect.left || |
602 | || e.clientY < rect.top | 642 | e.clientX > rect.left + rect.width || |
603 | || e.clientY > rect.top + rect.height | 643 | e.clientY < rect.top || |
644 | e.clientY > rect.top + rect.height | ||
604 | ) { | 645 | ) { |
605 | menu.style.display = 'none' | 646 | menu.style.display = "none"; |
606 | } | 647 | } |
607 | } | 648 | }; |
608 | 649 | ||
609 | document.addEventListener('click', actionOutsideMenu) | 650 | document.addEventListener("click", actionOutsideMenu); |
610 | 651 | ||
611 | // }}} | 652 | // }}} |
612 | 653 | ||
diff --git a/src/utils.mjs b/src/utils.mjs index ead3002..ad9131e 100644 --- a/src/utils.mjs +++ b/src/utils.mjs | |||
@@ -18,8 +18,8 @@ export const onRemove = (element, callback) => { | |||
18 | } | 18 | } |
19 | } | 19 | } |
20 | }); | 20 | }); |
21 | obs.observe(parent, { childList: true, }); | 21 | obs.observe(parent, { childList: true }); |
22 | } | 22 | }; |
23 | 23 | ||
24 | /** | 24 | /** |
25 | * Animate transition of DOMRect, with Web Animation API | 25 | * Animate transition of DOMRect, with Web Animation API |
@@ -32,18 +32,24 @@ export const onRemove = (element, callback) => { | |||
32 | * @returns {Animation} https://developer.mozilla.org/en-US/docs/Web/API/Animation | 32 | * @returns {Animation} https://developer.mozilla.org/en-US/docs/Web/API/Animation |
33 | */ | 33 | */ |
34 | export const animateRectTransition = (element, rect, options = {}) => { | 34 | export const animateRectTransition = (element, rect, options = {}) => { |
35 | if (!element.parentElement) throw new Error("The node must already be attached"); | 35 | if (!element.parentElement) |
36 | throw new Error("The node must already be attached"); | ||
36 | 37 | ||
37 | const { width: w1, height: h1, left: x1, top: y1 } = rect | 38 | const { width: w1, height: h1, left: x1, top: y1 } = rect; |
38 | const { width: w2, height: h2, left: x2, top: y2 } = element.getBoundingClientRect() | 39 | const { |
40 | width: w2, | ||
41 | height: h2, | ||
42 | left: x2, | ||
43 | top: y2, | ||
44 | } = element.getBoundingClientRect(); | ||
39 | 45 | ||
40 | const rw = (w1 ?? w2) / w2 | 46 | const rw = (w1 ?? w2) / w2; |
41 | const rh = (h1 ?? h2) / h2 | 47 | const rh = (h1 ?? h2) / h2; |
42 | const dx = x1 - x2; | 48 | const dx = x1 - x2; |
43 | const dy = y1 - y2; | 49 | const dy = y1 - y2; |
44 | 50 | ||
45 | if (dx === 0 && dy === 0 || !isFinite(rw) || !isFinite(rh)) { | 51 | if ((dx === 0 && dy === 0) || !isFinite(rw) || !isFinite(rh)) { |
46 | return element.animate([], { duration: 0 }) | 52 | return element.animate([], { duration: 0 }); |
47 | } | 53 | } |
48 | 54 | ||
49 | const transform1 = `translate(0, 0) scale(1, 1)`; | 55 | const transform1 = `translate(0, 0) scale(1, 1)`; |
@@ -51,18 +57,14 @@ export const animateRectTransition = (element, rect, options = {}) => { | |||
51 | const keyframes = [ | 57 | const keyframes = [ |
52 | { transform: transform1, opacity: 1 }, | 58 | { transform: transform1, opacity: 1 }, |
53 | { transform: transform2, opacity: 0.3 }, | 59 | { transform: transform2, opacity: 0.3 }, |
54 | ] | 60 | ]; |
55 | if (options.resume === true) keyframes.reverse() | 61 | if (options.resume === true) keyframes.reverse(); |
56 | |||
57 | return element.animate( | ||
58 | keyframes, | ||
59 | { | ||
60 | duration: options.duration ?? 500, | ||
61 | easing: 'ease-in-out', | ||
62 | } | ||
63 | ); | ||
64 | } | ||
65 | 62 | ||
63 | return element.animate(keyframes, { | ||
64 | duration: options.duration ?? 500, | ||
65 | easing: "ease-in-out", | ||
66 | }); | ||
67 | }; | ||
66 | 68 | ||
67 | /** | 69 | /** |
68 | * Throttle for function call | 70 | * Throttle for function call |
@@ -74,13 +76,13 @@ export const animateRectTransition = (element, rect, options = {}) => { | |||
74 | export function throttle(func, delay) { | 76 | export function throttle(func, delay) { |
75 | let timerFlag = null; | 77 | let timerFlag = null; |
76 | 78 | ||
77 | return function(...args) { | 79 | return function (...args) { |
78 | const context = this | 80 | const context = this; |
79 | if (timerFlag !== null) return null | 81 | if (timerFlag !== null) return null; |
80 | 82 | ||
81 | timerFlag = setTimeout( | 83 | timerFlag = setTimeout( |
82 | () => timerFlag = null, | 84 | () => (timerFlag = null), |
83 | typeof delay === 'function' ? delay.call(context) : delay | 85 | typeof delay === "function" ? delay.call(context) : delay, |
84 | ); | 86 | ); |
85 | 87 | ||
86 | return func.call(context, ...args); | 88 | return func.call(context, ...args); |