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/Layout.mjs | 231 +++++++++++----------- src/MenuItem.mjs | 248 ++++++++++++------------ src/dumbyUtils.mjs | 130 ++++++------- src/dumbymap.mjs | 469 +++++++++++++++++++++++---------------------- src/editor.mjs | 548 ++++++++++++++++++++++++++--------------------------- src/utils.mjs | 67 ++++--- 6 files changed, 846 insertions(+), 847 deletions(-) (limited to 'src') 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 @@ -import PlainDraggable from 'plain-draggable'; -import { onRemove, animateRectTransition } from './utils'; +import PlainDraggable from 'plain-draggable' +import { onRemove, animateRectTransition } from './utils' export class Layout { - constructor(options = {}) { - if (!options.name) throw Error('Layout name is not given'); - this.name = options.name; - this.enterHandler = options.enterHandler; - this.leaveHandler = options.leaveHandler; + constructor (options = {}) { + if (!options.name) throw Error('Layout name is not given') + this.name = options.name + this.enterHandler = options.enterHandler + this.leaveHandler = options.leaveHandler } - valueOf = () => this.name; + + valueOf = () => this.name } export class SideBySide extends Layout { - name = 'side-by-side'; + name = 'side-by-side' enterHandler = ({ container, htmlHolder, showcase }) => { - const bar = document.createElement('div'); - bar.className = 'bar'; - bar.innerHTML = '
'; - const handle = bar.querySelector('.bar-handle'); - container.appendChild(bar); + const bar = document.createElement('div') + bar.className = 'bar' + bar.innerHTML = '
' + const handle = bar.querySelector('.bar-handle') + container.appendChild(bar) // Resize views by value const resizeByLeft = left => { - htmlHolder.style.width = left + 'px'; + htmlHolder.style.width = left + 'px' showcase.style.width = - parseFloat(getComputedStyle(container).width) - left + 'px'; - }; + parseFloat(window.getComputedStyle(container).width) - left + 'px' + } const draggable = new PlainDraggable(bar, { - handle: handle, - containment: { left: '25%', top: 0, right: '75%', height: 0 }, - }); - draggable.draggableCursor = 'grab'; + handle, + containment: { left: '25%', top: 0, right: '75%', height: 0 } + }) + draggable.draggableCursor = 'grab' draggable.onDrag = pos => { - handle.style.transform = 'unset'; - resizeByLeft(pos.left); - }; + handle.style.transform = 'unset' + resizeByLeft(pos.left) + } draggable.onDragEnd = _ => { - handle.removeAttribute('style'); - }; + handle.removeAttribute('style') + } - onRemove(bar, () => draggable.remove()); - }; + onRemove(bar, () => draggable.remove()) + } leaveHandler = ({ container }) => { - container.querySelector('.bar')?.remove(); - }; + container.querySelector('.bar')?.remove() + } } export class Overlay extends Layout { - name = 'overlay'; + name = 'overlay' saveLeftTopAsData = element => { - const { left, top } = element.getBoundingClientRect(); - element.setAttribute('data-left', left); - element.setAttribute('data-top', top); - }; + const { left, top } = element.getBoundingClientRect() + element.setAttribute('data-left', left) + element.setAttribute('data-top', top) + } addDraggable = element => { // Make sure current element always on top const siblings = Array.from( - element.parentElement?.querySelectorAll(':scope > *') ?? [], - ); - let popTimer = null; + element.parentElement?.querySelectorAll(':scope > *') ?? [] + ) + let popTimer = null element.onmouseover = () => { popTimer = setTimeout(() => { - siblings.forEach(e => e.style.removeProperty('z-index')); - element.style.zIndex = '9001'; - }, 200); - }; + siblings.forEach(e => e.style.removeProperty('z-index')) + element.style.zIndex = '9001' + }, 200) + } element.onmouseout = () => { - clearTimeout(popTimer); - }; + clearTimeout(popTimer) + } // Add draggable part - const draggablePart = document.createElement('div'); - element.appendChild(draggablePart); - draggablePart.className = 'draggable-part'; - draggablePart.innerHTML = '
\u2630
'; + const draggablePart = document.createElement('div') + element.appendChild(draggablePart) + draggablePart.className = 'draggable-part' + draggablePart.innerHTML = '
\u2630
' // Add draggable instance - const { left, top } = element.getBoundingClientRect(); + const { left, top } = element.getBoundingClientRect() const draggable = new PlainDraggable(element, { - top: top, - left: left, + top, + left, handle: draggablePart, - snap: { x: { step: 20 }, y: { step: 20 } }, - }); + snap: { x: { step: 20 }, y: { step: 20 } } + }) // FIXME use pure CSS to hide utils - const utils = element.querySelector('.utils'); + const utils = element.querySelector('.utils') draggable.onDragStart = () => { - utils.style.display = 'none'; - element.classList.add('drag'); - }; + utils.style.display = 'none' + element.classList.add('drag') + } draggable.onDragEnd = () => { - utils.style = ''; - element.classList.remove('drag'); - element.style.zIndex = '9000'; - }; + utils.style = '' + element.classList.remove('drag') + element.style.zIndex = '9000' + } // Reposition draggable instance when resized - new ResizeObserver(() => { + new window.ResizeObserver(() => { try { - draggable.position(); - } catch (_) { - null; + draggable.position() + } catch (err) { + console.warn(err) } - }).observe(element); + }).observe(element) // Callback for remove onRemove(element, () => { - draggable.remove(); - }); - }; + draggable.remove() + }) + } enterHandler = ({ htmlHolder, blocks }) => { // FIXME It is weird rect from this method and this scope are different... - blocks.forEach(this.saveLeftTopAsData); + blocks.forEach(this.saveLeftTopAsData) // Create draggable blocks and set each position by previous one - let [left, top] = [20, 20]; + let [left, top] = [20, 20] blocks.forEach(block => { - const originLeft = Number(block.getAttribute('data-left')); - const originTop = Number(block.getAttribute('data-top')); + const originLeft = Number(block.getAttribute('data-left')) + const originTop = Number(block.getAttribute('data-top')) // Create draggable block - const wrapper = document.createElement('div'); - wrapper.classList.add('draggable-block'); + const wrapper = document.createElement('div') + wrapper.classList.add('draggable-block') wrapper.innerHTML = `
\u274C
\u2795
\u2796
- `; - wrapper.title = 'Middle-click to hide block'; + ` + wrapper.title = 'Middle-click to hide block' wrapper.onmouseup = e => { // Hide block with middle click if (e.button === 1) { - wrapper.classList.add('hide'); + wrapper.classList.add('hide') } - }; + } // Set DOMRect for wrapper - wrapper.appendChild(block); - wrapper.style.left = left + 'px'; - wrapper.style.top = top + 'px'; - htmlHolder.appendChild(wrapper); - const { width } = wrapper.getBoundingClientRect(); - left += width + 30; + wrapper.appendChild(block) + wrapper.style.left = left + 'px' + wrapper.style.top = top + 'px' + htmlHolder.appendChild(wrapper) + const { width } = wrapper.getBoundingClientRect() + left += width + 30 if (left > window.innerWidth) { - top += 200; - left = left % window.innerWidth; + top += 200 + left = left % window.innerWidth } // Animation for DOMRect animateRectTransition( wrapper, { left: originLeft, top: originTop }, - { resume: true, duration: 300 }, - ).finished.finally(() => this.addDraggable(wrapper)); + { resume: true, duration: 300 } + ).finished.finally(() => this.addDraggable(wrapper)) // Trivial case: // This hack make sure utils remains at the same place even when wrapper resized // Prevent DOMRect changes when user clicking plus/minus button many times - const utils = wrapper.querySelector('.utils'); + const utils = wrapper.querySelector('.utils') utils.onmouseover = () => { - const { left, top } = utils.getBoundingClientRect(); - utils.style.cssText = `visibility: visible; z-index: 9000; position: fixed; transition: unset; left: ${left}px; top: ${top}px;`; - document.body.appendChild(utils); - }; + const { left, top } = utils.getBoundingClientRect() + utils.style.cssText = `visibility: visible; z-index: 9000; position: fixed; transition: unset; left: ${left}px; top: ${top}px;` + document.body.appendChild(utils) + } utils.onmouseout = () => { - wrapper.appendChild(utils); - utils.removeAttribute('style'); - }; + wrapper.appendChild(utils) + utils.removeAttribute('style') + } // Close button wrapper.querySelector('#close').onclick = () => { - wrapper.classList.add('hide'); - utils.removeAttribute('style'); - }; + wrapper.classList.add('hide') + utils.removeAttribute('style') + } // Plus/Minus font-size of content wrapper.querySelector('#plus-font-size').onclick = () => { - const fontSize = parseFloat(getComputedStyle(block).fontSize) / 16; - block.style.fontSize = `${fontSize + 0.2}rem`; - }; + const fontSize = parseFloat(window.getComputedStyle(block).fontSize) / 16 + block.style.fontSize = `${fontSize + 0.2}rem` + } wrapper.querySelector('#minus-font-size').onclick = () => { - const fontSize = parseFloat(getComputedStyle(block).fontSize) / 16; - block.style.fontSize = `${fontSize - 0.2}rem`; - }; - }); - }; + const fontSize = parseFloat(window.getComputedStyle(block).fontSize) / 16 + block.style.fontSize = `${fontSize - 0.2}rem` + } + }) + } leaveHandler = ({ htmlHolder, blocks }) => { const resumeFromDraggable = block => { - const draggableContainer = block.closest('.draggable-block'); - if (!draggableContainer) return; - htmlHolder.appendChild(block); - draggableContainer.remove(); - }; - blocks.forEach(resumeFromDraggable); - }; + const draggableContainer = block.closest('.draggable-block') + if (!draggableContainer) return + htmlHolder.appendChild(block) + draggableContainer.remove() + } + blocks.forEach(resumeFromDraggable) + } } 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 @@ -class Item extends HTMLDivElement { - constructor({ text, innerHTML, onclick, style }) { - super(); - this.innerHTML = innerHTML ?? text; - this.onclick = onclick; - this.classList.add('menu-item'); - this.style.cssText = style; +class Item extends window.HTMLDivElement { + constructor ({ text, innerHTML, onclick, style }) { + super() + this.innerHTML = innerHTML ?? text + this.onclick = onclick + this.classList.add('menu-item') + this.style.cssText = style this.onmouseover = () => { this.parentElement .querySelectorAll('.sub-menu') - .forEach(sub => sub.remove()); + .forEach(sub => sub.remove()) } } } -window.customElements.define('menu-item', Item, { extends: 'div' }); +window.customElements.define('menu-item', Item, { extends: 'div' }) -class Folder extends HTMLDivElement { - constructor({ text, innerHTML, items }) { - super(); - this.innerHTML = innerHTML ?? text; - this.classList.add('folder', 'menu-item'); - this.items = items; +class Folder extends window.HTMLDivElement { + constructor ({ text, innerHTML, items }) { + super() + this.innerHTML = innerHTML ?? text + this.classList.add('folder', 'menu-item') + this.items = items this.onmouseover = () => { - if (this.querySelector('.sub-menu')) return; + if (this.querySelector('.sub-menu')) return // Prepare submenu - const submenu = document.createElement('div'); - submenu.className = 'sub-menu'; - submenu.style.cssText = `position: absolute; left: 105%; top: 0px;`; - this.items.forEach(item => submenu.appendChild(item)); + const submenu = document.createElement('div') + submenu.className = 'sub-menu' + submenu.style.cssText = 'position: absolute; left: 105%; top: 0px;' + this.items.forEach(item => submenu.appendChild(item)) submenu.onmouseleave = () => submenu.remove() // hover effect this.parentElement .querySelectorAll('.sub-menu') - .forEach(sub => sub.remove()); - this.appendChild(submenu); - }; + .forEach(sub => sub.remove()) + this.appendChild(submenu) + } } } -window.customElements.define('menu-folder', Folder, { extends: 'div' }); +window.customElements.define('menu-folder', Folder, { extends: 'div' }) export const pickMapItem = ({ utils }) => new Folder({ @@ -48,12 +48,12 @@ export const pickMapItem = ({ utils }) => new Item({ text: map.id, onclick: () => { - map.classList.add('focus'); - map.scrollIntoView({ behavior: 'smooth' }); - }, - }), - ), - }); + map.classList.add('focus') + map.scrollIntoView({ behavior: 'smooth' }) + } + }) + ) + }) export const pickBlockItem = ({ blocks, utils }) => new Folder({ @@ -68,12 +68,12 @@ export const pickBlockItem = ({ blocks, utils }) => ?.textContent.substring(0, 15) .concat(' ...'), onclick: () => { - block.classList.add('focus'); - utils.scrollToBlock(block); - }, - }), - ), - }); + block.classList.add('focus') + utils.scrollToBlock(block) + } + }) + ) + }) export const pickLayoutItem = ({ container, layouts }) => new Folder({ @@ -82,78 +82,78 @@ export const pickLayoutItem = ({ container, layouts }) => new Item({ text: 'EDIT', onclick: () => - container.closest('[data-mode]').setAttribute('data-mode', 'editing'), + container.closest('[data-mode]').setAttribute('data-mode', 'editing') }), ...layouts.map( layout => new Item({ text: layout.name, - onclick: () => container.setAttribute('data-layout', layout.name), - }), + onclick: () => container.setAttribute('data-layout', layout.name) + }) ), new Item({ innerHTML: 'More...', style: 'padding: 0;' - }), - ], - }); + }) + ] + }) export const addGeoLink = ({ utils }, range) => new Item({ text: 'Add GeoLink', onclick: () => { - const content = range.toString(); + const content = range.toString() // FIXME Apply geolink only on matching sub-range - const match = content.match(/(^\D*[\d.]+)\D+([\d.]+)\D*$/); - if (!match) return false; + const match = content.match(/(^\D*[\d.]+)\D+([\d.]+)\D*$/) + if (!match) return false - const [x, y] = match.slice(1); - const anchor = document.createElement('a'); - anchor.textContent = content; + const [x, y] = match.slice(1) + const anchor = document.createElement('a') + anchor.textContent = content // FIXME apply WGS84 - anchor.href = `geo:${y},${x}?xy=${x},${y}`; + anchor.href = `geo:${y},${x}?xy=${x},${y}` // FIXME if (utils.createGeoLink(anchor)) { - range.deleteContents(); - range.insertNode(anchor); + range.deleteContents() + range.insertNode(anchor) } - }, - }); + } + }) export class Suggestion { - constructor({ text, replace }) { - this.text = text; - this.replace = replace; + constructor ({ text, replace }) { + this.text = text + this.replace = replace } - createElement(codemirror) { - const option = document.createElement('div'); + createElement (codemirror) { + const option = document.createElement('div') if (this.text.startsWith('<')) { - option.innerHTML = this.text; + option.innerHTML = this.text } else { - option.innerText = this.text; + option.innerText = this.text } - option.classList.add('container__suggestion'); + option.classList.add('container__suggestion') option.onmouseover = () => { Array.from(option.parentElement?.children)?.forEach(s => - s.classList.remove('focus'), - ); - option.classList.add('focus'); - }; + s.classList.remove('focus') + ) + option.classList.add('focus') + } option.onmouseout = () => { - option.classList.remove('focus'); - }; + option.classList.remove('focus') + } option.onclick = () => { - const anchor = codemirror.getCursor(); - codemirror.setSelection(anchor, { ...anchor, ch: 0 }); - codemirror.replaceSelection(this.replace); - codemirror.focus(); - const newAnchor = { ...anchor, ch: this.replace.length }; - codemirror.setCursor(newAnchor); - }; + const anchor = codemirror.getCursor() + codemirror.setSelection(anchor, { ...anchor, ch: 0 }) + codemirror.replaceSelection(this.replace) + codemirror.focus() + const newAnchor = { ...anchor, ch: this.replace.length } + codemirror.setCursor(newAnchor) + } - return option; + return option } } @@ -161,100 +161,100 @@ export const renderResults = ({ modal, modalContent }, map) => new Item({ text: 'Render Results', onclick: () => { - modal.open(); - modal.overlayBlur = 3; - modal.closeByEscKey = false; + modal.open() + modal.overlayBlur = 3 + modal.closeByEscKey = false // HACK find another way to override inline style document.querySelector('.plainmodal-overlay-force').style.position = - 'relative'; + 'relative' - modalContent.innerHTML = ''; + modalContent.innerHTML = '' const sourceCode = document.createElement('div') sourceCode.innerHTML = `Source Code` modalContent.appendChild(sourceCode) const printDetails = result => { - const funcBody = result.func.toString(); - const loc = funcBody.split('\n').length; + const funcBody = result.func.toString() + const loc = funcBody.split('\n').length const color = { success: 'green', fail: 'red', skip: 'black', - stop: 'yellow', - }[result.state] ?? 'black'; + stop: 'yellow' + }[result.state] ?? 'black' printObject( result, modalContent, - `${result.func.name} ${loc}LOC\x20\x20\x20${result.state}`, - ); - }; + `${result.func.name} ${loc}LOC\x20\x20\x20${result.state}` + ) + } // Add contents about prepare steps - const prepareHeading = document.createElement('h3'); - prepareHeading.textContent = 'Prepare Steps'; - modalContent.appendChild(prepareHeading); + const prepareHeading = document.createElement('h3') + prepareHeading.textContent = 'Prepare Steps' + modalContent.appendChild(prepareHeading) const prepareSteps = map.renderer.results.filter( - r => r.type === 'prepare', - ); - prepareSteps.forEach(printDetails); + r => r.type === 'prepare' + ) + prepareSteps.forEach(printDetails) // Add contents about render steps - const renderHeading = document.createElement('h3'); - renderHeading.textContent = 'Render Steps'; - modalContent.appendChild(renderHeading); - const renderSteps = map.renderer.results.filter(r => r.type === 'render'); - renderSteps.forEach(printDetails); - }, - }); + const renderHeading = document.createElement('h3') + renderHeading.textContent = 'Render Steps' + modalContent.appendChild(renderHeading) + const renderSteps = map.renderer.results.filter(r => r.type === 'render') + renderSteps.forEach(printDetails) + } + }) -function printObject(obj, parentElement, name = null) { +function printObject (obj, parentElement, name = null) { // Create
and inside - const detailsEle = document.createElement('details'); - const details = name ?? (obj instanceof Error ? obj.name : Object.values(obj)[0]); - detailsEle.innerHTML = `${details}`; - parentElement.appendChild(detailsEle); + const detailsEle = document.createElement('details') + const details = name ?? (obj instanceof Error ? obj.name : Object.values(obj)[0]) + detailsEle.innerHTML = `${details}` + parentElement.appendChild(detailsEle) detailsEle.onclick = () => { // Don't add items if it has contents - if (detailsEle.querySelector(':scope > :not(summary)')) return; + if (detailsEle.querySelector(':scope > :not(summary)')) return if (obj instanceof Error) { // Handle Error objects specially - const errorProps = ['name', 'message', 'stack', ...Object.keys(obj)]; + const errorProps = ['name', 'message', 'stack', ...Object.keys(obj)] errorProps.forEach(key => { - const value = obj[key]; - const valueString = key === 'stack' ? `
${value}
` : value; - const propertyElement = document.createElement('p'); - propertyElement.innerHTML = `${key}: ${valueString}`; - detailsEle.appendChild(propertyElement); - }); + const value = obj[key] + const valueString = key === 'stack' ? `
${value}
` : value + const propertyElement = document.createElement('p') + propertyElement.innerHTML = `${key}: ${valueString}` + detailsEle.appendChild(propertyElement) + }) } else { // Handle regular objects Object.entries(obj).forEach(([key, value]) => { if (typeof value === 'object' && value !== null) { - printObject(value, detailsEle, key); + printObject(value, detailsEle, key) } else { const valueString = typeof value === 'function' ? `
${value}
` - : value ?? typeof value; - const propertyElement = document.createElement('p'); - propertyElement.innerHTML = `${key}: ${valueString}`; - detailsEle.appendChild(propertyElement); + : value ?? typeof value + const propertyElement = document.createElement('p') + propertyElement.innerHTML = `${key}: ${valueString}` + detailsEle.appendChild(propertyElement) } - }); + }) } - }; + } } export const toggleBlockFocus = block => new Item({ text: 'Toggle Focus', - onclick: () => block.classList.toggle('focus'), - }); + onclick: () => block.classList.toggle('focus') + }) export const toggleMapFocus = map => new Item({ text: 'Toggle Focus', - onclick: () => map.classList.toggle('focus'), - }); + onclick: () => map.classList.toggle('focus') + }) 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 @@ -import LeaderLine from 'leader-line'; +import LeaderLine from 'leader-line' -export function focusNextMap(reverse = false) { - const renderedList = this.utils.renderedMaps(); - const index = renderedList.findIndex(e => e.classList.contains('focus')); +export function focusNextMap (reverse = false) { + const renderedList = this.utils.renderedMaps() + const index = renderedList.findIndex(e => e.classList.contains('focus')) const nextIndex = - index === -1 ? 0 : (index + (reverse ? -1 : 1)) % renderedList.length; + index === -1 ? 0 : (index + (reverse ? -1 : 1)) % renderedList.length - const nextMap = renderedList.at(nextIndex); - nextMap.classList.add('focus'); + const nextMap = renderedList.at(nextIndex) + nextMap.classList.add('focus') } -export function focusNextBlock(reverse = false) { +export function focusNextBlock (reverse = false) { const blocks = this.blocks.filter(b => b.checkVisibility({ contentVisibilityAuto: true, opacityProperty: true, - visibilityProperty: true, - }), - ); - const index = blocks.findIndex(e => e.classList.contains('focus')); + visibilityProperty: true + }) + ) + const index = blocks.findIndex(e => e.classList.contains('focus')) const nextIndex = - index === -1 ? 0 : (index + (reverse ? -1 : 1)) % blocks.length; + index === -1 ? 0 : (index + (reverse ? -1 : 1)) % blocks.length - blocks.forEach(b => b.classList.remove('focus')); - const nextBlock = blocks.at(nextIndex); - nextBlock?.classList?.add('focus'); - scrollToBlock(nextBlock); + blocks.forEach(b => b.classList.remove('focus')) + const nextBlock = blocks.at(nextIndex) + nextBlock?.classList?.add('focus') + scrollToBlock(nextBlock) } // Consider block is bigger then viewport height export const scrollToBlock = block => { - const parentRect = block.parentElement.getBoundingClientRect(); + const parentRect = block.parentElement.getBoundingClientRect() const scrollBlock = block.getBoundingClientRect().height > parentRect.height * 0.8 ? 'nearest' - : 'center'; - block.scrollIntoView({ behavior: 'smooth', block: scrollBlock }); -}; + : 'center' + block.scrollIntoView({ behavior: 'smooth', block: scrollBlock }) +} -export function focusDelay() { - return window.getComputedStyle(this.showcase).display === 'none' ? 50 : 300; +export function focusDelay () { + return window.window.getComputedStyle(this.showcase).display === 'none' ? 50 : 300 } -export function switchToNextLayout(reverse = false) { - const layouts = this.layouts; - const currentLayoutName = this.container.getAttribute('data-layout'); - const currentIndex = layouts.map(l => l.name).indexOf(currentLayoutName); - const padding = reverse ? -1 : 1; +export function switchToNextLayout (reverse = false) { + const layouts = this.layouts + const currentLayoutName = this.container.getAttribute('data-layout') + const currentIndex = layouts.map(l => l.name).indexOf(currentLayoutName) + const padding = reverse ? -1 : 1 const nextIndex = currentIndex === -1 ? 0 - : (currentIndex + padding + layouts.length) % layouts.length; - const nextLayout = layouts[nextIndex]; - this.container.setAttribute('data-layout', nextLayout.name); + : (currentIndex + padding + layouts.length) % layouts.length + const nextLayout = layouts[nextIndex] + this.container.setAttribute('data-layout', nextLayout.name) } -export function removeBlockFocus() { - this.blocks.forEach(b => b.classList.remove('focus')); +export function removeBlockFocus () { + this.blocks.forEach(b => b.classList.remove('focus')) } /** @@ -66,31 +66,31 @@ export function removeBlockFocus() { * @returns {Boolean} ture is link is created, false if coordinates are invalid */ export const createGeoLink = (link, callback = null) => { - const url = new URL(link.href); - const xyInParams = url.searchParams.get('xy'); + const url = new URL(link.href) + const xyInParams = url.searchParams.get('xy') const xy = xyInParams ? xyInParams.split(',')?.map(Number) : url?.href - ?.match(/^geo:([0-9.,]+)/) - ?.at(1) - ?.split(',') - ?.reverse() - ?.map(Number); + ?.match(/^geo:([0-9.,]+)/) + ?.at(1) + ?.split(',') + ?.reverse() + ?.map(Number) - if (!xy || isNaN(xy[0]) || isNaN(xy[1])) return false; + if (!xy || isNaN(xy[0]) || isNaN(xy[1])) return false // Geo information in link - link.url = url; - link.xy = xy; - link.classList.add('with-leader-line', 'geolink'); - link.targets = link.url.searchParams.get('id')?.split(',') ?? null; + link.url = url + link.xy = xy + link.classList.add('with-leader-line', 'geolink') + link.targets = link.url.searchParams.get('id')?.split(',') ?? null // LeaderLine - link.lines = []; - callback?.call(this, link); + link.lines = [] + callback?.call(this, link) - return true; -}; + return true +} /** * CreateDocLink. @@ -98,30 +98,30 @@ export const createGeoLink = (link, callback = null) => { * @param {HTMLElement} Elements contains anchor elements for doclinks */ export const createDocLink = link => { - link.classList.add('with-leader-line', 'doclink'); - link.lines = []; + link.classList.add('with-leader-line', 'doclink') + link.lines = [] link.onmouseover = () => { - const label = decodeURIComponent(link.href.split('#')[1]); - const selector = link.title.split('=>')[1] ?? '#' + label; - const target = document.querySelector(selector); - if (!target?.checkVisibility()) return; + const label = decodeURIComponent(link.href.split('#')[1]) + const selector = link.title.split('=>')[1] ?? '#' + label + const target = document.querySelector(selector) + if (!target?.checkVisibility()) return const line = new LeaderLine({ start: link, end: target, middleLabel: LeaderLine.pathLabel({ text: label, - fontWeight: 'bold', + fontWeight: 'bold' }), hide: true, - path: 'magnet', - }); - link.lines.push(line); - line.show('draw', { duration: 300 }); - }; + path: 'magnet' + }) + link.lines.push(line) + line.show('draw', { duration: 300 }) + } link.onmouseout = () => { - link.lines.forEach(line => line.remove()); - link.lines.length = 0; - }; -}; + link.lines.forEach(line => line.remove()) + link.lines.length = 0 + } +} 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) +} 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 @@ -/*global EasyMDE*/ -/*eslint no-undef: "error"*/ -import { markdown2HTML, generateMaps } from './dumbymap'; -import { defaultAliases, parseConfigsFromYaml } from 'mapclay'; -import * as menuItem from './MenuItem'; +/* global EasyMDE */ +/* eslint no-undef: "error" */ +import { markdown2HTML, generateMaps } from './dumbymap' +import { defaultAliases, parseConfigsFromYaml } from 'mapclay' +import * as menuItem from './MenuItem' // Set up Containers {{{ -const HtmlContainer = document.querySelector('.DumbyMap'); -const textArea = document.querySelector('.editor textarea'); -let dumbymap; +const HtmlContainer = document.querySelector('.DumbyMap') +const textArea = document.querySelector('.editor textarea') +let dumbymap -new MutationObserver(mutations => { - const mutation = mutations.at(-1); - const mode = mutation.target.getAttribute('data-mode'); - const layout = HtmlContainer.getAttribute('data-layout'); +new window.MutationObserver(mutations => { + const mutation = mutations.at(-1) + const mode = mutation.target.getAttribute('data-mode') + const layout = HtmlContainer.getAttribute('data-layout') if (mode === 'editing' && layout !== 'normal') { - HtmlContainer.setAttribute('data-layout', 'normal'); + HtmlContainer.setAttribute('data-layout', 'normal') } }).observe(document.querySelector('[data-mode]'), { attributes: true, attributeFilter: ['data-mode'], - attributeOldValue: true, -}); + attributeOldValue: true +}) const toggleEditing = () => { - const mode = document.body.getAttribute('data-mode'); - document.body.setAttribute('data-mode', mode === 'editing' ? '' : 'editing'); -}; + const mode = document.body.getAttribute('data-mode') + document.body.setAttribute('data-mode', mode === 'editing' ? '' : 'editing') +} // }}} // Set up EasyMDE {{{ // Content values for editor const defaultContent = - '## 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'; + '## 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' const editor = new EasyMDE({ element: textArea, initialValue: defaultContent, autosave: { enabled: true, - uniqueId: 'dumbymap', + uniqueId: 'dumbymap' }, indentWithTabs: false, lineNumbers: true, @@ -51,105 +51,105 @@ const editor = new EasyMDE({ map: 'Ctrl-Alt-M', debug: 'Ctrl-Alt-D', toggleUnorderedList: null, - toggleOrderedList: null, + toggleOrderedList: null }, toolbar: [ { name: 'map', title: 'Toggle Map Generation', text: '🌏', - action: () => toggleEditing(), + action: () => toggleEditing() }, { name: 'debug', title: 'Save content as URL', text: '🤔', action: () => { - const state = { content: editor.value() }; - window.location.hash = encodeURIComponent(JSON.stringify(state)); - navigator.clipboard.writeText(window.location.href); - alert('URL copied to clipboard'); - }, + const state = { content: editor.value() } + window.location.hash = encodeURIComponent(JSON.stringify(state)) + navigator.clipboard.writeText(window.location.href) + window.alert('URL copied to clipboard') + } }, { name: 'undo', title: 'Undo last editing', text: '\u27F2', - action: EasyMDE.undo, + action: EasyMDE.undo }, { name: 'redo', text: '\u27F3', title: 'Redo editing', - action: EasyMDE.redo, + action: EasyMDE.redo }, '|', { name: 'heading-1', text: 'H1', title: 'Big Heading', - action: EasyMDE['heading-1'], + action: EasyMDE['heading-1'] }, { name: 'heading-2', text: 'H2', title: 'Medium Heading', - action: EasyMDE['heading-2'], + action: EasyMDE['heading-2'] }, '|', { name: 'link', text: '\u{1F517}', title: 'Create Link', - action: EasyMDE.drawLink, + action: EasyMDE.drawLink }, { name: 'image', text: '\u{1F5BC}', title: 'Create Image', - action: EasyMDE.drawImage, + action: EasyMDE.drawImage }, '|', { name: 'Bold', text: '\u{1D401}', title: 'Bold', - action: EasyMDE.toggleBold, + action: EasyMDE.toggleBold }, { name: 'Italic', text: '\u{1D43C}', title: 'Italic', - action: EasyMDE.toggleItalic, - }, - ], -}); + action: EasyMDE.toggleItalic + } + ] +}) -const cm = editor.codemirror; +const cm = editor.codemirror const getStateFromHash = hash => { - const hashValue = hash.substring(1); - const stateString = decodeURIComponent(hashValue); + const hashValue = hash.substring(1) + const stateString = decodeURIComponent(hashValue) try { - return JSON.parse(stateString) ?? {}; + return JSON.parse(stateString) ?? {} } catch (_) { - return {}; + return {} } -}; +} const getContentFromHash = hash => { - const state = getStateFromHash(hash); - return state.content; -}; + const state = getStateFromHash(hash) + return state.content +} -const initialState = getStateFromHash(window.location.hash); -window.location.hash = ''; -const contentFromHash = initialState.content; +const initialState = getStateFromHash(window.location.hash) +window.location.hash = '' +const contentFromHash = initialState.content // Seems like autosave would overwrite initialValue, set content from hash here if (contentFromHash) { - editor.cleanup(); - editor.value(contentFromHash); + editor.cleanup() + editor.value(contentFromHash) } // }}} // Set up logic about editor content {{{ @@ -159,30 +159,30 @@ const afterMapRendered = _ => { // const lonLat = mapHolder.renderer.unproject([event.x, event.y]) // // TODO... // } -}; -markdown2HTML(HtmlContainer, editor.value()); -dumbymap = generateMaps(HtmlContainer, afterMapRendered); +} +markdown2HTML(HtmlContainer, editor.value()) +dumbymap = generateMaps(HtmlContainer, afterMapRendered) // Quick hack to style lines inside code block const addClassToCodeLines = () => { - const lines = cm.getLineHandle(0).parent.lines; - let insideCodeBlock = false; + const lines = cm.getLineHandle(0).parent.lines + let insideCodeBlock = false lines.forEach((line, index) => { if (line.text.match(/^[\u0060]{3}/)) { - insideCodeBlock = !insideCodeBlock; + insideCodeBlock = !insideCodeBlock } else if (insideCodeBlock) { - cm.addLineClass(index, 'text', 'inside-code-block'); + cm.addLineClass(index, 'text', 'inside-code-block') } else { - cm.removeLineClass(index, 'text', 'inside-code-block'); + cm.removeLineClass(index, 'text', 'inside-code-block') } - }); -}; -addClassToCodeLines(); + }) +} +addClassToCodeLines() const completeForCodeBlock = change => { - const line = change.to.line; + const line = change.to.line if (change.origin === '+input') { - const text = change.text[0]; + const text = change.text[0] // Completion for YAML doc separator if ( @@ -190,21 +190,21 @@ const completeForCodeBlock = change => { change.to.ch === 0 && insideCodeblockForMap(cm.getCursor()) ) { - cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 1 }); - cm.replaceSelection(text.repeat(3) + '\n'); + cm.setSelection({ line, ch: 0 }, { line, ch: 1 }) + cm.replaceSelection(text.repeat(3) + '\n') } // Completion for Code fence if (text === '`' && change.to.ch === 0) { - cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 1 }); - cm.replaceSelection(text.repeat(3)); + cm.setSelection({ line, ch: 0 }, { line, ch: 1 }) + cm.replaceSelection(text.repeat(3)) const numberOfFences = cm .getValue() .split('\n') - .filter(line => line.match(/[\u0060]{3}/)).length; + .filter(line => line.match(/[\u0060]{3}/)).length if (numberOfFences % 2 === 1) { - cm.replaceSelection('map\n\n```'); - cm.setCursor({ line: line + 1 }); + cm.replaceSelection('map\n\n```') + cm.setCursor({ line: line + 1 }) } } } @@ -212,63 +212,64 @@ const completeForCodeBlock = change => { // For YAML doc separator,
    and code fence // Auto delete to start of line if (change.origin === '+delete') { - const match = change.removed[0].match(/^[-\u0060]$/)?.at(0); + const match = change.removed[0].match(/^[-\u0060]$/)?.at(0) if (match && cm.getLine(line) === match.repeat(2) && match) { - cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 2 }); - cm.replaceSelection(''); + cm.setSelection({ line, ch: 0 }, { line, ch: 2 }) + cm.replaceSelection('') } } -}; - -const debounceForMap = (() => { - let timer = null; +} - return function (...args) { - dumbymap = generateMaps.apply(this, args); - // clearTimeout(timer); - // timer = setTimeout(() => { - // dumbymap = generateMaps.apply(this, args) - // }, 10); - }; -})(); +/* Disable debounce temporarily */ +// const debounceForMap = (() => { +// const timer = null +// +// return function (...args) { +// dumbymap = generateMaps.apply(this, args) +// clearTimeout(timer); +// timer = setTimeout(() => { +// dumbymap = generateMaps.apply(this, args) +// }, 10); +// } +// })() const updateDumbyMap = () => { - markdown2HTML(HtmlContainer, editor.value()); + markdown2HTML(HtmlContainer, editor.value()) // TODO Test if generate maps intantly is OK with map cache // debounceForMap(HtmlContainer, afterMapRendered) - dumbymap = generateMaps(HtmlContainer, afterMapRendered); -}; + dumbymap = generateMaps(HtmlContainer, afterMapRendered) +} -updateDumbyMap(); +updateDumbyMap() // Re-render HTML by editor content cm.on('change', (_, change) => { - updateDumbyMap(); - addClassToCodeLines(); - completeForCodeBlock(change); -}); + updateDumbyMap() + addClassToCodeLines() + completeForCodeBlock(change) +}) // Set class for focus cm.on('focus', () => { - cm.getWrapperElement().classList.add('focus'); - HtmlContainer.classList.remove('focus'); -}); + cm.getWrapperElement().classList.add('focus') + HtmlContainer.classList.remove('focus') +}) cm.on('beforeChange', (_, change) => { - const line = change.to.line; + const line = change.to.line // Don't allow more content after YAML doc separator if (change.origin.match(/^(\+input|paste)$/)) { if (cm.getLine(line) === '---' && change.text[0] !== '') { - change.cancel(); + change.cancel() } } -}); +}) // Reload editor content by hash value window.onhashchange = () => { - const content = getContentFromHash(window.location.hash); - if (content) editor.value(content); -}; + const content = getContentFromHash(window.location.hash) + if (content) editor.value(content) +} // FIXME DEBUGONLY // generateMaps(HtmlContainer) @@ -279,159 +280,159 @@ window.onhashchange = () => { // }}} // Completion in Code Blok {{{ // Elements about suggestions {{{ -const menu = document.createElement('div'); -menu.className = 'menu editor-menu'; -menu.style.display = 'none'; -menu.onclick = () => (menu.style.display = 'none'); -new MutationObserver(() => { +const menu = document.createElement('div') +menu.className = 'menu editor-menu' +menu.style.display = 'none' +menu.onclick = () => (menu.style.display = 'none') +new window.MutationObserver(() => { if (menu.style.display === 'none') { - menu.replaceChildren(); + menu.replaceChildren() } }).observe(menu, { attributes: true, - attributeFilter: ['style'], -}); -document.body.append(menu); + attributeFilter: ['style'] +}) +document.body.append(menu) -const rendererOptions = {}; +const rendererOptions = {} // }}} // Aliases for map options {{{ -const aliasesForMapOptions = {}; -const defaultApply = './dist/default.yml'; +const aliasesForMapOptions = {} +const defaultApply = './dist/default.yml' fetch(defaultApply) .then(res => res.text()) .then(rawText => { - const config = parseConfigsFromYaml(rawText)?.at(0); - Object.assign(aliasesForMapOptions, config.aliases ?? {}); + const config = parseConfigsFromYaml(rawText)?.at(0) + Object.assign(aliasesForMapOptions, config.aliases ?? {}) }) - .catch(err => console.warn(`Fail to get aliases from ${defaultApply}`, err)); + .catch(err => console.warn(`Fail to get aliases from ${defaultApply}`, err)) // }}} // FUNCTION: Check if current token is inside code block {{{ const insideCodeblockForMap = anchor => { - const token = cm.getTokenAt(anchor); + const token = cm.getTokenAt(anchor) const insideCodeBlock = token.state.overlay.codeBlock && - !cm.getLine(anchor.line).match(/^[\u0060]{3}/); - if (!insideCodeBlock) return false; + !cm.getLine(anchor.line).match(/^[\u0060]{3}/) + if (!insideCodeBlock) return false - let line = anchor.line - 1; + let line = anchor.line - 1 while (line >= 0) { - const content = cm.getLine(line); + const content = cm.getLine(line) if (content === '```map') { - return true; + return true } else if (content === '```') { - return false; + return false } - line = line - 1; + line = line - 1 } - return false; -}; + return false +} // }}} // FUNCTION: Get Renderer by cursor position in code block {{{ const getLineWithRenderer = anchor => { - const currentLine = anchor.line; - if (!cm.getLine) return null; + const currentLine = anchor.line + if (!cm.getLine) return null - const match = line => cm.getLine(line).match(/^use: /); + const match = line => cm.getLine(line).match(/^use: /) - if (match(currentLine)) return currentLine; + if (match(currentLine)) return currentLine // Look backward/forward for pattern of used renderer: /use: .+/ - let pl = currentLine - 1; + let pl = currentLine - 1 while (pl > 0 && insideCodeblockForMap(anchor)) { - const text = cm.getLine(pl); + const text = cm.getLine(pl) if (match(pl)) { - return pl; + return pl } else if (text.match(/^---|^[\u0060]{3}/)) { - break; + break } - pl = pl - 1; + pl = pl - 1 } - let nl = currentLine + 1; + let nl = currentLine + 1 while (insideCodeblockForMap(anchor)) { - const text = cm.getLine(nl); + const text = cm.getLine(nl) if (match(nl)) { - return nl; + return nl } else if (text.match(/^---|^[\u0060]{3}/)) { - return null; + return null } - nl = nl + 1; + nl = nl + 1 } - return null; -}; + return null +} // }}} // FUNCTION: Return suggestions for valid options {{{ const getSuggestionsForOptions = (optionTyped, validOptions) => { - let suggestOptions = []; + let suggestOptions = [] const matchedOptions = validOptions.filter(o => - o.valueOf().toLowerCase().includes(optionTyped.toLowerCase()), - ); + o.valueOf().toLowerCase().includes(optionTyped.toLowerCase()) + ) if (matchedOptions.length > 0) { - suggestOptions = matchedOptions; + suggestOptions = matchedOptions } else { - suggestOptions = validOptions; + suggestOptions = validOptions } return suggestOptions.map( o => new menuItem.Suggestion({ text: `${o.valueOf()}`, - replace: `${o.valueOf()}: `, - }), - ); -}; + replace: `${o.valueOf()}: ` + }) + ) +} // }}} // FUNCTION: Return suggestion for example of option value {{{ const getSuggestionFromMapOption = option => { - if (!option.example) return null; + if (!option.example) return null const text = option.example_desc ? `${option.example_desc}${option.example}` - : `${option.example}`; + : `${option.example}` return new menuItem.Suggestion({ - text: text, - replace: `${option.valueOf()}: ${option.example ?? ''}`, - }); -}; + text, + replace: `${option.valueOf()}: ${option.example ?? ''}` + }) +} // }}} // FUNCTION: Return suggestions from aliases {{{ const getSuggestionsFromAliases = option => Object.entries(aliasesForMapOptions[option.valueOf()] ?? {})?.map(record => { - const [alias, value] = record; - const valueString = JSON.stringify(value).replaceAll('"', ''); + const [alias, value] = record + const valueString = JSON.stringify(value).replaceAll('"', '') return new menuItem.Suggestion({ text: `${alias}${valueString}`, - replace: `${option.valueOf()}: ${valueString}`, - }); - }) ?? []; + replace: `${option.valueOf()}: ${valueString}` + }) + }) ?? [] // }}} // FUCNTION: Handler for map codeblock {{{ const handleTypingInCodeBlock = anchor => { - const text = cm.getLine(anchor.line); + const text = cm.getLine(anchor.line) if (text.match(/^\s\+$/) && text.length % 2 !== 0) { // TODO Completion for even number of spaces } else if (text.match(/^-/)) { // TODO Completion for YAML doc separator } else { - const suggestions = getSuggestions(anchor); - addSuggestions(anchor, suggestions); + const suggestions = getSuggestions(anchor) + addSuggestions(anchor, suggestions) } -}; +} // }}} // FUNCTION: get suggestions by current input {{{ const getSuggestions = anchor => { - const text = cm.getLine(anchor.line); + const text = cm.getLine(anchor.line) // Clear marks on text cm.findMarks({ ...anchor, ch: 0 }, { ...anchor, ch: text.length }).forEach( - m => m.clear(), - ); + m => m.clear() + ) // Mark user input invalid by case const markInputIsInvalid = () => @@ -440,231 +441,230 @@ const getSuggestions = anchor => { .markText( { ...anchor, ch: 0 }, { ...anchor, ch: text.length }, - { className: 'invalid-input' }, - ); + { className: 'invalid-input' } + ) // Check if "use: " is set - const lineWithRenderer = getLineWithRenderer(anchor); + const lineWithRenderer = getLineWithRenderer(anchor) const renderer = lineWithRenderer ? cm.getLine(lineWithRenderer).split(' ')[1] - : null; + : null if (renderer && anchor.line !== lineWithRenderer) { // Do not check properties - if (text.startsWith(' ')) return []; + if (text.startsWith(' ')) return [] // If no valid options for current used renderer, go get it! - const validOptions = rendererOptions[renderer]; + const validOptions = rendererOptions[renderer] if (!validOptions) { // Get list of valid options for current renderer - const rendererUrl = defaultAliases.use[renderer]?.value; + const rendererUrl = defaultAliases.use[renderer]?.value import(rendererUrl) .then(rendererModule => { - rendererOptions[renderer] = rendererModule.default.validOptions; - const currentAnchor = cm.getCursor(); + rendererOptions[renderer] = rendererModule.default.validOptions + const currentAnchor = cm.getCursor() if (insideCodeblockForMap(currentAnchor)) { - handleTypingInCodeBlock(currentAnchor); + handleTypingInCodeBlock(currentAnchor) } }) .catch(_ => { - markInputIsInvalid(lineWithRenderer); + markInputIsInvalid(lineWithRenderer) console.warn( - `Fail to get valid options from Renderer typed: ${renderer}`, - ); - }); - return []; + `Fail to get valid options from Renderer typed: ${renderer}` + ) + }) + return [] } // If input is "key:value" (no space left after colon), then it is invalid - const isKeyFinished = text.includes(':'); - const isValidKeyValue = text.match(/^[^:]+:\s+/); + const isKeyFinished = text.includes(':') + const isValidKeyValue = text.match(/^[^:]+:\s+/) if (isKeyFinished && !isValidKeyValue) { - markInputIsInvalid(); - return []; + markInputIsInvalid() + return [] } // If user is typing option - const keyTyped = text.split(':')[0].trim(); + const keyTyped = text.split(':')[0].trim() if (!isKeyFinished) { - markInputIsInvalid(); - return getSuggestionsForOptions(keyTyped, validOptions); + markInputIsInvalid() + return getSuggestionsForOptions(keyTyped, validOptions) } // If user is typing value - const matchedOption = validOptions.find(o => o.name === keyTyped); + const matchedOption = validOptions.find(o => o.name === keyTyped) if (isKeyFinished && !matchedOption) { - markInputIsInvalid(); + markInputIsInvalid() } if (isKeyFinished && matchedOption) { - const valueTyped = text.substring(text.indexOf(':') + 1).trim(); - const isValidValue = matchedOption.isValid(valueTyped); + const valueTyped = text.substring(text.indexOf(':') + 1).trim() + const isValidValue = matchedOption.isValid(valueTyped) if (!valueTyped) { return [ getSuggestionFromMapOption(matchedOption), - ...getSuggestionsFromAliases(matchedOption), - ].filter(s => s instanceof menuItem.Suggestion); + ...getSuggestionsFromAliases(matchedOption) + ].filter(s => s instanceof menuItem.Suggestion) } if (valueTyped && !isValidValue) { - markInputIsInvalid(); - return []; + markInputIsInvalid() + return [] } } } else { // Suggestion for "use" const rendererSuggestions = Object.entries(defaultAliases.use) .filter(([renderer]) => { - const suggestion = `use: ${renderer}`; - const suggestionPattern = suggestion.replace(' ', '').toLowerCase(); - const textPattern = text.replace(' ', '').toLowerCase(); - return suggestion !== text && suggestionPattern.includes(textPattern); + const suggestion = `use: ${renderer}` + const suggestionPattern = suggestion.replace(' ', '').toLowerCase() + const textPattern = text.replace(' ', '').toLowerCase() + return suggestion !== text && suggestionPattern.includes(textPattern) }) .map( ([renderer, info]) => new menuItem.Suggestion({ text: `use: ${renderer}`, - replace: `use: ${renderer}`, - }), - ); - return rendererSuggestions.length > 0 ? rendererSuggestions : []; + replace: `use: ${renderer}` + }) + ) + return rendererSuggestions.length > 0 ? rendererSuggestions : [] } - return []; -}; + return [] +} // }}} // {{{ FUNCTION: Show element about suggestions const addSuggestions = (anchor, suggestions) => { if (suggestions.length === 0) { - menu.style.display = 'none'; - return; + menu.style.display = 'none' + return } else { - menu.style.display = 'block'; + menu.style.display = 'block' } - menu.innerHTML = ''; + menu.innerHTML = '' suggestions .map(s => s.createElement(cm)) - .forEach(option => menu.appendChild(option)); - - const widgetAnchor = document.createElement('div'); - cm.addWidget(anchor, widgetAnchor, true); - const rect = widgetAnchor.getBoundingClientRect(); - menu.style.left = `calc(${rect.left}px + 2rem)`; - menu.style.top = `calc(${rect.bottom}px + 1rem)`; - menu.style.maxWidth = `calc(${window.innerWidth}px - ${rect.x}px - 3rem)`; - menu.style.display = 'block'; -}; + .forEach(option => menu.appendChild(option)) + + const widgetAnchor = document.createElement('div') + cm.addWidget(anchor, widgetAnchor, true) + const rect = widgetAnchor.getBoundingClientRect() + menu.style.left = `calc(${rect.left}px + 2rem)` + menu.style.top = `calc(${rect.bottom}px + 1rem)` + menu.style.maxWidth = `calc(${window.innerWidth}px - ${rect.x}px - 3rem)` + menu.style.display = 'block' +} // }}} // EVENT: Suggests for current selection {{{ // FIXME Dont show suggestion when selecting multiple chars cm.on('cursorActivity', _ => { - menu.style.display = 'none'; - const anchor = cm.getCursor(); + menu.style.display = 'none' + const anchor = cm.getCursor() if (insideCodeblockForMap(anchor)) { - handleTypingInCodeBlock(anchor); + handleTypingInCodeBlock(anchor) } -}); +}) cm.on('blur', () => { if (menu.checkVisibility()) { cm.focus() } else { - cm.getWrapperElement().classList.remove('focus'); - HtmlContainer.classList.add('focus'); + cm.getWrapperElement().classList.remove('focus') + HtmlContainer.classList.add('focus') } -}); +}) // }}} // EVENT: keydown for suggestions {{{ -const keyForSuggestions = ['Tab', 'Enter', 'Escape']; +const keyForSuggestions = ['Tab', 'Enter', 'Escape'] cm.on('keydown', (_, e) => { if ( !cm.hasFocus || !keyForSuggestions.includes(e.key) || menu.style.display === 'none' - ) - return; + ) { return } // Directly add a newline when no suggestion is selected - const currentSuggestion = menu.querySelector('.container__suggestion.focus'); - if (!currentSuggestion && e.key === 'Enter') return; + const currentSuggestion = menu.querySelector('.container__suggestion.focus') + if (!currentSuggestion && e.key === 'Enter') return // Override default behavior - e.preventDefault(); + e.preventDefault() // Suggestion when pressing Tab or Shift + Tab const nextSuggestion = currentSuggestion?.nextSibling ?? - menu.querySelector('.container__suggestion:first-child'); + menu.querySelector('.container__suggestion:first-child') const previousSuggestion = currentSuggestion?.previousSibling ?? - menu.querySelector('.container__suggestion:last-child'); - const focusSuggestion = e.shiftKey ? previousSuggestion : nextSuggestion; + menu.querySelector('.container__suggestion:last-child') + const focusSuggestion = e.shiftKey ? previousSuggestion : nextSuggestion // Current editor selection state switch (e.key) { case 'Tab': - Array.from(menu.children).forEach(s => s.classList.remove('focus')); - focusSuggestion.classList.add('focus'); - focusSuggestion.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); - break; + Array.from(menu.children).forEach(s => s.classList.remove('focus')) + focusSuggestion.classList.add('focus') + focusSuggestion.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) + break case 'Enter': - currentSuggestion.onclick(); - break; + currentSuggestion.onclick() + break case 'Escape': - if (!menu.checkVisibility()) break; + if (!menu.checkVisibility()) break // HACK delay menu display change for blur event, mark cm focus should keep - setTimeout(() => (menu.style.display = 'none'), 50); - break; + setTimeout(() => (menu.style.display = 'none'), 50) + break } -}); +}) document.onkeydown = e => { if (e.altKey && e.ctrlKey && e.key === 'm') { - toggleEditing(); - e.preventDefault(); - return null; + toggleEditing() + e.preventDefault() + return null } if (!cm.hasFocus()) { if (e.key === 'F1') { - e.preventDefault(); - cm.focus(); + e.preventDefault() + cm.focus() } if (e.key === 'Tab') { - e.preventDefault(); - dumbymap.utils.focusNextMap(e.shiftKey); + e.preventDefault() + dumbymap.utils.focusNextMap(e.shiftKey) } if (e.key === 'x' || e.key === 'X') { - e.preventDefault(); - dumbymap.utils.switchToNextLayout(e.shiftKey); + e.preventDefault() + dumbymap.utils.switchToNextLayout(e.shiftKey) } if (e.key === 'n') { - e.preventDefault(); - dumbymap.utils.focusNextBlock(); + e.preventDefault() + dumbymap.utils.focusNextBlock() } if (e.key === 'p') { - e.preventDefault(); - dumbymap.utils.focusNextBlock(true); + e.preventDefault() + dumbymap.utils.focusNextBlock(true) } if (e.key === 'Escape') { - e.preventDefault(); - dumbymap.utils.removeBlockFocus(); + e.preventDefault() + dumbymap.utils.removeBlockFocus() } } -}; +} // }}} // }}} // Layout Switch {{{ -new MutationObserver(mutaions => { - const mutation = mutaions.at(-1); - const layout = HtmlContainer.getAttribute('data-layout'); +new window.MutationObserver(mutaions => { + const mutation = mutaions.at(-1) + const layout = HtmlContainer.getAttribute('data-layout') if (layout !== 'normal' || mutation.oldValue === 'normal') { - document.body.setAttribute('data-mode', ''); + document.body.setAttribute('data-mode', '') } }).observe(HtmlContainer, { attributes: true, attributeFilter: ['data-layout'], - attributeOldValue: true, -}); + attributeOldValue: true +}) // }}} // 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 @@ * @param {Function} callback */ export const onRemove = (element, callback) => { - const parent = element.parentNode; - if (!parent) throw new Error('The node must already be attached'); + const parent = element.parentNode + if (!parent) throw new Error('The node must already be attached') - const obs = new MutationObserver(mutations => { + const obs = new window.MutationObserver(mutations => { for (const mutation of mutations) { for (const el of mutation.removedNodes) { if (el === element) { - obs.disconnect(); - callback(); + obs.disconnect() + callback() } } } - }); - obs.observe(parent, { childList: true }); -}; + }) + obs.observe(parent, { childList: true }) +} /** * Animate transition of DOMRect, with Web Animation API @@ -32,39 +32,38 @@ export const onRemove = (element, callback) => { * @returns {Animation} https://developer.mozilla.org/en-US/docs/Web/API/Animation */ export const animateRectTransition = (element, rect, options = {}) => { - if (!element.parentElement) - throw new Error('The node must already be attached'); + if (!element.parentElement) { throw new Error('The node must already be attached') } - const { width: w1, height: h1, left: x1, top: y1 } = rect; + const { width: w1, height: h1, left: x1, top: y1 } = rect const { width: w2, height: h2, left: x2, - top: y2, - } = element.getBoundingClientRect(); + top: y2 + } = element.getBoundingClientRect() - const rw = (w1 ?? w2) / w2; - const rh = (h1 ?? h2) / h2; - const dx = x1 - x2; - const dy = y1 - y2; + const rw = (w1 ?? w2) / w2 + const rh = (h1 ?? h2) / h2 + const dx = x1 - x2 + const dy = y1 - y2 if ((dx === 0 && dy === 0) || !isFinite(rw) || !isFinite(rh)) { - return element.animate([], { duration: 0 }); + return element.animate([], { duration: 0 }) } - const transform1 = `translate(0, 0) scale(1, 1)`; - const transform2 = `translate(${dx}px, ${dy}px) scale(${rw}, ${rh})`; + const transform1 = 'translate(0, 0) scale(1, 1)' + const transform2 = `translate(${dx}px, ${dy}px) scale(${rw}, ${rh})` const keyframes = [ { transform: transform1, opacity: 1 }, - { transform: transform2, opacity: 0.3 }, - ]; - if (options.resume === true) keyframes.reverse(); + { transform: transform2, opacity: 0.3 } + ] + if (options.resume === true) keyframes.reverse() return element.animate(keyframes, { duration: options.duration ?? 500, - easing: 'ease-in-out', - }); -}; + easing: 'ease-in-out' + }) +} /** * Throttle for function call @@ -73,18 +72,18 @@ export const animateRectTransition = (element, rect, options = {}) => { * @param {Number} delay milliseconds * @returns {Any} return value of function call, or null if throttled */ -export function throttle(func, delay) { - let timerFlag = null; +export function throttle (func, delay) { + let timerFlag = null return function (...args) { - const context = this; - if (timerFlag !== null) return null; + const context = this + if (timerFlag !== null) return null timerFlag = setTimeout( () => (timerFlag = null), - typeof delay === 'function' ? delay.call(context) : delay, - ); + typeof delay === 'function' ? delay.call(context) : delay + ) - return func.call(context, ...args); - }; + return func.call(context, ...args) + } } -- cgit v1.2.3-70-g09d2