diff options
-rw-r--r-- | addon/index.mjs | 5 | ||||
-rw-r--r-- | src/Layout.mjs | 179 | ||||
-rw-r--r-- | src/css/dumbymap.css | 56 | ||||
-rw-r--r-- | src/dumbymap.mjs | 15 | ||||
-rw-r--r-- | src/editor.mjs | 2 |
5 files changed, 155 insertions, 102 deletions
diff --git a/addon/index.mjs b/addon/index.mjs index b5d71ba..77716b6 100644 --- a/addon/index.mjs +++ b/addon/index.mjs | |||
@@ -1,4 +1,5 @@ | |||
1 | const url = new URL(window.location) | 1 | const url = new URL(window.location) |
2 | const use = url.searchParams.get('use') | ||
2 | if (url.host === 'www.ptt.cc') { | 3 | if (url.host === 'www.ptt.cc') { |
3 | const content = document.querySelector('#main-content') | 4 | const content = document.querySelector('#main-content') |
4 | Array.from(content.childNodes) | 5 | Array.from(content.childNodes) |
@@ -22,7 +23,7 @@ const addBlocks = blockSelector | |||
22 | : undefined | 23 | : undefined |
23 | 24 | ||
24 | const simpleRender = window.mapclay.renderWith(config => ({ | 25 | const simpleRender = window.mapclay.renderWith(config => ({ |
25 | use: 'Leaflet', | 26 | use: use ?? 'Leaflet', |
26 | width: '100%', | 27 | width: '100%', |
27 | height: '200px', | 28 | height: '200px', |
28 | XYZ: 'https://tile.openstreetmap.jp/styles/osm-bright/512/{z}/{x}/{y}.png', | 29 | XYZ: 'https://tile.openstreetmap.jp/styles/osm-bright/512/{z}/{x}/{y}.png', |
@@ -36,7 +37,7 @@ const simpleRender = window.mapclay.renderWith(config => ({ | |||
36 | window.generateMaps(document.querySelector('main') ?? document.body, { | 37 | window.generateMaps(document.querySelector('main') ?? document.body, { |
37 | crs: url.searchParams.get('crs') ?? 'EPSG:4326', | 38 | crs: url.searchParams.get('crs') ?? 'EPSG:4326', |
38 | addBlocks, | 39 | addBlocks, |
39 | initialLayout: '', | 40 | initialLayout: 'sticky', |
40 | render: simpleRender, | 41 | render: simpleRender, |
41 | autoMap: true, | 42 | autoMap: true, |
42 | }) | 43 | }) |
diff --git a/src/Layout.mjs b/src/Layout.mjs index 485b513..f79c70e 100644 --- a/src/Layout.mjs +++ b/src/Layout.mjs | |||
@@ -86,6 +86,83 @@ export class SideBySide extends Layout { | |||
86 | } | 86 | } |
87 | 87 | ||
88 | /** | 88 | /** |
89 | * addDraggable. | ||
90 | * | ||
91 | * @param {HTMLElement} element | ||
92 | */ | ||
93 | const addDraggable = (element, { snap, left, top } = {}) => { | ||
94 | element.classList.add('draggable-block') | ||
95 | |||
96 | // Make sure current element always on top | ||
97 | const siblings = Array.from( | ||
98 | element.parentElement?.querySelectorAll(':scope > *') ?? [], | ||
99 | ) | ||
100 | let popTimer = null | ||
101 | const onmouseover = () => { | ||
102 | popTimer = setTimeout(() => { | ||
103 | siblings.forEach(e => e.style.removeProperty('z-index')) | ||
104 | element.style.zIndex = '9001' | ||
105 | }, 200) | ||
106 | } | ||
107 | const onmouseout = () => { | ||
108 | clearTimeout(popTimer) | ||
109 | } | ||
110 | element.addEventListener('mouseover', onmouseover) | ||
111 | element.addEventListener('mouseout', onmouseout) | ||
112 | |||
113 | // Add draggable part | ||
114 | const draggablePart = document.createElement('div') | ||
115 | element.appendChild(draggablePart) | ||
116 | draggablePart.className = 'draggable-part' | ||
117 | draggablePart.innerHTML = '<div class="handle">\u2630</div>' | ||
118 | |||
119 | // Add draggable instance | ||
120 | const draggable = new PlainDraggable(element, { | ||
121 | left, | ||
122 | top, | ||
123 | handle: draggablePart, | ||
124 | snap, | ||
125 | }) | ||
126 | |||
127 | // FIXME use pure CSS to hide utils | ||
128 | const utils = element.querySelector('.utils') | ||
129 | draggable.onDragStart = () => { | ||
130 | if (utils) utils.style.display = 'none' | ||
131 | element.classList.add('drag') | ||
132 | } | ||
133 | |||
134 | draggable.onDragEnd = () => { | ||
135 | if (utils) utils.style = '' | ||
136 | element.classList.remove('drag') | ||
137 | element.style.zIndex = '9000' | ||
138 | } | ||
139 | |||
140 | // Reposition draggable instance when resized | ||
141 | const resizeObserver = new window.ResizeObserver(() => { | ||
142 | draggable?.position() | ||
143 | }) | ||
144 | resizeObserver.observe(element) | ||
145 | |||
146 | // Callback for remove | ||
147 | onRemove(element, () => { | ||
148 | resizeObserver.disconnect() | ||
149 | }) | ||
150 | |||
151 | new window.MutationObserver(() => { | ||
152 | if (!element.classList.contains('draggable-block') && draggable) { | ||
153 | element.removeEventListener('mouseover', onmouseover) | ||
154 | element.removeEventListener('mouseout', onmouseout) | ||
155 | resizeObserver.disconnect() | ||
156 | } | ||
157 | }).observe(element, { | ||
158 | attributes: true, | ||
159 | attributeFilter: ['class'], | ||
160 | }) | ||
161 | |||
162 | return draggable | ||
163 | } | ||
164 | |||
165 | /** | ||
89 | * Overlay Layout, Showcase occupies viewport, and HTML content becomes draggable blocks | 166 | * Overlay Layout, Showcase occupies viewport, and HTML content becomes draggable blocks |
90 | * | 167 | * |
91 | * @extends {Layout} | 168 | * @extends {Layout} |
@@ -103,70 +180,6 @@ export class Overlay extends Layout { | |||
103 | } | 180 | } |
104 | 181 | ||
105 | /** | 182 | /** |
106 | * addDraggable. | ||
107 | * | ||
108 | * @param {HTMLElement} element | ||
109 | */ | ||
110 | addDraggable = element => { | ||
111 | // Make sure current element always on top | ||
112 | const siblings = Array.from( | ||
113 | element.parentElement?.querySelectorAll(':scope > *') ?? [], | ||
114 | ) | ||
115 | let popTimer = null | ||
116 | element.onmouseover = () => { | ||
117 | popTimer = setTimeout(() => { | ||
118 | siblings.forEach(e => e.style.removeProperty('z-index')) | ||
119 | element.style.zIndex = '9001' | ||
120 | }, 200) | ||
121 | } | ||
122 | element.onmouseout = () => { | ||
123 | clearTimeout(popTimer) | ||
124 | } | ||
125 | |||
126 | // Add draggable part | ||
127 | const draggablePart = document.createElement('div') | ||
128 | element.appendChild(draggablePart) | ||
129 | draggablePart.className = 'draggable-part' | ||
130 | draggablePart.innerHTML = '<div class="handle">\u2630</div>' | ||
131 | |||
132 | // Add draggable instance | ||
133 | const { left, top } = element.getBoundingClientRect() | ||
134 | const draggable = new PlainDraggable(element, { | ||
135 | top, | ||
136 | left, | ||
137 | handle: draggablePart, | ||
138 | snap: { x: { step: 20 }, y: { step: 20 } }, | ||
139 | }) | ||
140 | |||
141 | // FIXME use pure CSS to hide utils | ||
142 | const utils = element.querySelector('.utils') | ||
143 | draggable.onDragStart = () => { | ||
144 | utils.style.display = 'none' | ||
145 | element.classList.add('drag') | ||
146 | } | ||
147 | |||
148 | draggable.onDragEnd = () => { | ||
149 | utils.style = '' | ||
150 | element.classList.remove('drag') | ||
151 | element.style.zIndex = '9000' | ||
152 | } | ||
153 | |||
154 | // Reposition draggable instance when resized | ||
155 | new window.ResizeObserver(() => { | ||
156 | try { | ||
157 | draggable.position() | ||
158 | } catch (err) { | ||
159 | console.warn(err) | ||
160 | } | ||
161 | }).observe(element) | ||
162 | |||
163 | // Callback for remove | ||
164 | onRemove(element, () => { | ||
165 | draggable.remove() | ||
166 | }) | ||
167 | } | ||
168 | |||
169 | /** | ||
170 | * enterHandler. | 183 | * enterHandler. |
171 | * | 184 | * |
172 | * @param {HTMLElement} options.hemlHolder - Parent element for block | 185 | * @param {HTMLElement} options.hemlHolder - Parent element for block |
@@ -182,7 +195,7 @@ export class Overlay extends Layout { | |||
182 | } | 195 | } |
183 | 196 | ||
184 | // Create draggable blocks and set each position by previous one | 197 | // Create draggable blocks and set each position by previous one |
185 | let [left, top] = [20, 20] | 198 | const [left, top] = [20, 20] |
186 | blocks.forEach(block => { | 199 | blocks.forEach(block => { |
187 | const originLeft = Number(block.dataset.left) | 200 | const originLeft = Number(block.dataset.left) |
188 | const originTop = Number(block.dataset.top) | 201 | const originTop = Number(block.dataset.top) |
@@ -210,8 +223,8 @@ export class Overlay extends Layout { | |||
210 | wrapper.style.left = left + 'px' | 223 | wrapper.style.left = left + 'px' |
211 | wrapper.style.top = top + 'px' | 224 | wrapper.style.top = top + 'px' |
212 | htmlHolder.appendChild(wrapper) | 225 | htmlHolder.appendChild(wrapper) |
213 | const { width } = wrapper.getBoundingClientRect() | 226 | const rect = wrapper.getBoundingClientRect() |
214 | left += width + 30 | 227 | left += rect.width + 30 |
215 | if (left > window.innerWidth) { | 228 | if (left > window.innerWidth) { |
216 | top += 200 | 229 | top += 200 |
217 | left = left % window.innerWidth | 230 | left = left % window.innerWidth |
@@ -222,7 +235,14 @@ export class Overlay extends Layout { | |||
222 | wrapper, | 235 | wrapper, |
223 | { left: originLeft, top: originTop }, | 236 | { left: originLeft, top: originTop }, |
224 | { resume: true, duration: 300 }, | 237 | { resume: true, duration: 300 }, |
225 | ).finished.finally(() => this.addDraggable(wrapper)) | 238 | ).finished.finally(() => addDraggable(wrapper, { |
239 | left: rect.left, | ||
240 | top: rect.top, | ||
241 | snap: { | ||
242 | x: { step: 20 }, | ||
243 | y: { step: 20 }, | ||
244 | }, | ||
245 | })) | ||
226 | 246 | ||
227 | // Trivial case: | 247 | // Trivial case: |
228 | // This hack make sure utils remains at the same place even when wrapper resized | 248 | // This hack make sure utils remains at the same place even when wrapper resized |
@@ -271,3 +291,26 @@ export class Overlay extends Layout { | |||
271 | blocks.forEach(resumeFromDraggable) | 291 | blocks.forEach(resumeFromDraggable) |
272 | } | 292 | } |
273 | } | 293 | } |
294 | |||
295 | /** | ||
296 | * Sticky Layout, Showcase is draggable and stick to viewport | ||
297 | * | ||
298 | * @extends {Layout} | ||
299 | */ | ||
300 | export class Sticky extends Layout { | ||
301 | draggable = document.createElement('div') | ||
302 | |||
303 | enterHandler = ({ showcase }) => { | ||
304 | showcase.replaceWith(this.draggable) | ||
305 | this.draggable.appendChild(showcase) | ||
306 | this.draggableInstance = addDraggable(this.draggable) | ||
307 | const rect = this.draggable.getBoundingClientRect() | ||
308 | this.draggable.style.cssText = `left: ${window.innerWidth - rect.width - 20}px; top: ${window.innerHeight - rect.height - 20}px;` | ||
309 | } | ||
310 | |||
311 | leaveHandler = ({ showcase }) => { | ||
312 | this.draggableInstance?.remove() | ||
313 | this.draggable.replaceWith(showcase) | ||
314 | this.draggable.querySelectorAll(':scope > :not(.mapclay)').forEach(e => e.remove()) | ||
315 | } | ||
316 | } | ||
diff --git a/src/css/dumbymap.css b/src/css/dumbymap.css index c0bb1a9..cc161fa 100644 --- a/src/css/dumbymap.css +++ b/src/css/dumbymap.css | |||
@@ -141,7 +141,7 @@ root { | |||
141 | background-repeat: no-repeat; | 141 | background-repeat: no-repeat; |
142 | background-position: right 2px top 2px; | 142 | background-position: right 2px top 2px; |
143 | 143 | ||
144 | color: #555; | 144 | color: #555 !important; |
145 | background-size: 12px 12px; | 145 | background-size: 12px 12px; |
146 | 146 | ||
147 | &.geolink { | 147 | &.geolink { |
@@ -166,7 +166,7 @@ root { | |||
166 | } | 166 | } |
167 | } | 167 | } |
168 | 168 | ||
169 | .Dumby:not([data-layout=""]) .dumby-block { | 169 | .DumbyMap.Dumby[data-layout] .dumby-block { |
170 | padding: 1rem 1rem 1rem 2rem; | 170 | padding: 1rem 1rem 1rem 2rem; |
171 | 171 | ||
172 | position: relative; | 172 | position: relative; |
@@ -495,7 +495,7 @@ root { | |||
495 | } | 495 | } |
496 | } | 496 | } |
497 | 497 | ||
498 | .Dumby[data-layout='normal'] { | 498 | .DumbyMap.Dumby[data-layout='normal'] { |
499 | max-width: 60em; | 499 | max-width: 60em; |
500 | 500 | ||
501 | &::after { | 501 | &::after { |
@@ -595,6 +595,35 @@ root { | |||
595 | } | 595 | } |
596 | } | 596 | } |
597 | 597 | ||
598 | .Dumby[data-layout='sticky'] { | ||
599 | .SemanticHtml { | ||
600 | max-width: 60em; | ||
601 | margin: 0 auto; | ||
602 | } | ||
603 | |||
604 | .draggable-block { | ||
605 | position: fixed; | ||
606 | |||
607 | &::before { | ||
608 | display: none; | ||
609 | } | ||
610 | } | ||
611 | |||
612 | .Showcase { | ||
613 | display: block; | ||
614 | overflow: hidden; | ||
615 | width: 20vw; | ||
616 | min-width: 10vw; | ||
617 | height: 200px; | ||
618 | min-height: 100px; | ||
619 | resize: both; | ||
620 | |||
621 | .mapclay { | ||
622 | border-radius: 4px !important; | ||
623 | } | ||
624 | } | ||
625 | } | ||
626 | |||
598 | .utils { | 627 | .utils { |
599 | display: flex; | 628 | display: flex; |
600 | padding-block: 1rem; | 629 | padding-block: 1rem; |
@@ -642,6 +671,7 @@ root { | |||
642 | .draggable-block { | 671 | .draggable-block { |
643 | overflow: visible; | 672 | overflow: visible; |
644 | width: fit-content; | 673 | width: fit-content; |
674 | height: fit-content; | ||
645 | 675 | ||
646 | position: absolute; | 676 | position: absolute; |
647 | 677 | ||
@@ -659,7 +689,7 @@ root { | |||
659 | opacity: 0; | 689 | opacity: 0; |
660 | pointer-events: auto; | 690 | pointer-events: auto; |
661 | 691 | ||
662 | &:has(.dumby-block.focus) { | 692 | &:has(.dumby-block.focus, .mapclay.focus) { |
663 | visibility: visible; | 693 | visibility: visible; |
664 | opacity: 1; | 694 | opacity: 1; |
665 | } | 695 | } |
@@ -730,7 +760,7 @@ root { | |||
730 | position: absolute; | 760 | position: absolute; |
731 | left: 0; | 761 | left: 0; |
732 | top: 0; | 762 | top: 0; |
733 | z-index: 1; | 763 | z-index: 2; |
734 | border-top-left-radius: 0.3rem; | 764 | border-top-left-radius: 0.3rem; |
735 | border-top-right-radius: 0.3rem; | 765 | border-top-right-radius: 0.3rem; |
736 | } | 766 | } |
@@ -782,22 +812,6 @@ root { | |||
782 | font-weight: bold; | 812 | font-weight: bold; |
783 | } | 813 | } |
784 | 814 | ||
785 | .Dumby[data-layout='sticky'] { | ||
786 | max-width: 80em; | ||
787 | |||
788 | .Showcase { | ||
789 | display: block; | ||
790 | width: 20vw; | ||
791 | height: 40vh; | ||
792 | |||
793 | position: absolute; | ||
794 | right: 20px; | ||
795 | bottom: 20px; | ||
796 | |||
797 | background: red; | ||
798 | } | ||
799 | } | ||
800 | |||
801 | .dumby-block details { | 815 | .dumby-block details { |
802 | display: block; | 816 | display: block; |
803 | width: fit-content; | 817 | width: fit-content; |
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs index b612367..13f4574 100644 --- a/src/dumbymap.mjs +++ b/src/dumbymap.mjs | |||
@@ -5,7 +5,7 @@ import MarkdownItFrontMatter from 'markdown-it-front-matter' | |||
5 | import MarkdownItInjectLinenumbers from 'markdown-it-inject-linenumbers' | 5 | import MarkdownItInjectLinenumbers from 'markdown-it-inject-linenumbers' |
6 | import * as mapclay from 'mapclay' | 6 | import * as mapclay from 'mapclay' |
7 | import { replaceTextNodes, onRemove, animateRectTransition, throttle, shiftByWindow } from './utils' | 7 | import { replaceTextNodes, onRemove, animateRectTransition, throttle, shiftByWindow } from './utils' |
8 | import { Layout, SideBySide, Overlay } from './Layout' | 8 | import { Layout, SideBySide, Overlay, Sticky } from './Layout' |
9 | import * as utils from './dumbyUtils' | 9 | import * as utils from './dumbyUtils' |
10 | import * as menuItem from './MenuItem' | 10 | import * as menuItem from './MenuItem' |
11 | import PlainModal from 'plain-modal' | 11 | import PlainModal from 'plain-modal' |
@@ -23,6 +23,7 @@ const defaultLayouts = [ | |||
23 | new Layout({ name: 'normal' }), | 23 | new Layout({ name: 'normal' }), |
24 | new SideBySide({ name: 'side-by-side' }), | 24 | new SideBySide({ name: 'side-by-side' }), |
25 | new Overlay({ name: 'overlay' }), | 25 | new Overlay({ name: 'overlay' }), |
26 | new Sticky({ name: 'sticky' }), | ||
26 | ] | 27 | ] |
27 | 28 | ||
28 | /** Cache across every dumbymap generation */ | 29 | /** Cache across every dumbymap generation */ |
@@ -169,13 +170,12 @@ export const generateMaps = (container, { | |||
169 | /** Prepare Contaner */ | 170 | /** Prepare Contaner */ |
170 | container.classList.add('Dumby') | 171 | container.classList.add('Dumby') |
171 | delete container.dataset.layout | 172 | delete container.dataset.layout |
172 | container.dataset.layout = initialLayout ?? defaultLayouts[0].name | ||
173 | 173 | ||
174 | /** Prepare Semantic HTML part and blocks of contents inside */ | 174 | /** Prepare Semantic HTML part and blocks of contents inside */ |
175 | const htmlHolder = container.querySelector('.SemanticHtml') ?? | 175 | const htmlHolder = container.querySelector('.SemanticHtml') ?? |
176 | Array.from(container.children).find(e => e.id?.includes('main') || e.className.includes('main')) ?? | 176 | Array.from(container.children).find(e => e.id?.includes('main') || e.className.includes('main')) ?? |
177 | Array.from(container.children).sort((a, b) => a.textContent.length < b.textContent.length).at(0) | 177 | Array.from(container.children).sort((a, b) => a.textContent.length < b.textContent.length).at(0) |
178 | htmlHolder.classList.add('.SemanticHtml') | 178 | htmlHolder.classList.add('SemanticHtml') |
179 | 179 | ||
180 | const blocks = addBlocks(htmlHolder) | 180 | const blocks = addBlocks(htmlHolder) |
181 | blocks.forEach(b => { | 181 | blocks.forEach(b => { |
@@ -280,13 +280,7 @@ export const generateMaps = (container, { | |||
280 | const mutation = mutations.at(-1) | 280 | const mutation = mutations.at(-1) |
281 | const target = mutation.target | 281 | const target = mutation.target |
282 | const focus = target.classList.contains('focus') | 282 | const focus = target.classList.contains('focus') |
283 | const shouldBeInShowcase = | 283 | const shouldBeInShowcase = focus && showcase.checkVisibility() |
284 | focus && | ||
285 | showcase.checkVisibility({ | ||
286 | contentVisibilityAuto: true, | ||
287 | opacityProperty: true, | ||
288 | visibilityProperty: true, | ||
289 | }) | ||
290 | 284 | ||
291 | if (focus) { | 285 | if (focus) { |
292 | dumbymap.utils | 286 | dumbymap.utils |
@@ -393,6 +387,7 @@ export const generateMaps = (container, { | |||
393 | }) | 387 | }) |
394 | 388 | ||
395 | onRemove(htmlHolder, () => layoutObserver.disconnect()) | 389 | onRemove(htmlHolder, () => layoutObserver.disconnect()) |
390 | container.dataset.layout = initialLayout ?? defaultLayouts[0].name | ||
396 | 391 | ||
397 | /** | 392 | /** |
398 | * afterMapRendered. callback of each map rendered | 393 | * afterMapRendered. callback of each map rendered |
diff --git a/src/editor.mjs b/src/editor.mjs index d56ad4b..2854373 100644 --- a/src/editor.mjs +++ b/src/editor.mjs | |||
@@ -24,7 +24,7 @@ const initialLayout = pageParams.get('layout') | |||
24 | /** Variables: dumbymap and editor **/ | 24 | /** Variables: dumbymap and editor **/ |
25 | const context = document.querySelector('[data-mode]') | 25 | const context = document.querySelector('[data-mode]') |
26 | const textArea = document.querySelector('.editor textarea') | 26 | const textArea = document.querySelector('.editor textarea') |
27 | const dumbyContainer = document.querySelector('.DumbyMap') | 27 | const dumbyContainer = document.querySelector('.Dumby') |
28 | dumbyContainer.dataset.scrollLine = '' | 28 | dumbyContainer.dataset.scrollLine = '' |
29 | /** Watch: DumbyMap */ | 29 | /** Watch: DumbyMap */ |
30 | new window.MutationObserver(mutations => { | 30 | new window.MutationObserver(mutations => { |