From 1c7e77b8546ac32b8176ab54dd06ede6ba7deb55 Mon Sep 17 00:00:00 2001 From: Hsieh Chin Fan Date: Wed, 2 Oct 2024 10:12:46 +0800 Subject: style: switch to standardjs --- src/dumbymap.mjs | 469 +++++++++++++++++++++++++++---------------------------- 1 file changed, 234 insertions(+), 235 deletions(-) (limited to 'src/dumbymap.mjs') 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 @@ -import MarkdownIt from 'markdown-it'; -import MarkdownItAnchor from 'markdown-it-anchor'; -import MarkdownItFootnote from 'markdown-it-footnote'; -import MarkdownItFrontMatter from 'markdown-it-front-matter'; -import MarkdownItTocDoneRight from 'markdown-it-toc-done-right'; -import LeaderLine from 'leader-line'; -import { renderWith, defaultAliases, parseConfigsFromYaml } from 'mapclay'; -import { onRemove, animateRectTransition, throttle } from './utils'; -import { Layout, SideBySide, Overlay } from './Layout'; -import * as utils from './dumbyUtils'; -import * as menuItem from './MenuItem'; -import { default as PlainModal } from 'plain-modal'; - -const docLinkSelector = 'a[href^="#"][title^="=>"]'; -const geoLinkSelector = 'a[href^="geo:"]'; +import MarkdownIt from 'markdown-it' +import MarkdownItAnchor from 'markdown-it-anchor' +import MarkdownItFootnote from 'markdown-it-footnote' +import MarkdownItFrontMatter from 'markdown-it-front-matter' +import MarkdownItTocDoneRight from 'markdown-it-toc-done-right' +import LeaderLine from 'leader-line' +import { renderWith, defaultAliases, parseConfigsFromYaml } from 'mapclay' +import { onRemove, animateRectTransition, throttle } from './utils' +import { Layout, SideBySide, Overlay } from './Layout' +import * as utils from './dumbyUtils' +import * as menuItem from './MenuItem' +import PlainModal from 'plain-modal' + +const docLinkSelector = 'a[href^="#"][title^="=>"]' +const geoLinkSelector = 'a[href^="geo:"]' const layouts = [ new Layout({ name: 'normal' }), new SideBySide({ name: 'side-by-side' }), - new Overlay({ name: 'overlay' }), -]; -const mapCache = {}; + new Overlay({ name: 'overlay' }) +] +const mapCache = {} export const markdown2HTML = (container, mdContent) => { // Render: Markdown -> HTML {{{ - container.replaceChildren(); + container.replaceChildren() - container.innerHTML = '
'; - const htmlHolder = container.querySelector('.SemanticHtml'); + container.innerHTML = '
' + const htmlHolder = container.querySelector('.SemanticHtml') const md = MarkdownIt({ html: true, - breaks: true, + breaks: true }) .use(MarkdownItAnchor, { permalink: MarkdownItAnchor.permalink.linkInsideHeader({ - placement: 'before', - }), + placement: 'before' + }) }) .use(MarkdownItFootnote) .use(MarkdownItFrontMatter) - .use(MarkdownItTocDoneRight); + .use(MarkdownItTocDoneRight) // FIXME A better way to generate blocks - md.renderer.rules.dumby_block_open = () => '
'; - md.renderer.rules.dumby_block_close = () => '
'; + md.renderer.rules.dumby_block_open = () => '
' + md.renderer.rules.dumby_block_close = () => '
' md.core.ruler.before('block', 'dumby_block', state => { - state.tokens.push(new state.Token('dumby_block_open', '', 1)); - }); + state.tokens.push(new state.Token('dumby_block_open', '', 1)) + }) // Add close tag for block with more than 2 empty lines md.block.ruler.before('table', 'dumby_block', (state, startLine) => { @@ -56,39 +56,39 @@ export const markdown2HTML = (container, mdContent) => { state.src[state.bMarks[startLine - 2]] === '\n' && state.tokens.at(-1).type !== 'list_item_open' // Quick hack for not adding tag after "::marker" for
  • ) { - state.push('dumby_block_close', '', -1); - state.push('dumby_block_open', '', 1); + state.push('dumby_block_close', '', -1) + state.push('dumby_block_open', '', 1) } - }); + }) md.core.ruler.after('block', 'dumby_block', state => { - state.tokens.push(new state.Token('dumby_block_close', '', -1)); - }); + state.tokens.push(new state.Token('dumby_block_close', '', -1)) + }) - const contentWithToc = '${toc}\n\n\n' + mdContent; - htmlHolder.innerHTML = md.render(contentWithToc); + const contentWithToc = '${toc}\n\n\n' + mdContent // eslint-disable-line + htmlHolder.innerHTML = md.render(contentWithToc) // TODO Do this in markdown-it - const blocks = htmlHolder.querySelectorAll(':scope > div:not(:has(nav))'); + const blocks = htmlHolder.querySelectorAll(':scope > div:not(:has(nav))') blocks.forEach(b => { - b.classList.add('dumby-block'); - b.setAttribute('data-total', blocks.length); - }); + b.classList.add('dumby-block') + b.setAttribute('data-total', blocks.length) + }) - return container; - //}}} -}; + return container + // }}} +} export const generateMaps = (container, { delay, mapCallback }) => { - container.classList.add('Dumby'); - const htmlHolder = container.querySelector('.SemanticHtml') ?? container; - const blocks = Array.from(htmlHolder.querySelectorAll('.dumby-block')); - const showcase = document.createElement('div'); - container.appendChild(showcase); - showcase.classList.add('Showcase'); - const renderPromises = []; - const modalContent = document.createElement('div'); - container.appendChild(modalContent); - const modal = new PlainModal(modalContent); + container.classList.add('Dumby') + const htmlHolder = container.querySelector('.SemanticHtml') ?? container + const blocks = Array.from(htmlHolder.querySelectorAll('.dumby-block')) + const showcase = document.createElement('div') + container.appendChild(showcase) + showcase.classList.add('Showcase') + const renderPromises = [] + const modalContent = document.createElement('div') + container.appendChild(modalContent) + const modal = new PlainModal(modalContent) const dumbymap = { layouts, @@ -102,50 +102,50 @@ export const generateMaps = (container, { delay, mapCallback }) => { ...utils, renderedMaps: () => Array.from( - container.querySelectorAll('.mapclay[data-render=fulfilled]'), + container.querySelectorAll('.mapclay[data-render=fulfilled]') ), focusNextMap: throttle(utils.focusNextMap, utils.focusDelay), - switchToNextLayout: throttle(utils.switchToNextLayout, 300), - }, - }; + switchToNextLayout: throttle(utils.switchToNextLayout, 300) + } + } Object.entries(dumbymap.utils).forEach(([util, func]) => { - dumbymap.utils[util] = func.bind(dumbymap); - }); + dumbymap.utils[util] = func.bind(dumbymap) + }) // LeaderLine {{{ Array.from(container.querySelectorAll(docLinkSelector)).filter( - utils.createDocLink, - ); + utils.createDocLink + ) // Get anchors with "geo:" scheme - htmlHolder.anchors = []; + htmlHolder.anchors = [] const geoLinkCallback = link => { - link.onmouseover = () => addLeaderLines(link); - link.onmouseout = () => removeLeaderLines(link); + link.onmouseover = () => addLeaderLines(link) + link.onmouseout = () => removeLeaderLines(link) link.onclick = event => { - event.preventDefault(); + event.preventDefault() htmlHolder.anchors .filter(isAnchorPointedBy(link)) - .forEach(updateMapByMarker(link.xy)); + .forEach(updateMapByMarker(link.xy)) // TODO Just hide leader line and show it again - removeLeaderLines(link); - }; - }; + removeLeaderLines(link) + } + } const geoLinks = Array.from( - container.querySelectorAll(geoLinkSelector), - ).filter(l => utils.createGeoLink(l, geoLinkCallback)); + container.querySelectorAll(geoLinkSelector) + ).filter(l => utils.createGeoLink(l, geoLinkCallback)) const isAnchorPointedBy = link => anchor => { - const mapContainer = anchor.closest('.mapclay'); - const isTarget = !link.targets || link.targets.includes(mapContainer.id); - return anchor.title === link.url.pathname && isTarget; - }; + const mapContainer = anchor.closest('.mapclay') + const isTarget = !link.targets || link.targets.includes(mapContainer.id) + return anchor.title === link.url.pathname && isTarget + } const isAnchorVisible = anchor => { - const mapContainer = anchor.closest('.mapclay'); - return insideWindow(anchor) && insideParent(anchor, mapContainer); - }; + const mapContainer = anchor.closest('.mapclay') + return insideWindow(anchor) && insideParent(anchor, mapContainer) + } const drawLeaderLine = link => anchor => { const line = new LeaderLine({ @@ -153,223 +153,222 @@ export const generateMaps = (container, { delay, mapCallback }) => { end: anchor, hide: true, middleLabel: link.url.searchParams.get('text'), - path: 'magnet', - }); - line.show('draw', { duration: 300 }); - return line; - }; + path: 'magnet' + }) + line.show('draw', { duration: 300 }) + return line + } const addLeaderLines = link => { link.lines = htmlHolder.anchors .filter(isAnchorPointedBy(link)) .filter(isAnchorVisible) - .map(drawLeaderLine(link)); - }; + .map(drawLeaderLine(link)) + } const removeLeaderLines = link => { - if (!link.lines) return; - link.lines.forEach(line => line.remove()); - link.lines = []; - }; + if (!link.lines) return + link.lines.forEach(line => line.remove()) + link.lines = [] + } const updateMapByMarker = xy => marker => { - const renderer = marker.closest('.mapclay')?.renderer; - renderer.updateCamera({ center: xy }, true); - }; + const renderer = marker.closest('.mapclay')?.renderer + renderer.updateCamera({ center: xy }, true) + } const insideWindow = element => { - const rect = element.getBoundingClientRect(); + const rect = element.getBoundingClientRect() return ( rect.left > 0 && rect.right < window.innerWidth + rect.width && rect.top > 0 && rect.bottom < window.innerHeight + rect.height - ); - }; + ) + } const insideParent = (childElement, parentElement) => { - const childRect = childElement.getBoundingClientRect(); - const parentRect = parentElement.getBoundingClientRect(); - const offset = 20; + const childRect = childElement.getBoundingClientRect() + const parentRect = parentElement.getBoundingClientRect() + const offset = 20 return ( childRect.left > parentRect.left + offset && childRect.right < parentRect.right - offset && childRect.top > parentRect.top + offset && childRect.bottom < parentRect.bottom - offset - ); - }; - //}}} + ) + } + // }}} // CSS observer {{{ // Focus Map {{{ // Set focusArea const mapFocusObserver = () => - new MutationObserver(mutations => { - const mutation = mutations.at(-1); - const target = mutation.target; - const focus = target.classList.contains('focus'); + new window.MutationObserver(mutations => { + const mutation = mutations.at(-1) + const target = mutation.target + const focus = target.classList.contains('focus') const shouldBeInShowcase = focus && showcase.checkVisibility({ contentVisibilityAuto: true, opacityProperty: true, - visibilityProperty: true, - }); + visibilityProperty: true + }) if (focus) { dumbymap.utils .renderedMaps() .filter(map => map.id !== target.id) - .forEach(map => map.classList.remove('focus')); + .forEach(map => map.classList.remove('focus')) } if (shouldBeInShowcase) { - if (showcase.contains(target)) return; + if (showcase.contains(target)) return // Placeholder for map in Showcase, it should has the same DOMRect - const placeholder = target.cloneNode(true); - placeholder.removeAttribute('id'); - placeholder.classList.remove('mapclay', 'focus'); - target.parentElement.replaceChild(placeholder, target); + const placeholder = target.cloneNode(true) + placeholder.removeAttribute('id') + placeholder.classList.remove('mapclay', 'focus') + target.parentElement.replaceChild(placeholder, target) // FIXME Maybe use @start-style for CSS // Trigger CSS transition, if placeholde is the olny chil element in block, // reduce its height to zero. // To make sure the original height of placeholder is applied, DOM changes seems needed // then set data-attribute for CSS selector to change height to 0 - placeholder.getBoundingClientRect(); - placeholder.setAttribute('data-placeholder', target.id); + placeholder.getBoundingClientRect() + placeholder.setAttribute('data-placeholder', target.id) // To fit showcase, remove all inline style - target.removeAttribute('style'); - showcase.appendChild(target); + target.removeAttribute('style') + showcase.appendChild(target) // Resume rect from Semantic HTML to Showcase, with animation animateRectTransition(target, placeholder.getBoundingClientRect(), { duration: 300, - resume: true, - }); + resume: true + }) } else if (showcase.contains(target)) { // Check placeholder is inside Semantic HTML const placeholder = htmlHolder.querySelector( - `[data-placeholder="${target.id}"]`, - ); - if (!placeholder) - throw Error(`Cannot fine placeholder for map "${target.id}"`); + `[data-placeholder="${target.id}"]` + ) + if (!placeholder) { throw Error(`Cannot fine placeholder for map "${target.id}"`) } // Consider animation may fail, write callback const afterAnimation = () => { - placeholder.parentElement.replaceChild(target, placeholder); - target.style = placeholder.style.cssText; - placeholder.remove(); - }; + placeholder.parentElement.replaceChild(target, placeholder) + target.style = placeholder.style.cssText + placeholder.remove() + } // animation from Showcase to placeholder animateRectTransition(target, placeholder.getBoundingClientRect(), { - duration: 300, - }).finished.finally(afterAnimation); + duration: 300 + }).finished.finally(afterAnimation) } - }); + }) // }}} // Layout {{{ // press key to switch layout // observe layout change - const layoutObserver = new MutationObserver(mutations => { - const mutation = mutations.at(-1); - const oldLayout = mutation.oldValue; - const newLayout = container.getAttribute(mutation.attributeName); + const layoutObserver = new window.MutationObserver(mutations => { + const mutation = mutations.at(-1) + const oldLayout = mutation.oldValue + const newLayout = container.getAttribute(mutation.attributeName) // Apply handler for leaving/entering layouts if (oldLayout) { layouts .find(l => l.name === oldLayout) - ?.leaveHandler?.call(this, dumbymap); + ?.leaveHandler?.call(this, dumbymap) } Object.values(dumbymap) .flat() - .filter(ele => ele instanceof HTMLElement) - .forEach(ele => ele.removeAttribute('style')); + .filter(ele => ele instanceof window.HTMLElement) + .forEach(ele => ele.removeAttribute('style')) if (newLayout) { layouts .find(l => l.name === newLayout) - ?.enterHandler?.call(this, dumbymap); + ?.enterHandler?.call(this, dumbymap) } // Since layout change may show/hide showcase, the current focused map may need to go into/outside showcase // Reset attribute triggers MutationObserver which is observing it const focusMap = container.querySelector('.mapclay.focus') ?? - container.querySelector('.mapclay'); - focusMap?.classList?.add('focus'); - }); + container.querySelector('.mapclay') + focusMap?.classList?.add('focus') + }) layoutObserver.observe(container, { attributes: true, attributeFilter: ['data-layout'], attributeOldValue: true, - characterDataOldValue: true, - }); + characterDataOldValue: true + }) - onRemove(htmlHolder, () => layoutObserver.disconnect()); - //}}} - //}}} + onRemove(htmlHolder, () => layoutObserver.disconnect()) + // }}} + // }}} // Render Maps {{{ const afterMapRendered = renderer => { - const mapElement = renderer.target; - //FIXME - mapElement.renderer = renderer; - mapElement.setAttribute('tabindex', '-1'); + const mapElement = renderer.target + // FIXME + mapElement.renderer = renderer + mapElement.setAttribute('tabindex', '-1') if (mapElement.getAttribute('data-render') === 'fulfilled') { - mapCache[mapElement.id] = renderer; + mapCache[mapElement.id] = renderer } // Execute callback from caller - mapCallback?.call(this, mapElement); + mapCallback?.call(this, mapElement) const markers = geoLinks .filter(link => !link.targets || link.targets.includes(mapElement.id)) - .map(link => ({ xy: link.xy, title: link.url.pathname })); + .map(link => ({ xy: link.xy, title: link.url.pathname })) // Add markers with Geolinks - renderer.addMarkers(markers); + renderer.addMarkers(markers) mapElement .querySelectorAll('.marker') - .forEach(marker => htmlHolder.anchors.push(marker)); + .forEach(marker => htmlHolder.anchors.push(marker)) // Work with Mutation Observer - const observer = mapFocusObserver(); + const observer = mapFocusObserver() mapFocusObserver().observe(mapElement, { attributes: true, attributeFilter: ['class'], - attributeOldValue: true, - }); - onRemove(mapElement, () => observer.disconnect()); - }; + attributeOldValue: true + }) + onRemove(mapElement, () => observer.disconnect()) + } // Set unique ID for map container - const mapIdList = []; + const mapIdList = [] const assignMapId = config => { - let mapId = config.id; + let mapId = config.id if (!mapId) { - mapId = config.use?.split('/')?.at(-1); - let counter = 1; + mapId = config.use?.split('/')?.at(-1) + let counter = 1 while (!mapId || mapIdList.includes(mapId)) { - mapId = `${config.use ?? 'unnamed'}-${counter}`; - counter++; + mapId = `${config.use ?? 'unnamed'}-${counter}` + counter++ } - config.id = mapId; + config.id = mapId } - mapIdList.push(mapId); - return config; - }; + mapIdList.push(mapId) + return config + } // Render each code block with "language-map" class const elementsWithMapConfig = Array.from( - container.querySelectorAll('pre:has(.language-map)') ?? [], - ); + container.querySelectorAll('pre:has(.language-map)') ?? [] + ) /** * updateAttributeByStep. * @@ -377,21 +376,21 @@ export const generateMaps = (container, { delay, mapCallback }) => { */ const updateAttributeByStep = ({ results, target, steps }) => { let passNum = results.filter( - r => r.type === 'render' && r.state.match(/success|skip/), - ).length; - const total = steps.length; - passNum += `/${total}`; + r => r.type === 'render' && r.state.match(/success|skip/) + ).length + const total = steps.length + passNum += `/${total}` if (results.filter(r => r.type === 'render').length === total) { - passNum += '\u0020'; + passNum += '\u0020' } // FIXME HACK use MutationObserver for animation - if (!target.animations) target.animations = Promise.resolve(); + if (!target.animations) target.animations = Promise.resolve() target.animations = target.animations.then(async () => { - await new Promise(resolve => setTimeout(resolve, 100)); - target.setAttribute('data-report', passNum); - }); - }; + await new Promise(resolve => setTimeout(resolve, 100)) + target.setAttribute('data-report', passNum) + }) + } /** * config converter for mapclay.renderWith() * @@ -404,42 +403,42 @@ export const generateMaps = (container, { delay, mapCallback }) => { ...config, aliases: { ...defaultAliases, - ...(config.aliases ?? {}), + ...(config.aliases ?? {}) }, - stepCallback: updateAttributeByStep, - }); - const render = renderWith(configConverter); + stepCallback: updateAttributeByStep + }) + const render = renderWith(configConverter) elementsWithMapConfig.forEach(target => { // Get text in code block starts with markdown text '```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'); + .replace(/\u00A0/g, '\u0020') - let configList = []; + let configList = [] try { - configList = parseConfigsFromYaml(configText).map(assignMapId); + configList = parseConfigsFromYaml(configText).map(assignMapId) } catch (_) { - console.warn('Fail to parse yaml config for element', target); - return; + console.warn('Fail to parse yaml config for element', target) + return } // If map in cache has the same ID, just put it into target // So user won't feel anything changes when editing markdown configList.forEach(config => { - const cache = mapCache[config.id]; - if (!cache) return; + const cache = mapCache[config.id] + if (!cache) return - target.appendChild(cache.target); - config.target = cache.target; - }); + target.appendChild(cache.target) + config.target = cache.target + }) // trivial: if map cache is applied, do not show yaml text if (target.querySelector('.mapclay')) { target .querySelectorAll(':scope > :not([data-render=fulfilled])') - .forEach(e => e.remove()); + .forEach(e => e.remove()) } // TODO Use debounce of user input to decide rendering timing @@ -447,76 +446,76 @@ export const generateMaps = (container, { delay, mapCallback }) => { const timer = setTimeout( () => render(target, configList).forEach(renderPromise => { - renderPromises.push(renderPromise); - renderPromise.then(afterMapRendered); + renderPromises.push(renderPromise) + renderPromise.then(afterMapRendered) }), - delay ?? 1000, - ); + delay ?? 1000 + ) onRemove(htmlHolder, () => { - clearTimeout(timer); - }); - }); + clearTimeout(timer) + }) + }) // }}} // Menu {{{ - const menu = document.createElement('div'); - menu.className = 'menu'; - menu.style.display = 'none'; - menu.onclick = () => (menu.style.display = 'none'); - container.appendChild(menu); + const menu = document.createElement('div') + menu.className = 'menu' + menu.style.display = 'none' + menu.onclick = () => (menu.style.display = 'none') + container.appendChild(menu) // Menu Items container.oncontextmenu = e => { - menu.replaceChildren(); - menu.style.display = 'block'; - menu.style.cssText = `left: ${e.x - menu.offsetParent.offsetLeft + 10}px; top: ${e.y - menu.offsetParent.offsetTop + 5}px;`; - e.preventDefault(); + menu.replaceChildren() + menu.style.display = 'block' + menu.style.cssText = `left: ${e.x - menu.offsetParent.offsetLeft + 10}px; top: ${e.y - menu.offsetParent.offsetTop + 5}px;` + e.preventDefault() // GeoLinks - const selection = document.getSelection(); + const selection = document.getSelection() if (selection.type === 'Range') { - const range = selection.getRangeAt(0); - menu.appendChild(menuItem.addGeoLink(dumbymap, range)); + const range = selection.getRangeAt(0) + menu.appendChild(menuItem.addGeoLink(dumbymap, range)) } // Menu Items for map - const map = e.target.closest('.mapclay'); + const map = e.target.closest('.mapclay') if (map?.renderer?.results) { // Focus or Print Map Results - menu.appendChild(menuItem.toggleMapFocus(map)); - menu.appendChild(menuItem.renderResults(dumbymap, map)); + menu.appendChild(menuItem.toggleMapFocus(map)) + menu.appendChild(menuItem.renderResults(dumbymap, map)) } else { // Toggle block focus - const block = e.target.closest('.dumby-block'); + const block = e.target.closest('.dumby-block') if (block) { - menu.appendChild(menuItem.toggleBlockFocus(block)); + menu.appendChild(menuItem.toggleBlockFocus(block)) } } // Menu Items for map/block/layout if (!map || map.closest('.Showcase')) { - menu.appendChild(menuItem.pickMapItem(dumbymap)); - menu.appendChild(menuItem.pickBlockItem(dumbymap)); - menu.appendChild(menuItem.pickLayoutItem(dumbymap)); + menu.appendChild(menuItem.pickMapItem(dumbymap)) + menu.appendChild(menuItem.pickBlockItem(dumbymap)) + menu.appendChild(menuItem.pickLayoutItem(dumbymap)) } - }; + } // Remove menu when click outside const actionOutsideMenu = e => { - if (menu.style.display === 'none') return; - const rect = menu.getBoundingClientRect(); + if (menu.style.display === 'none') return + const rect = menu.getBoundingClientRect() if ( e.clientX < rect.left || e.clientX > rect.left + rect.width || e.clientY < rect.top || e.clientY > rect.top + rect.height ) { - menu.style.display = 'none'; + menu.style.display = 'none' } - }; - document.addEventListener('click', actionOutsideMenu); + } + document.addEventListener('click', actionOutsideMenu) onRemove(htmlHolder, () => - document.removeEventListener('click', actionOutsideMenu), - ); - //}}} - return Object.seal(dumbymap); -}; + document.removeEventListener('click', actionOutsideMenu) + ) + // }}} + return Object.seal(dumbymap) +} -- cgit v1.2.3-70-g09d2