aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorHsieh Chin Fan <pham@topo.tw>2024-09-18 23:41:15 +0800
committerHsieh Chin Fan <pham@topo.tw>2024-09-18 23:52:49 +0800
commit4cf84d2fe96c004fef3b508ce344885db74917e2 (patch)
tree87fad915787b4621d0eb2f3e9086f54afdffa5a4 /src
parentfdf3e2780edf92bd0cdb7aefc7a196e608b9cbd1 (diff)
feat: swith focus map by tab key and click
* Use data-focus attribute and MutationObserver to move map-container between Sementic HTML and Showcase. * Currently set return value of docuement.onkeydown in editor, then reset onkeydown function in dumbymap. Maybe there is a better way? * CSS to make placeholder not so obvious
Diffstat (limited to 'src')
-rw-r--r--src/css/dumbymap.css27
-rw-r--r--src/dumbymap.mjs282
-rw-r--r--src/editor.mjs32
3 files changed, 208 insertions, 133 deletions
diff --git a/src/css/dumbymap.css b/src/css/dumbymap.css
index 6554ceb..03c6c2a 100644
--- a/src/css/dumbymap.css
+++ b/src/css/dumbymap.css
@@ -40,8 +40,11 @@
40 40
41 .Showcase { 41 .Showcase {
42 order: 2; 42 order: 2;
43 #mapPlaceholder { 43 display: none;
44 display: none; 44
45 .map-container {
46 width: 100% !important;
47 height: 100% !important;
45 } 48 }
46 } 49 }
47 50
@@ -74,9 +77,11 @@
74 } 77 }
75 78
76 .map-container { 79 .map-container {
80 background: white;
77 margin: 2px; 81 margin: 2px;
78 &.focus { 82 border: 3px solid transparent;
79 border: solid gray; 83 &[data-focus=true] {
84 border: 3px solid gray;
80 } 85 }
81 } 86 }
82 87
@@ -90,6 +95,11 @@
90 width: fit-content; 95 width: fit-content;
91 flex: 0 0; 96 flex: 0 0;
92 } 97 }
98 [data-placeholder] {
99 box-sizing: border-box;
100 border: 5px solid white;
101 background: lightgray;
102 margin: 0;
93 } 103 }
94 } 104 }
95 105
@@ -116,6 +126,10 @@
116 display: none; 126 display: none;
117 pointer-events: none; 127 pointer-events: none;
118 } 128 }
129
130 .Showcase {
131 display: block;
132 }
119} 133}
120 134
121.DumbyMap[data-layout=side] { 135.DumbyMap[data-layout=side] {
@@ -191,9 +205,14 @@
191 } 205 }
192 } 206 }
193 } 207 }
208
194 > :not(.draggable-block) { 209 > :not(.draggable-block) {
195 display: none; 210 display: none;
196 } 211 }
212
213 [data-placeholder] {
214 max-width: 50px;
215 }
197 } 216 }
198} 217}
199 218
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs
index 6b64ee4..5800351 100644
--- a/src/dumbymap.mjs
+++ b/src/dumbymap.mjs
@@ -217,90 +217,6 @@ export const generateMaps = async (container, callback) => {
217 childRect.bottom < parentRect.bottom - offset 217 childRect.bottom < parentRect.bottom - offset
218 } 218 }
219 //}}} 219 //}}}
220 // Render Maps {{{
221
222 const afterEachMapLoaded = (mapContainer) => {
223 const focusClickedMap = () => {
224 if (container.getAttribute('data-layout') !== 'none') return
225
226 container.querySelectorAll('.map-container')
227 .forEach(c => c.classList.remove('focus'))
228 mapContainer.classList.add('focus')
229 }
230 mapContainer.onclick = focusClickedMap
231 }
232
233 // Set unique ID for map container
234 const mapIdList = []
235 const assignMapId = (config) => {
236 let mapId = config.id
237 if (!mapId) {
238 mapId = config.use?.split('/')?.at(-1)
239 let counter = 2
240 while (mapIdList.includes(mapId)) {
241 mapId = `${config.use}.${counter}`
242 counter++
243 }
244 config.id = mapId
245 }
246 mapIdList.push(mapId)
247 return config
248 }
249
250 // Render each code block with "language-map" class
251 const render = renderWith(config => ({ width: "100%", ...config }))
252 const renderTargets = Array.from(container.querySelectorAll('pre:has(.language-map)'))
253 .map(async (target) => {
254 // Get text in code block starts with '```map'
255 const configText = target.querySelector('.language-map')
256 .textContent
257 // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content
258 // replace it by normal space
259 .replace(/\u00A0/g, '\u0020')
260
261 let configList = []
262 try {
263 configList = parseConfigsFromYaml(configText).map(assignMapId)
264 } catch (_) {
265 console.warn('Fail to parse yaml config for element', target)
266 }
267
268 // Render maps
269 return render(target, configList)
270 .then(results => {
271 results.forEach((mapByConfig) => {
272 if (mapByConfig.status === 'fulfilled') {
273 afterEachMapLoaded(mapByConfig.value)
274 return mapByConfig.value
275 } else {
276 console.error('Fail to render target element', mapByConfig.reason)
277 }
278 })
279 })
280 })
281
282 const renderAllTargets = Promise.all(renderTargets)
283 renderAllTargets.then(() => {
284 console.info('Finish Rendering')
285
286 const maps = htmlHolder.querySelectorAll('.map-container') ?? []
287 Array.from(maps)
288 .forEach(ele => {
289 callback(ele)
290 const markers = geoLinks
291 .filter(link => !link.targets || link.targets.includes(ele.id))
292 .map(link => ({
293 xy: link.xy,
294 title: link.url.pathname
295 }))
296 ele?.renderer?.addMarkers(markers)
297 })
298
299 htmlHolder.querySelectorAll('.marker')
300 .forEach(marker => htmlHolder.anchors.push(marker))
301 })
302
303 //}}}
304 // Draggable Blocks{{{ 220 // Draggable Blocks{{{
305 // Add draggable part for blocks 221 // Add draggable part for blocks
306 htmlHolder.blocks = Array.from(htmlHolder.querySelectorAll('.draggable-block')) 222 htmlHolder.blocks = Array.from(htmlHolder.querySelectorAll('.draggable-block'))
@@ -322,16 +238,35 @@ export const generateMaps = async (container, callback) => {
322 238
323 // }}} 239 // }}}
324 // CSS observer {{{ 240 // CSS observer {{{
325
326 // Set focusArea 241 // Set focusArea
327 const showcase = document.createElement('div') 242 const showcase = document.createElement('div')
328 container.appendChild(showcase) 243 container.appendChild(showcase)
329 showcase.classList.add('Showcase') 244 showcase.classList.add('Showcase')
330 const mapPlaceholder = document.createElement('div')
331 mapPlaceholder.id = 'mapPlaceholder'
332 showcase.appendChild(mapPlaceholder)
333 245
334 // Layout{{{ 246 // Focus Map {{{
247 const mapFocusObserver = () => new MutationObserver((mutations) => {
248 const mutation = mutations.at(-1)
249 const target = mutation.target
250 const focus = target.getAttribute(mutation.attributeName) === 'true'
251 const shouldBeInShowcase = focus && getComputedStyle(showcase).display !== 'none'
252
253 if (shouldBeInShowcase) {
254 if (showcase.contains(target)) return
255
256 const placeholder = document.createElement('div')
257 placeholder.setAttribute('data-placeholder', target.id)
258 placeholder.style.width = target.style.width
259 target.parentElement.replaceChild(placeholder, target)
260 showcase.appendChild(target)
261 } else if (showcase.contains(target)) {
262 const placeholder = htmlHolder.querySelector(`[data-placeholder="${target.id}"]`)
263 if (!placeholder) throw Error(`Cannot fine placeholder for map "${target.id}"`)
264 placeholder.parentElement.replaceChild(target, placeholder)
265 placeholder.remove()
266 }
267 })
268 // }}}
269 // Layout {{{
335 270
336 // press key to switch layout 271 // press key to switch layout
337 const layouts = ['none', 'side', 'overlay'] 272 const layouts = ['none', 'side', 'overlay']
@@ -339,41 +274,62 @@ export const generateMaps = async (container, callback) => {
339 274
340 // FIXME Use UI to switch layouts 275 // FIXME Use UI to switch layouts
341 const originalKeyDown = document.onkeydown 276 const originalKeyDown = document.onkeydown
342 document.onkeydown = (event) => { 277 document.onkeydown = (e) => {
343 originalKeyDown(event) 278 const event = originalKeyDown(e)
279 if (!event) return
280
344 if (event.key === 'x' && container.querySelector('.map-container')) { 281 if (event.key === 'x' && container.querySelector('.map-container')) {
282 e.preventDefault()
345 let currentLayout = container.getAttribute('data-layout') 283 let currentLayout = container.getAttribute('data-layout')
346 currentLayout = currentLayout ? currentLayout : 'none' 284 currentLayout = currentLayout ? currentLayout : 'none'
347 const nextLayout = layouts[(layouts.indexOf(currentLayout) + 1) % layouts.length] 285 const nextIndex = (layouts.indexOf(currentLayout) + 1) % layouts.length
286 const nextLayout = layouts[nextIndex]
348 287
349 container.setAttribute("data-layout", nextLayout) 288 container.setAttribute("data-layout", nextLayout)
350 } 289 }
290
291 if (event.key === 'Tab') {
292 e.preventDefault()
293
294 const selector = '.map-container, [data-placeholder]'
295 const candidates = Array.from(htmlHolder.querySelectorAll(selector))
296 if (candidates.length === 0) return
297
298 const currentFocus = htmlHolder.querySelector('.map-container[data-focus=true]')
299 ?? htmlHolder.querySelector('[data-placeholder]')
300 Array.from(container.querySelectorAll('.map-container')).forEach(e =>
301 e.removeAttribute('data-focus')
302 );
303 const index = currentFocus
304 ? (candidates.indexOf(currentFocus) + (event.shiftKey ? -1 : 1)) % candidates.length
305 : 0
306 const nextFocus = candidates.at(index)
307 nextFocus.setAttribute('data-focus', "true")
308 }
351 } 309 }
352 310
353 // observe layout change 311 // observe layout change
354 const layoutObserver = new MutationObserver(() => { 312 const layoutObserver = new MutationObserver((mutations) => {
355 const layout = container.getAttribute('data-layout') 313 const mutation = mutations.at(-1)
356 htmlHolder.blocks.forEach(b => b.style.display = "block") 314 const layout = container.getAttribute(mutation.attributeName)
357 315
358 if (layout === 'none') { 316 // Trigger Mutation Observer
359 mapPlaceholder.innerHTML = "" 317 const focusMap = container.querySelector('.map-container[data-focus=true]')
360 const map = showcase.querySelector('.map-container') 318 ?? container.querySelector('.map-container')
361 // Swap focused map and palceholder in markdown 319 focusMap?.setAttribute('data-focus', 'true')
362 if (map) { 320
363 mapPlaceholder.parentElement?.replaceChild(map, mapPlaceholder) 321 // Check empty block with map-container in showcase
364 showcase.append(mapPlaceholder) 322 htmlHolder.blocks.forEach(b => {
365 } 323 const contentChildren = Array.from(b.querySelectorAll(':scope > :not(.draggable)')) ?? []
366 } else { 324 if (contentChildren.length === 1
367 // If paceholder is not set, create one and put map into focusArea 325 && elementsWithMapConfig.includes(contentChildren[0])
368 if (showcase.contains(mapPlaceholder)) { 326 && !contentChildren[0].querySelector('.map-container')
369 const mapContainer = container.querySelector('.map-container.focus') ?? container.querySelector('.map-container') 327 ) {
370 mapPlaceholder.innerHTML = `<div>Placeholder</div>` 328 b.style.display = "none"
371 // TODO Get snapshot image 329 } else {
372 // mapPlaceholder.src = map.map.getCanvas().toDataURL() 330 b.style.display = "block"
373 mapContainer.parentElement?.replaceChild(mapPlaceholder, mapContainer)
374 showcase.appendChild(mapContainer)
375 } 331 }
376 } 332 })
377 333
378 if (layout === 'overlay') { 334 if (layout === 'overlay') {
379 let [x, y] = [0, 0]; 335 let [x, y] = [0, 0];
@@ -398,7 +354,9 @@ export const generateMaps = async (container, callback) => {
398 block.removeAttribute('style') 354 block.removeAttribute('style')
399 try { 355 try {
400 block.draggableInstance.remove() 356 block.draggableInstance.remove()
401 } catch (_) { null } 357 } catch (_) {
358 null
359 }
402 }) 360 })
403 } 361 }
404 }); 362 });
@@ -411,5 +369,99 @@ export const generateMaps = async (container, callback) => {
411 onRemove(htmlHolder, () => layoutObserver.disconnect()) 369 onRemove(htmlHolder, () => layoutObserver.disconnect())
412 //}}} 370 //}}}
413 //}}} 371 //}}}
372 // Render Maps {{{
373
374 const afterEachMapLoaded = (mapContainer) => {
375 const focusClickedMap = () => {
376 container.querySelectorAll('.map-container')
377 .forEach(c => c.removeAttribute('data-focus'))
378 mapContainer.setAttribute('data-focus', true)
379 }
380 mapContainer.onclick = focusClickedMap
381 mapContainer.setAttribute('tabindex', "-1")
382
383 const observer = mapFocusObserver()
384 mapFocusObserver().observe(mapContainer, {
385 attributes: true,
386 attributeFilter: ["data-focus"],
387 attributeOldValue: true
388 });
389 onRemove(mapContainer, () => observer.disconnect())
390 }
391
392 // Set unique ID for map container
393 const mapIdList = []
394 const assignMapId = (config) => {
395 let mapId = config.id
396 if (!mapId) {
397 mapId = config.use?.split('/')?.at(-1)
398 let counter = 1
399 while (!mapId || mapIdList.includes(mapId)) {
400 mapId = `${config.use ?? "unnamed"}-${counter}`
401 counter++
402 }
403 config.id = mapId
404 }
405 mapIdList.push(mapId)
406 return config
407 }
408
409 // Render each code block with "language-map" class
410 const elementsWithMapConfig = Array.from(container.querySelectorAll('pre:has(.language-map)') ?? [])
411 const render = renderWith(config => ({ width: "100%", ...config }))
412 const renderTargets = elementsWithMapConfig
413 .map(async (target) => {
414 // Get text in code block starts with '```map'
415 const configText = target.querySelector('.language-map')
416 .textContent
417 // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content
418 // replace it by normal space
419 .replace(/\u00A0/g, '\u0020')
420
421 let configList = []
422 try {
423 configList = parseConfigsFromYaml(configText).map(assignMapId)
424 } catch (_) {
425 console.warn('Fail to parse yaml config for element', target)
426 }
427
428 // Render maps
429 return render(target, configList)
430 .then(results => {
431 results.forEach((mapByConfig) => {
432 if (mapByConfig.status === 'fulfilled') {
433 afterEachMapLoaded(mapByConfig.value)
434 return mapByConfig.value
435 } else {
436 console.error('Fail to render target element', mapByConfig.reason)
437 }
438 })
439 })
440 })
441
442 const renderAllTargets = Promise.all(renderTargets)
443 .then(() => {
444 console.info('Finish Rendering')
445
446 const maps = htmlHolder.querySelectorAll('.map-container') ?? []
447 Array.from(maps)
448 .forEach(ele => {
449 callback(ele)
450 const markers = geoLinks
451 .filter(link => !link.targets || link.targets.includes(ele.id))
452 .map(link => ({
453 xy: link.xy,
454 title: link.url.pathname
455 }))
456 ele?.renderer?.addMarkers(markers)
457 })
458
459 htmlHolder.querySelectorAll('.marker')
460 .forEach(marker => htmlHolder.anchors.push(marker))
461
462 return maps
463 })
464
465 //}}}
414 return renderAllTargets 466 return renderAllTargets
415} 467}
diff --git a/src/editor.mjs b/src/editor.mjs
index b2c4d54..168e66b 100644
--- a/src/editor.mjs
+++ b/src/editor.mjs
@@ -40,7 +40,8 @@ const editor = new EasyMDE({
40 shortcuts: { 40 shortcuts: {
41 "map": "Ctrl-Alt-M", 41 "map": "Ctrl-Alt-M",
42 "debug": "Ctrl-Alt-D", 42 "debug": "Ctrl-Alt-D",
43 "toggleUnorderedList": "Ctrl-Shift-L", 43 "toggleUnorderedList": null,
44 "toggleOrderedList": null,
44 }, 45 },
45 toolbar: [ 46 toolbar: [
46 { 47 {
@@ -156,12 +157,12 @@ const debounceForMap = (() => {
156 } 157 }
157})() 158})()
158 159
159const afterMapRendered = (mapHolder) => { 160const afterMapRendered = (_) => {
160 mapHolder.oncontextmenu = (event) => { 161 // mapHolder.oncontextmenu = (event) => {
161 event.preventDefault() 162 // event.preventDefault()
162 const lonLat = mapHolder.renderer.unproject([event.x, event.y]) 163 // const lonLat = mapHolder.renderer.unproject([event.x, event.y])
163 // TODO... 164 // // TODO...
164 } 165 // }
165} 166}
166 167
167const updateDumbyMap = () => { 168const updateDumbyMap = () => {
@@ -174,7 +175,6 @@ updateDumbyMap()
174 175
175// Re-render HTML by editor content 176// Re-render HTML by editor content
176cm.on("change", (_, change) => { 177cm.on("change", (_, change) => {
177 console.log('change', change.text)
178 updateDumbyMap() 178 updateDumbyMap()
179 addClassToCodeLines() 179 addClassToCodeLines()
180 completeForCodeBlock(change) 180 completeForCodeBlock(change)
@@ -345,7 +345,6 @@ const handleTypingInCodeBlock = (anchor) => {
345// }}} 345// }}}
346// FUNCTION: get suggestions by current input {{{ 346// FUNCTION: get suggestions by current input {{{
347const getSuggestions = (anchor) => { 347const getSuggestions = (anchor) => {
348 let suggestions = []
349 const text = cm.getLine(anchor.line) 348 const text = cm.getLine(anchor.line)
350 349
351 // Clear marks on text 350 // Clear marks on text
@@ -445,7 +444,7 @@ const getSuggestions = (anchor) => {
445 ) 444 )
446 return rendererSuggestions.length > 0 ? rendererSuggestions : [] 445 return rendererSuggestions.length > 0 ? rendererSuggestions : []
447 } 446 }
448 return suggestions 447 return []
449} 448}
450// }}} 449// }}}
451// {{{ FUNCTION: Show element about suggestions 450// {{{ FUNCTION: Show element about suggestions
@@ -505,11 +504,9 @@ cm.on("cursorActivity", (_) => {
505}); 504});
506// }}} 505// }}}
507// EVENT: keydown for suggestions {{{ 506// EVENT: keydown for suggestions {{{
507const keyForSuggestions = ['Tab', 'Enter', 'Escape']
508cm.on('keydown', (_, e) => { 508cm.on('keydown', (_, e) => {
509 509 if (!cm.hasFocus || !keyForSuggestions.includes(e.key) || suggestionsEle.style.display === 'none') return;
510 // Only the following keys are used
511 const keyForSuggestions = ['Tab', 'Enter', 'Escape'].includes(e.key)
512 if (!keyForSuggestions || suggestionsEle.style.display === 'none') return;
513 510
514 // Directly add a newline when no suggestion is selected 511 // Directly add a newline when no suggestion is selected
515 const currentSuggestion = suggestionsEle.querySelector('.container__suggestion.focus') 512 const currentSuggestion = suggestionsEle.querySelector('.container__suggestion.focus')
@@ -545,6 +542,13 @@ cm.on('keydown', (_, e) => {
545document.onkeydown = (e) => { 542document.onkeydown = (e) => {
546 if (e.altKey && e.ctrlKey && e.key === 'm') { 543 if (e.altKey && e.ctrlKey && e.key === 'm') {
547 toggleEditing() 544 toggleEditing()
545 e.preventDefault()
546 return null
547 }
548 if (cm.hasFocus()) {
549 return null
550 } else {
551 return e
548 } 552 }
549} 553}
550 554