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