From 4cf84d2fe96c004fef3b508ce344885db74917e2 Mon Sep 17 00:00:00 2001 From: Hsieh Chin Fan Date: Wed, 18 Sep 2024 23:41:15 +0800 Subject: 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 --- src/css/dumbymap.css | 27 ++++- src/dumbymap.mjs | 282 ++++++++++++++++++++++++++++++--------------------- src/editor.mjs | 32 +++--- 3 files changed, 208 insertions(+), 133 deletions(-) (limited to 'src') 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 @@ .Showcase { order: 2; - #mapPlaceholder { - display: none; + display: none; + + .map-container { + width: 100% !important; + height: 100% !important; } } @@ -74,9 +77,11 @@ } .map-container { + background: white; margin: 2px; - &.focus { - border: solid gray; + border: 3px solid transparent; + &[data-focus=true] { + border: 3px solid gray; } } @@ -90,6 +95,11 @@ width: fit-content; flex: 0 0; } + [data-placeholder] { + box-sizing: border-box; + border: 5px solid white; + background: lightgray; + margin: 0; } } @@ -116,6 +126,10 @@ display: none; pointer-events: none; } + + .Showcase { + display: block; + } } .DumbyMap[data-layout=side] { @@ -191,9 +205,14 @@ } } } + > :not(.draggable-block) { display: none; } + + [data-placeholder] { + max-width: 50px; + } } } diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs index 6b64ee4..5800351 100644 --- a/src/dumbymap.mjs +++ b/src/dumbymap.mjs @@ -216,90 +216,6 @@ export const generateMaps = async (container, callback) => { childRect.top > parentRect.top + offset && childRect.bottom < parentRect.bottom - offset } - //}}} - // Render Maps {{{ - - const afterEachMapLoaded = (mapContainer) => { - const focusClickedMap = () => { - if (container.getAttribute('data-layout') !== 'none') return - - container.querySelectorAll('.map-container') - .forEach(c => c.classList.remove('focus')) - mapContainer.classList.add('focus') - } - mapContainer.onclick = focusClickedMap - } - - // Set unique ID for map container - const mapIdList = [] - const assignMapId = (config) => { - let mapId = config.id - if (!mapId) { - mapId = config.use?.split('/')?.at(-1) - let counter = 2 - while (mapIdList.includes(mapId)) { - mapId = `${config.use}.${counter}` - counter++ - } - config.id = mapId - } - mapIdList.push(mapId) - return config - } - - // Render each code block with "language-map" class - const render = renderWith(config => ({ width: "100%", ...config })) - const renderTargets = Array.from(container.querySelectorAll('pre:has(.language-map)')) - .map(async (target) => { - // Get text in code block starts with '```map' - const configText = target.querySelector('.language-map') - .textContent - // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content - // replace it by normal space - .replace(/\u00A0/g, '\u0020') - - let configList = [] - try { - configList = parseConfigsFromYaml(configText).map(assignMapId) - } catch (_) { - console.warn('Fail to parse yaml config for element', target) - } - - // Render maps - return render(target, configList) - .then(results => { - results.forEach((mapByConfig) => { - if (mapByConfig.status === 'fulfilled') { - afterEachMapLoaded(mapByConfig.value) - return mapByConfig.value - } else { - console.error('Fail to render target element', mapByConfig.reason) - } - }) - }) - }) - - const renderAllTargets = Promise.all(renderTargets) - renderAllTargets.then(() => { - console.info('Finish Rendering') - - const maps = htmlHolder.querySelectorAll('.map-container') ?? [] - Array.from(maps) - .forEach(ele => { - callback(ele) - const markers = geoLinks - .filter(link => !link.targets || link.targets.includes(ele.id)) - .map(link => ({ - xy: link.xy, - title: link.url.pathname - })) - ele?.renderer?.addMarkers(markers) - }) - - htmlHolder.querySelectorAll('.marker') - .forEach(marker => htmlHolder.anchors.push(marker)) - }) - //}}} // Draggable Blocks{{{ // Add draggable part for blocks @@ -322,16 +238,35 @@ export const generateMaps = async (container, callback) => { // }}} // CSS observer {{{ - // Set focusArea const showcase = document.createElement('div') container.appendChild(showcase) showcase.classList.add('Showcase') - const mapPlaceholder = document.createElement('div') - mapPlaceholder.id = 'mapPlaceholder' - showcase.appendChild(mapPlaceholder) - // Layout{{{ + // Focus Map {{{ + const mapFocusObserver = () => new MutationObserver((mutations) => { + const mutation = mutations.at(-1) + const target = mutation.target + const focus = target.getAttribute(mutation.attributeName) === 'true' + const shouldBeInShowcase = focus && getComputedStyle(showcase).display !== 'none' + + if (shouldBeInShowcase) { + if (showcase.contains(target)) return + + const placeholder = document.createElement('div') + placeholder.setAttribute('data-placeholder', target.id) + placeholder.style.width = target.style.width + target.parentElement.replaceChild(placeholder, target) + showcase.appendChild(target) + } else if (showcase.contains(target)) { + const placeholder = htmlHolder.querySelector(`[data-placeholder="${target.id}"]`) + if (!placeholder) throw Error(`Cannot fine placeholder for map "${target.id}"`) + placeholder.parentElement.replaceChild(target, placeholder) + placeholder.remove() + } + }) + // }}} + // Layout {{{ // press key to switch layout const layouts = ['none', 'side', 'overlay'] @@ -339,41 +274,62 @@ export const generateMaps = async (container, callback) => { // FIXME Use UI to switch layouts const originalKeyDown = document.onkeydown - document.onkeydown = (event) => { - originalKeyDown(event) + document.onkeydown = (e) => { + const event = originalKeyDown(e) + if (!event) return + if (event.key === 'x' && container.querySelector('.map-container')) { + e.preventDefault() let currentLayout = container.getAttribute('data-layout') currentLayout = currentLayout ? currentLayout : 'none' - const nextLayout = layouts[(layouts.indexOf(currentLayout) + 1) % layouts.length] + const nextIndex = (layouts.indexOf(currentLayout) + 1) % layouts.length + const nextLayout = layouts[nextIndex] container.setAttribute("data-layout", nextLayout) } + + if (event.key === 'Tab') { + e.preventDefault() + + const selector = '.map-container, [data-placeholder]' + const candidates = Array.from(htmlHolder.querySelectorAll(selector)) + if (candidates.length === 0) return + + const currentFocus = htmlHolder.querySelector('.map-container[data-focus=true]') + ?? htmlHolder.querySelector('[data-placeholder]') + Array.from(container.querySelectorAll('.map-container')).forEach(e => + e.removeAttribute('data-focus') + ); + const index = currentFocus + ? (candidates.indexOf(currentFocus) + (event.shiftKey ? -1 : 1)) % candidates.length + : 0 + const nextFocus = candidates.at(index) + nextFocus.setAttribute('data-focus', "true") + } } // observe layout change - const layoutObserver = new MutationObserver(() => { - const layout = container.getAttribute('data-layout') - htmlHolder.blocks.forEach(b => b.style.display = "block") - - if (layout === 'none') { - mapPlaceholder.innerHTML = "" - const map = showcase.querySelector('.map-container') - // Swap focused map and palceholder in markdown - if (map) { - mapPlaceholder.parentElement?.replaceChild(map, mapPlaceholder) - showcase.append(mapPlaceholder) - } - } else { - // If paceholder is not set, create one and put map into focusArea - if (showcase.contains(mapPlaceholder)) { - const mapContainer = container.querySelector('.map-container.focus') ?? container.querySelector('.map-container') - mapPlaceholder.innerHTML = `
Placeholder
` - // TODO Get snapshot image - // mapPlaceholder.src = map.map.getCanvas().toDataURL() - mapContainer.parentElement?.replaceChild(mapPlaceholder, mapContainer) - showcase.appendChild(mapContainer) + const layoutObserver = new MutationObserver((mutations) => { + const mutation = mutations.at(-1) + const layout = container.getAttribute(mutation.attributeName) + + // Trigger Mutation Observer + const focusMap = container.querySelector('.map-container[data-focus=true]') + ?? container.querySelector('.map-container') + focusMap?.setAttribute('data-focus', 'true') + + // Check empty block with map-container in showcase + htmlHolder.blocks.forEach(b => { + const contentChildren = Array.from(b.querySelectorAll(':scope > :not(.draggable)')) ?? [] + if (contentChildren.length === 1 + && elementsWithMapConfig.includes(contentChildren[0]) + && !contentChildren[0].querySelector('.map-container') + ) { + b.style.display = "none" + } else { + b.style.display = "block" } - } + }) if (layout === 'overlay') { let [x, y] = [0, 0]; @@ -398,7 +354,9 @@ export const generateMaps = async (container, callback) => { block.removeAttribute('style') try { block.draggableInstance.remove() - } catch (_) { null } + } catch (_) { + null + } }) } }); @@ -410,6 +368,100 @@ export const generateMaps = async (container, callback) => { onRemove(htmlHolder, () => layoutObserver.disconnect()) //}}} + //}}} + // Render Maps {{{ + + const afterEachMapLoaded = (mapContainer) => { + const focusClickedMap = () => { + container.querySelectorAll('.map-container') + .forEach(c => c.removeAttribute('data-focus')) + mapContainer.setAttribute('data-focus', true) + } + mapContainer.onclick = focusClickedMap + mapContainer.setAttribute('tabindex', "-1") + + const observer = mapFocusObserver() + mapFocusObserver().observe(mapContainer, { + attributes: true, + attributeFilter: ["data-focus"], + attributeOldValue: true + }); + onRemove(mapContainer, () => observer.disconnect()) + } + + // Set unique ID for map container + const mapIdList = [] + const assignMapId = (config) => { + let mapId = config.id + if (!mapId) { + mapId = config.use?.split('/')?.at(-1) + let counter = 1 + while (!mapId || mapIdList.includes(mapId)) { + mapId = `${config.use ?? "unnamed"}-${counter}` + counter++ + } + config.id = mapId + } + mapIdList.push(mapId) + return config + } + + // Render each code block with "language-map" class + const elementsWithMapConfig = Array.from(container.querySelectorAll('pre:has(.language-map)') ?? []) + const render = renderWith(config => ({ width: "100%", ...config })) + const renderTargets = elementsWithMapConfig + .map(async (target) => { + // Get text in code block starts with '```map' + const configText = target.querySelector('.language-map') + .textContent + // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content + // replace it by normal space + .replace(/\u00A0/g, '\u0020') + + let configList = [] + try { + configList = parseConfigsFromYaml(configText).map(assignMapId) + } catch (_) { + console.warn('Fail to parse yaml config for element', target) + } + + // Render maps + return render(target, configList) + .then(results => { + results.forEach((mapByConfig) => { + if (mapByConfig.status === 'fulfilled') { + afterEachMapLoaded(mapByConfig.value) + return mapByConfig.value + } else { + console.error('Fail to render target element', mapByConfig.reason) + } + }) + }) + }) + + const renderAllTargets = Promise.all(renderTargets) + .then(() => { + console.info('Finish Rendering') + + const maps = htmlHolder.querySelectorAll('.map-container') ?? [] + Array.from(maps) + .forEach(ele => { + callback(ele) + const markers = geoLinks + .filter(link => !link.targets || link.targets.includes(ele.id)) + .map(link => ({ + xy: link.xy, + title: link.url.pathname + })) + ele?.renderer?.addMarkers(markers) + }) + + htmlHolder.querySelectorAll('.marker') + .forEach(marker => htmlHolder.anchors.push(marker)) + + return maps + }) + //}}} return renderAllTargets } 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({ shortcuts: { "map": "Ctrl-Alt-M", "debug": "Ctrl-Alt-D", - "toggleUnorderedList": "Ctrl-Shift-L", + "toggleUnorderedList": null, + "toggleOrderedList": null, }, toolbar: [ { @@ -156,12 +157,12 @@ const debounceForMap = (() => { } })() -const afterMapRendered = (mapHolder) => { - mapHolder.oncontextmenu = (event) => { - event.preventDefault() - const lonLat = mapHolder.renderer.unproject([event.x, event.y]) - // TODO... - } +const afterMapRendered = (_) => { + // mapHolder.oncontextmenu = (event) => { + // event.preventDefault() + // const lonLat = mapHolder.renderer.unproject([event.x, event.y]) + // // TODO... + // } } const updateDumbyMap = () => { @@ -174,7 +175,6 @@ updateDumbyMap() // Re-render HTML by editor content cm.on("change", (_, change) => { - console.log('change', change.text) updateDumbyMap() addClassToCodeLines() completeForCodeBlock(change) @@ -345,7 +345,6 @@ const handleTypingInCodeBlock = (anchor) => { // }}} // FUNCTION: get suggestions by current input {{{ const getSuggestions = (anchor) => { - let suggestions = [] const text = cm.getLine(anchor.line) // Clear marks on text @@ -445,7 +444,7 @@ const getSuggestions = (anchor) => { ) return rendererSuggestions.length > 0 ? rendererSuggestions : [] } - return suggestions + return [] } // }}} // {{{ FUNCTION: Show element about suggestions @@ -505,11 +504,9 @@ cm.on("cursorActivity", (_) => { }); // }}} // EVENT: keydown for suggestions {{{ +const keyForSuggestions = ['Tab', 'Enter', 'Escape'] cm.on('keydown', (_, e) => { - - // Only the following keys are used - const keyForSuggestions = ['Tab', 'Enter', 'Escape'].includes(e.key) - if (!keyForSuggestions || suggestionsEle.style.display === 'none') return; + if (!cm.hasFocus || !keyForSuggestions.includes(e.key) || suggestionsEle.style.display === 'none') return; // Directly add a newline when no suggestion is selected const currentSuggestion = suggestionsEle.querySelector('.container__suggestion.focus') @@ -545,6 +542,13 @@ cm.on('keydown', (_, e) => { document.onkeydown = (e) => { if (e.altKey && e.ctrlKey && e.key === 'm') { toggleEditing() + e.preventDefault() + return null + } + if (cm.hasFocus()) { + return null + } else { + return e } } -- cgit v1.2.3-70-g09d2