From 933eba7dc3bdc979fefadd47388b20b8360e1d6b Mon Sep 17 00:00:00 2001 From: Hsieh Chin Fan Date: Sat, 28 Sep 2024 17:47:52 +0800 Subject: style: prettier --- src/Layout.mjs | 240 +++++++++---------- src/MenuItem.mjs | 102 ++++---- src/css/dumbymap.css | 83 ++++--- src/css/index.css | 4 +- src/css/style.css | 36 ++- src/dumbyUtils.mjs | 86 ++++--- src/dumbymap.mjs | 591 ++++++++++++++++++++++++---------------------- src/editor.mjs | 655 +++++++++++++++++++++++++++------------------------ src/utils.mjs | 52 ++-- 9 files changed, 980 insertions(+), 869 deletions(-) (limited to 'src') diff --git a/src/Layout.mjs b/src/Layout.mjs index abefd4d..9cf2ab9 100644 --- a/src/Layout.mjs +++ b/src/Layout.mjs @@ -1,208 +1,208 @@ -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 + 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" - showcase.style.width = (parseFloat(getComputedStyle(container).width) - left) + "px" - } + const resizeByLeft = left => { + htmlHolder.style.width = left + "px"; + showcase.style.width = + parseFloat(getComputedStyle(container).width) - left + "px"; + }; const draggable = new PlainDraggable(bar, { handle: handle, - containment: { left: '25%', top: 0, right: '75%', height: 0 }, - }) - draggable.draggableCursor = "grab" - - draggable.onDrag = (pos) => { - handle.style.transform = 'unset' - resizeByLeft(pos.left) - } - draggable.onDragEnd = (_) => { - handle.removeAttribute('style') - } - - onRemove(bar, () => draggable.remove()) - } + containment: { left: "25%", top: 0, right: "75%", height: 0 }, + }); + draggable.draggableCursor = "grab"; + + draggable.onDrag = pos => { + handle.style.transform = "unset"; + resizeByLeft(pos.left); + }; + draggable.onDragEnd = _ => { + handle.removeAttribute("style"); + }; + + 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) - } + saveLeftTopAsData = element => { + const { left, top } = element.getBoundingClientRect(); + element.setAttribute("data-left", left); + element.setAttribute("data-top", top); + }; - addDraggable = (element) => { + addDraggable = element => { // Make sure current element always on top - const siblings = Array.from(element.parentElement?.querySelectorAll(':scope > *') ?? []) - let popTimer = null + const siblings = Array.from( + 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, handle: draggablePart, 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(() => { try { draggable.position(); } catch (_) { - null + null; } }).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.onmouseup = (e) => { + `; + 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.querySelector("#close").onclick = () => { + 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` - } - wrapper.querySelector('#minus-font-size').onclick = () => { - const fontSize = parseFloat(getComputedStyle(block).fontSize) / 16 - block.style.fontSize = `${fontSize - 0.2}rem` - } - }) - } + wrapper.querySelector("#plus-font-size").onclick = () => { + const fontSize = parseFloat(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`; + }; + }); + }; leaveHandler = ({ htmlHolder, blocks }) => { - const resumeFromDraggable = (block) => { - const draggableContainer = block.closest('.draggable-block') - if (!draggableContainer) return - htmlHolder.appendChild(block) - draggableContainer.remove() - } - blocks.forEach(resumeFromDraggable) - } + const resumeFromDraggable = block => { + 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 85ab1a2..2f9aff4 100644 --- a/src/MenuItem.mjs +++ b/src/MenuItem.mjs @@ -1,97 +1,97 @@ -import { createGeoLink } from './dumbymap' +import { createGeoLink } from "./dumbymap"; export function nextMap() { - const element = document.createElement('div') - element.className = 'menu-item' - element.innerHTML = 'Next Map (Tab)' - element.onclick = () => this.utils.focusNextMap() + const element = document.createElement("div"); + element.className = "menu-item"; + element.innerHTML = 'Next Map (Tab)'; + element.onclick = () => this.utils.focusNextMap(); - return element + return element; } export function nextBlock() { - const element = document.createElement('div') - element.className = 'menu-item' - element.innerHTML = 'Next Block (n)' - element.onclick = () => this.utils.focusNextBlock() + const element = document.createElement("div"); + element.className = "menu-item"; + element.innerHTML = 'Next Block (n)'; + element.onclick = () => this.utils.focusNextBlock(); - return element + return element; } export function nextLayout() { - const element = document.createElement('div') - element.className = 'menu-item' - element.innerHTML = 'Next Layout (x)' - element.onclick = () => this.utils.switchToNextLayout() + const element = document.createElement("div"); + element.className = "menu-item"; + element.innerHTML = 'Next Layout (x)'; + element.onclick = () => this.utils.switchToNextLayout(); - return element + return element; } export class GeoLink { - constructor({ range }) { - this.range = range + this.range = range; } createElement = () => { - const element = document.createElement('div') - element.className = 'menu-item' - element.innerText = "Add GeoLink" - element.onclick = this.addGeoLinkbyRange + const element = document.createElement("div"); + element.className = "menu-item"; + element.innerText = "Add GeoLink"; + element.onclick = this.addGeoLinkbyRange; - return element - } + return element; + }; addGeoLinkbyRange = () => { - const range = this.range - const content = range.toString() + const range = this.range; + 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}`; if (createGeoLink(anchor)) { - range.deleteContents() - range.insertNode(anchor) + range.deleteContents(); + range.insertNode(anchor); } - } + }; } export class Suggestion { constructor({ text, replace }) { - this.text = text - this.replace = replace + this.text = text; + this.replace = replace; } createElement(codemirror) { - const option = document.createElement('div'); - if (this.text.startsWith('<')) { + const option = document.createElement("div"); + if (this.text.startsWith("<")) { option.innerHTML = this.text; } else { 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') - } + Array.from(option.parentElement?.children ?? []).forEach(s => + 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) + const anchor = codemirror.getCursor(); + codemirror.setSelection(anchor, { ...anchor, ch: 0 }); + codemirror.replaceSelection(this.replace); codemirror.focus(); - const newAnchor = { ...anchor, ch: this.replace.length } + const newAnchor = { ...anchor, ch: this.replace.length }; codemirror.setCursor(newAnchor); }; - return option + return option; } } diff --git a/src/css/dumbymap.css b/src/css/dumbymap.css index 5a67575..4826b5b 100644 --- a/src/css/dumbymap.css +++ b/src/css/dumbymap.css @@ -8,12 +8,18 @@ root { } @keyframes fade-in { - from { opacity: 0; } - to { opacity: 1; } + from { + opacity: 0; + } + to { + opacity: 1; + } } @keyframes fade-out { - from { opacity: 1; } + from { + opacity: 1; + } to { visibility: hidden; @@ -22,13 +28,21 @@ root { } @keyframes map-fade-in { - from { opacity: 0.3; } - to { opacity: 1; } + from { + opacity: 0.3; + } + to { + opacity: 1; + } } @keyframes map-fade-out { - from { opacity: 1; } - to { opacity: 0.3; } + from { + opacity: 1; + } + to { + opacity: 0.3; + } } .mapclay { @@ -37,7 +51,7 @@ root { &.focus::after { content: "Map-ID: " attr(id); - padding: .3rem .5rem; + padding: 0.3rem 0.5rem; position: absolute; left: 50%; @@ -56,7 +70,7 @@ root { transform: translate(-50%, -50%); opacity: 0; - animation: 1.5s forwards fade-out cubic-bezier(.44,.18,.86,-0.21); + animation: 1.5s forwards fade-out cubic-bezier(0.44, 0.18, 0.86, -0.21); } } @@ -89,7 +103,6 @@ root { } *:has(> nav) { - /* FIXME Hide nav at proper time */ display: none; padding: 0.5rem; @@ -131,11 +144,10 @@ root { border-left: 0.5em solid #e0e0e0; - transition: border-color .5s linear; + transition: border-color 0.5s linear; } &.focus { - /* Dress up when block is focus */ /* background: var(--block-focus-color); */ @@ -145,7 +157,7 @@ root { &::after { content: counter(block) "/" attr(data-total); - padding: .3rem .5rem; + padding: 0.3rem 0.5rem; position: absolute; left: 50%; @@ -163,7 +175,7 @@ root { transform: translate(-50%, -50%); opacity: 0; - animation: 1.5s forwards fade-out cubic-bezier(.44,.18,.86,-0.21); + animation: 1.5s forwards fade-out cubic-bezier(0.44, 0.18, 0.86, -0.21); } } } @@ -194,7 +206,7 @@ root { } } - :has( > .mapclay, > [data-placeholder]) { + :has(> .mapclay, > [data-placeholder]) { display: flex; gap: 0.5em; @@ -225,7 +237,7 @@ root { .Showcase { display: none; overflow: visible; - width: 100%; + width: 100%; height: 100%; position: relative; @@ -246,7 +258,7 @@ root { &::after { content: "Layout: " attr(data-layout); - padding: .3rem .5rem; + padding: 0.3rem 0.5rem; position: absolute; left: 50%; @@ -272,13 +284,13 @@ root { max-width: 60em; &::after { - animation: 1.5s forwards fade-out cubic-bezier(.44,.18,.86,-0.21); + animation: 1.5s forwards fade-out cubic-bezier(0.44, 0.18, 0.86, -0.21); } } .Dumby[data-layout="side-by-side"] { &::after { - animation: 1.5s forwards fade-out cubic-bezier(.44,.18,.86,-0.21); + animation: 1.5s forwards fade-out cubic-bezier(0.44, 0.18, 0.86, -0.21); } .SemanticHtml, @@ -336,13 +348,12 @@ root { transform: unset; } } - } } .Dumby[data-layout="overlay"] { &::after { - animation: 1.5s forwards fade-out cubic-bezier(.44,.18,.86,-0.21); + animation: 1.5s forwards fade-out cubic-bezier(0.44, 0.18, 0.86, -0.21); } .SemanticHtml, @@ -384,7 +395,6 @@ root { padding-inline: 1rem; padding-block: 1rem; - &:hover { visibility: visible; opacity: 1; @@ -402,16 +412,16 @@ root { } #close { - cursor: pointer; + cursor: pointer; } - #plus-font-size { - cursor: zoom-in; - } + #plus-font-size { + cursor: zoom-in; + } - #minus-font-size { - cursor: zoom-out; - } + #minus-font-size { + cursor: zoom-out; + } } .draggable-block { @@ -431,7 +441,7 @@ root { &:has(.dumby-block.focus) { z-index: 9001; outline: solid #505050 1px; - border-color: #505050 + border-color: #505050; } &:has(pre:only-child [data-placeholder]:only-child) { @@ -440,7 +450,7 @@ root { /* Fade out top of contents scrolling */ &::before { - content: ''; + content: ""; display: block; height: 1.5rem; @@ -504,7 +514,7 @@ root { border-top-right-radius: 0.3rem; } - .handle { + .handle { font-size: 1.1rem; text-align: center; @@ -525,7 +535,8 @@ root { } } - &.drag, &:has(.draggable-part:hover) { + &.drag, + &:has(.draggable-part:hover) { .handle { background: #e1e1e1; @@ -534,7 +545,7 @@ root { .utils { visibility: visible; - animation: fade-in .5s; + animation: fade-in 0.5s; } > *:not(.draggable-part, .utils) { @@ -550,8 +561,8 @@ root { [data-placeholder] { height: 50px !important; - scale: .9; - transition-duration: .4s; + scale: 0.9; + transition-duration: 0.4s; transition-timing-function: ease-out; } } diff --git a/src/css/index.css b/src/css/index.css index 194ca9c..a17ecce 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -65,7 +65,7 @@ body { padding-inline: 0; font-family: monospace; - font-size: 1.0rem; + font-size: 1rem; &.focus { border: var(--content-focus-border); @@ -158,7 +158,7 @@ body { text-overflow: ellipsis; ::before { - width: 2rem + width: 2rem; } } } diff --git a/src/css/style.css b/src/css/style.css index 14a2e96..1d39cc0 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -1,5 +1,7 @@ /* CSS Reset */ -*, *::before, *::after { +*, +*::before, +*::after { box-sizing: border-box; } @@ -9,15 +11,28 @@ line-height: calc(1em + 0.5rem); } -img, picture, video, canvas, svg { +img, +picture, +video, +canvas, +svg { display: block; } -input, button, textarea, select { +input, +button, +textarea, +select { font: inherit; } -p, h1, h2, h3, h4, h5, h6 { +p, +h1, +h2, +h3, +h4, +h5, +h6 { overflow-wrap: break-word; hyphens: auto; } @@ -29,7 +44,12 @@ body { line-height: 1.6; } -h1, h2, h3, h4, h5, h6 { +h1, +h2, +h3, +h4, +h5, +h6 { font-weight: bold; margin-bottom: 0.5em; } @@ -102,7 +122,8 @@ pre:has(code) { } } -ul, ol { +ul, +ol { margin-top: 1em; margin-bottom: 1em; padding-left: 20px; @@ -118,7 +139,8 @@ table { width: 100%; } -th, td { +th, +td { padding: 8px; border: 1px solid #ddd; diff --git a/src/dumbyUtils.mjs b/src/dumbyUtils.mjs index c878e48..d30145c 100644 --- a/src/dumbyUtils.mjs +++ b/src/dumbyUtils.mjs @@ -1,58 +1,72 @@ export function focusNextMap(reverse = false) { - const renderedList = Array.from(this.htmlHolder.querySelectorAll('[data-render=fulfilled]')) - const mapNum = renderedList.length - if (mapNum === 0) return + const renderedList = Array.from( + this.htmlHolder.querySelectorAll("[data-render=fulfilled]"), + ); + const mapNum = renderedList.length; + if (mapNum === 0) return; // Get current focused map element - const currentFocus = this.container.querySelector('.mapclay.focus') + const currentFocus = this.container.querySelector(".mapclay.focus"); // Remove class name of focus for ALL candidates // This may trigger animation - renderedList.forEach(ele => ele.classList.remove('focus')) + renderedList.forEach(ele => ele.classList.remove("focus")); // Get next existing map element - const padding = reverse ? -1 : 1 - let nextIndex = currentFocus ? renderedList.indexOf(currentFocus) + padding : 0 - nextIndex = (nextIndex + mapNum) % mapNum - const nextFocus = renderedList[nextIndex] - nextFocus.classList.add("focus") + const padding = reverse ? -1 : 1; + let nextIndex = currentFocus + ? renderedList.indexOf(currentFocus) + padding + : 0; + nextIndex = (nextIndex + mapNum) % mapNum; + const nextFocus = renderedList[nextIndex]; + nextFocus.classList.add("focus"); - return nextFocus + return nextFocus; } export function focusDelay() { - return window.getComputedStyle(this.showcase).display === 'none' ? 50 : 300 + return 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 - const nextIndex = currentIndex === -1 ? 0 : (currentIndex + padding + layouts.length) % layouts.length - const nextLayout = layouts[nextIndex] - this.container.setAttribute("data-layout", nextLayout.name) + 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); } export function focusNextBlock(reverse = false) { - const blocks = this.blocks.filter(b => b.checkVisibility({ - contentVisibilityAuto: true, - opacityProperty: true, - visibilityProperty: true, - })) - const currentBlock = blocks.find(b => b.classList.contains('focus')) - const currentIndex = blocks.indexOf(currentBlock) - const padding = reverse ? -1 : 1 - const nextIndex = currentIndex === -1 ? 0 : (currentIndex + padding + blocks.length) % blocks.length - const nextBlock = blocks[nextIndex] - blocks.forEach(b => b.classList.remove('focus')) - nextBlock?.classList?.add('focus') - const scrollBlock = nextBlock.getBoundingClientRect().height > nextBlock.parentElement.getBoundingClientRect().height * 0.8 - ? 'nearest' - : 'center' - nextBlock.scrollIntoView({ behavior: 'smooth', block: scrollBlock }) + const blocks = this.blocks.filter(b => + b.checkVisibility({ + contentVisibilityAuto: true, + opacityProperty: true, + visibilityProperty: true, + }), + ); + const currentBlock = blocks.find(b => b.classList.contains("focus")); + const currentIndex = blocks.indexOf(currentBlock); + const padding = reverse ? -1 : 1; + const nextIndex = + currentIndex === -1 + ? 0 + : (currentIndex + padding + blocks.length) % blocks.length; + const nextBlock = blocks[nextIndex]; + blocks.forEach(b => b.classList.remove("focus")); + nextBlock?.classList?.add("focus"); + const scrollBlock = + nextBlock.getBoundingClientRect().height > + nextBlock.parentElement.getBoundingClientRect().height * 0.8 + ? "nearest" + : "center"; + nextBlock.scrollIntoView({ behavior: "smooth", block: scrollBlock }); } export function removeBlockFocus() { - this.blocks.forEach(b => b.classList.remove('focus')) + this.blocks.forEach(b => b.classList.remove("focus")); } diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs index 56375f7..cb029de 100644 --- a/src/dumbymap.mjs +++ b/src/dumbymap.mjs @@ -1,23 +1,23 @@ -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' - -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"; + +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 = {} +]; +const mapCache = {}; // FUNCTION: Get DocLinks from special anchor element {{{ /** @@ -25,34 +25,34 @@ const mapCache = {} * * @param {HTMLElement} Elements contains anchor elements for doclinks */ -export const createDocLink = (link) => { - link.classList.add('with-leader-line', 'doclink') - link.lines = [] +export const createDocLink = link => { + 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; + }; +}; // }}} // FUNCTION: Get GeoLinks from special anchor element {{{ /** @@ -62,92 +62,98 @@ export const createDocLink = (link) => { * @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) + ? xyInParams.split(",")?.map(Number) + : url?.href + ?.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; +}; // }}} export const markdown2HTML = (container, mdContent) => { // Render: Markdown -> HTML {{{ - Array.from(container.children).map(e => e.remove()) + Array.from(container.children).map(e => e.remove()); - - container.innerHTML = '
' - const htmlHolder = container.querySelector('.SemanticHtml') + container.innerHTML = '
'; + const htmlHolder = container.querySelector(".SemanticHtml"); const md = MarkdownIt({ html: true, breaks: true, }) .use(MarkdownItAnchor, { - permalink: MarkdownItAnchor.permalink.linkInsideHeader({ placement: 'before' }) + permalink: MarkdownItAnchor.permalink.linkInsideHeader({ + 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)) - }) + md.core.ruler.before("block", "dumby_block", state => { + 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) => { + md.block.ruler.before("table", "dumby_block", (state, startLine) => { if ( - state.src[state.bMarks[startLine - 1]] === '\n' && - 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.src[state.bMarks[startLine - 1]] === "\n" && + 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)) - }) + md.core.ruler.after("block", "dumby_block", state => { + state.tokens.push(new state.Token("dumby_block_close", "", -1)); + }); - const contentWithToc = '${toc}\n\n\n' + mdContent + const contentWithToc = "${toc}\n\n\n" + mdContent; 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 renderMaps = [] +}; +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 renderMaps = []; const dumbymap = { layouts, @@ -161,310 +167,325 @@ export const generateMaps = (container, {delay, mapCallback}) => { switchToNextLayout: throttle(utils.switchToNextLayout, 300), focusNextBlock: utils.focusNextBlock, removeBlockFocus: utils.removeBlockFocus, - } - } - Object.entries(dumbymap.utils) - .forEach(([util, func]) => { - dumbymap.utils[util] = func.bind(dumbymap) - }) + }, + }; + Object.entries(dumbymap.utils).forEach(([util, func]) => { + dumbymap.utils[util] = func.bind(dumbymap); + }); // LeaderLine {{{ - Array.from(container.querySelectorAll(docLinkSelector)) - .filter(createDocLink) + Array.from(container.querySelectorAll(docLinkSelector)).filter(createDocLink); // Get anchors with "geo:" scheme - htmlHolder.anchors = [] - const geoLinkCallback = (link) => { - link.onmouseover = () => addLeaderLines(link) - link.onmouseout = () => removeLeaderLines(link) - link.onclick = (event) => { - event.preventDefault() + htmlHolder.anchors = []; + const geoLinkCallback = link => { + link.onmouseover = () => addLeaderLines(link); + link.onmouseout = () => removeLeaderLines(link); + link.onclick = event => { + 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) - } - } - const geoLinks = Array.from(container.querySelectorAll(geoLinkSelector)) - .filter(l => 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 isAnchorVisible = (anchor) => { - const mapContainer = anchor.closest('.mapclay') - return insideWindow(anchor) && insideParent(anchor, mapContainer) - } - - const drawLeaderLine = (link) => (anchor) => { + removeLeaderLines(link); + }; + }; + const geoLinks = Array.from( + container.querySelectorAll(geoLinkSelector), + ).filter(l => 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 isAnchorVisible = anchor => { + const mapContainer = anchor.closest(".mapclay"); + return insideWindow(anchor) && insideParent(anchor, mapContainer); + }; + + const drawLeaderLine = link => anchor => { const line = new LeaderLine({ start: link, end: anchor, hide: true, - middleLabel: link.url.searchParams.get('text'), + middleLabel: link.url.searchParams.get("text"), path: "magnet", - }) - line.show('draw', { duration: 300, }) - return line - } + }); + line.show("draw", { duration: 300 }); + return line; + }; - const addLeaderLines = (link) => { + const addLeaderLines = link => { link.lines = htmlHolder.anchors .filter(isAnchorPointedBy(link)) .filter(isAnchorVisible) - .map(drawLeaderLine(link)) - } - - const removeLeaderLines = (link) => { - 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 insideWindow = (element) => { - const rect = element.getBoundingClientRect() - return rect.left > 0 && + .map(drawLeaderLine(link)); + }; + + const removeLeaderLines = link => { + 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 insideWindow = element => { + 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 offset = 20; - return childRect.left > parentRect.left + offset && + return ( + childRect.left > parentRect.left + offset && childRect.right < parentRect.right - offset && childRect.top > parentRect.top + offset && childRect.bottom < parentRect.bottom - offset - } + ); + }; //}}} // Draggable Blocks {{{ // Add draggable part for blocks - // }}} // CSS observer {{{ // Focus Map {{{ // Set focusArea - const mapFocusObserver = () => new MutationObserver((mutations) => { - const mutation = mutations.at(-1) - const target = mutation.target - const focus = target.getAttribute(mutation.attributeName).includes('focus') - const shouldBeInShowcase = focus && showcase.checkVisibility({ - contentVisibilityAuto: true, - opacityProperty: true, - visibilityProperty: true, - }) - - if (shouldBeInShowcase) { - 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) - - // 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) - - // To fit showcase, remove all inline style - target.removeAttribute('style') - showcase.appendChild(target) - - // Resume rect from Semantic HTML to Showcase, with animation - animateRectTransition(target, placeholder.getBoundingClientRect(), { - duration: 300, - 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}"`) - - // Consider animation may fail, write callback - const afterAnimation = () => { - placeholder.parentElement.replaceChild(target, placeholder) - target.style = placeholder.style.cssText - placeholder.remove() + const mapFocusObserver = () => + new MutationObserver(mutations => { + const mutation = mutations.at(-1); + const target = mutation.target; + const focus = target + .getAttribute(mutation.attributeName) + .includes("focus"); + const shouldBeInShowcase = + focus && + showcase.checkVisibility({ + contentVisibilityAuto: true, + opacityProperty: true, + visibilityProperty: true, + }); + + if (shouldBeInShowcase) { + 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); + + // 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); + + // To fit showcase, remove all inline style + target.removeAttribute("style"); + showcase.appendChild(target); + + // Resume rect from Semantic HTML to Showcase, with animation + animateRectTransition(target, placeholder.getBoundingClientRect(), { + duration: 300, + 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}"`); + + // Consider animation may fail, write callback + const afterAnimation = () => { + 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); } - - // animation from Showcase to placeholder - animateRectTransition(target, placeholder.getBoundingClientRect(), { duration: 300 }) - .finished - .finally(afterAnimation) - } - }) + }); // }}} // Layout {{{ // press key to switch layout - const defaultLayout = layouts[0] - container.setAttribute("data-layout", defaultLayout.name) + const defaultLayout = layouts[0]; + container.setAttribute("data-layout", defaultLayout.name); // 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 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) + layouts + .find(l => l.name === oldLayout) + ?.leaveHandler?.call(this, dumbymap); } Object.values(dumbymap) .flat() .filter(ele => ele instanceof HTMLElement) - .forEach(ele => ele.removeAttribute('style')) + .forEach(ele => ele.removeAttribute("style")); if (newLayout) { - layouts.find(l => l.name === newLayout) - ?.enterHandler - ?.call(this, dumbymap) + layouts + .find(l => l.name === newLayout) + ?.enterHandler?.call(this, dumbymap); } // Since layout change may show/hide showcase, the current focused map should do something // Reset attribute triggers MutationObserver which is observing it - const focusMap = container.querySelector('.mapclay.focus') - ?? container.querySelector('.mapclay') - focusMap?.classList?.add('focus') + const focusMap = + container.querySelector(".mapclay.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 - mapElement.setAttribute('tabindex', "-1") - if (mapElement.getAttribute('data-render') === 'fulfilled') { - mapCache[mapElement.id] = renderer + const afterMapRendered = renderer => { + const mapElement = renderer.target; + mapElement.setAttribute("tabindex", "-1"); + if (mapElement.getAttribute("data-render") === "fulfilled") { + 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) - mapElement.querySelectorAll('.marker') - .forEach(marker => htmlHolder.anchors.push(marker)) + renderer.addMarkers(markers); + mapElement + .querySelectorAll(".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 + attributeOldValue: true, }); - onRemove(mapElement, () => observer.disconnect()) - } + onRemove(mapElement, () => observer.disconnect()); + }; // Set unique ID for map container - const mapIdList = [] - const assignMapId = (config) => { - let mapId = config.id + const mapIdList = []; + const assignMapId = config => { + 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)') ?? []) + const elementsWithMapConfig = Array.from( + container.querySelectorAll("pre:has(.language-map)") ?? [], + ); // Add default aliases into each config - const configConverter = (config => ({ - use: config.use ?? 'Leaflet', + const configConverter = config => ({ + use: config.use ?? "Leaflet", width: "100%", ...config, aliases: { ...defaultAliases, - ...config.aliases ?? {} + ...(config.aliases ?? {}), }, - })) - 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') - - let configList = [] - try { - configList = parseConfigsFromYaml(configText).map(assignMapId) - } catch (_) { - console.warn('Fail to parse yaml config for element', target) - return - } + }); + 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"); + + let configList = []; + try { + configList = parseConfigsFromYaml(configText).map(assignMapId); + } catch (_) { + 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; + // 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; - 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()) - } + // 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()); + } - // Render maps with delay - const timer = setTimeout(() => + // Render maps with delay + const timer = setTimeout( + () => render(target, configList).forEach(renderMap => { - renderMaps.push(renderMap) - renderMap.then(afterMapRendered) + renderMaps.push(renderMap); + renderMap.then(afterMapRendered); }), - delay ?? 1000 - ) - onRemove(htmlHolder, () => { - clearTimeout(timer) - }) - }) + delay ?? 1000, + ); + onRemove(htmlHolder, () => { + clearTimeout(timer); + }); + }); //}}} - return Object.seal(dumbymap) -} + return Object.seal(dumbymap); +}; diff --git a/src/editor.mjs b/src/editor.mjs index c04ba24..73177b6 100644 --- a/src/editor.mjs +++ b/src/editor.mjs @@ -1,143 +1,170 @@ /*global EasyMDE*/ /*eslint no-undef: "error"*/ -import { markdown2HTML, generateMaps } from './dumbymap' -import { defaultAliases, parseConfigsFromYaml } from 'mapclay' -import * as menuItem from './MenuItem' +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; const toggleEditing = () => { if (document.body.getAttribute("data-mode") === "editing") { - document.body.removeAttribute("data-mode") + document.body.removeAttribute("data-mode"); } else { - document.body.setAttribute("data-mode", "editing") + document.body.setAttribute("data-mode", "editing"); } - HtmlContainer.setAttribute("data-layout", "normal") -} + HtmlContainer.setAttribute("data-layout", "normal"); +}; // }}} // 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' +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'; const editor = new EasyMDE({ element: textArea, initialValue: defaultContent, autosave: { enabled: true, - uniqueId: 'dumbymap', + uniqueId: "dumbymap", }, indentWithTabs: false, lineNumbers: true, promptURLs: true, uploadImage: true, spellChecker: false, - toolbarButtonClassPrefix: 'mde', + toolbarButtonClassPrefix: "mde", status: false, shortcuts: { - "map": "Ctrl-Alt-M", - "debug": "Ctrl-Alt-D", - "toggleUnorderedList": null, - "toggleOrderedList": null, + map: "Ctrl-Alt-M", + debug: "Ctrl-Alt-D", + toggleUnorderedList: null, + toggleOrderedList: null, }, toolbar: [ { - name: 'map', - title: 'Toggle Map Generation', + name: "map", + title: "Toggle Map Generation", text: "🌏", action: () => toggleEditing(), }, { - name: 'debug', - title: 'Save content as URL', + 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); + alert("URL copied to clipboard"); }, - }, 'undo', 'redo', '|', 'heading-1', 'heading-2', '|', 'link', 'image', '|', 'bold', 'italic', 'strikethrough', 'code', 'clean-block', '|', 'unordered-list', 'ordered-list', 'quote', 'table' + }, + "undo", + "redo", + "|", + "heading-1", + "heading-2", + "|", + "link", + "image", + "|", + "bold", + "italic", + "strikethrough", + "code", + "clean-block", + "|", + "unordered-list", + "ordered-list", + "quote", + "table", ], }); -const cm = editor.codemirror +const cm = editor.codemirror; -const getStateFromHash = (hash) => { +const getStateFromHash = hash => { const hashValue = hash.substring(1); - const stateString = decodeURIComponent(hashValue) - try { return JSON.parse(stateString) ?? {} } - catch (_) { return {} } -} + const stateString = decodeURIComponent(hashValue); + try { + return JSON.parse(stateString) ?? {}; + } catch (_) { + return {}; + } +}; -const getContentFromHash = (hash) => { - const state = getStateFromHash(hash) - return state.content -} +const getContentFromHash = hash => { + 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 {{{ -const afterMapRendered = (_) => { +const afterMapRendered = _ => { // mapHolder.oncontextmenu = (event) => { // event.preventDefault() // 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 completeForCodeBlock = change => { + 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 (text === "-" && change.to.ch === 0 && insideCodeblockForMap(cm.getCursor())) { - cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 1 }) - cm.replaceSelection(text.repeat(3) + '\n') + if ( + text === "-" && + change.to.ch === 0 && + insideCodeblockForMap(cm.getCursor()) + ) { + cm.setSelection({ line: line, ch: 0 }, { line: 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)) - const numberOfFences = cm.getValue() - .split('\n') - .filter(line => line.match(/[\u0060]{3}/)) - .length + cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 1 }); + cm.replaceSelection(text.repeat(3)); + const numberOfFences = cm + .getValue() + .split("\n") + .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 }); } } } @@ -145,63 +172,63 @@ 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: line, ch: 0 }, { line: line, ch: 2 }); + cm.replaceSelection(""); } } -} +}; const debounceForMap = (() => { let timer = null; - return function(...args) { - dumbymap = generateMaps.apply(this, args) + 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) @@ -212,401 +239,415 @@ window.onhashchange = () => { // }}} // Completion in Code Blok {{{ // Elements about suggestions {{{ -const menu = document.createElement('div') -menu.id = 'menu' -menu.onclick = () => menu.style.display = 'none' -document.body.append(menu) +const menu = document.createElement("div"); +menu.id = "menu"; +menu.onclick = () => (menu.style.display = "none"); +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 insideCodeBlock = token.state.overlay.codeBlock && !cm.getLine(anchor.line).match(/^[\u0060]{3}/) - if (!insideCodeBlock) return false - - let line = anchor.line - 1 +const insideCodeblockForMap = anchor => { + const token = cm.getTokenAt(anchor); + const insideCodeBlock = + token.state.overlay.codeBlock && + !cm.getLine(anchor.line).match(/^[\u0060]{3}/); + if (!insideCodeBlock) return false; + + let line = anchor.line - 1; while (line >= 0) { - const content = cm.getLine(line) - if (content === '```map') { - return true - } else if (content === '```') { - return false + const content = cm.getLine(line); + if (content === "```map") { + return true; + } else if (content === "```") { + 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 getLineWithRenderer = anchor => { + const currentLine = anchor.line; + if (!cm.getLine) return null; - const match = (line) => cm.getLine(line).match(/^use: /) - - if (match(currentLine)) return currentLine + const match = line => cm.getLine(line).match(/^use: /); + 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())) + const matchedOptions = validOptions.filter(o => + 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()}: `, - })) -} + return suggestOptions.map( + o => + new menuItem.Suggestion({ + text: `${o.valueOf()}`, + replace: `${o.valueOf()}: `, + }), + ); +}; // }}} // FUNCTION: Return suggestion for example of option value {{{ -const getSuggestionFromMapOption = (option) => { - if (!option.example) return null +const getSuggestionFromMapOption = option => { + 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 ?? ""}`, - }) -} + }); +}; // }}} // 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 getSuggestionsFromAliases = option => + Object.entries(aliasesForMapOptions[option.valueOf()] ?? {})?.map(record => { + const [alias, value] = record; + const valueString = JSON.stringify(value).replaceAll('"', ""); return new menuItem.Suggestion({ text: `${alias}${valueString}`, replace: `${option.valueOf()}: ${valueString}`, - }) - }) - ?? [] + }); + }) ?? []; // }}} // FUCNTION: Handler for map codeblock {{{ -const handleTypingInCodeBlock = (anchor) => { - const text = cm.getLine(anchor.line) +const handleTypingInCodeBlock = anchor => { + 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 getSuggestions = anchor => { + const text = cm.getLine(anchor.line); // Clear marks on text - cm.findMarks( - { ...anchor, ch: 0 }, - { ...anchor, ch: text.length } - ).forEach(m => m.clear()) + cm.findMarks({ ...anchor, ch: 0 }, { ...anchor, ch: text.length }).forEach( + m => m.clear(), + ); // Mark user input invalid by case const markInputIsInvalid = () => - cm.getDoc().markText( - { ...anchor, ch: 0 }, - { ...anchor, ch: text.length }, - { className: 'invalid-input' }, - ) + cm + .getDoc() + .markText( + { ...anchor, ch: 0 }, + { ...anchor, ch: text.length }, + { 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 + ? cm.getLine(lineWithRenderer).split(" ")[1] + : 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) - console.warn(`Fail to get valid options from Renderer typed: ${renderer}`) - }) - return [] + markInputIsInvalid(lineWithRenderer); + console.warn( + `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)) + .filter(([renderer]) => { + 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 : [] + .map( + ([renderer, info]) => + new menuItem.Suggestion({ + text: `use: ${renderer}`, + 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)) + .forEach(option => menu.appendChild(option)); - const widgetAnchor = document.createElement('div') - cm.addWidget(anchor, widgetAnchor, true) - const rect = widgetAnchor.getBoundingClientRect() + 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' -} + 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() +cm.on("cursorActivity", _ => { + menu.style.display = "none"; + const anchor = cm.getCursor(); if (insideCodeblockForMap(anchor)) { - handleTypingInCodeBlock(anchor) + handleTypingInCodeBlock(anchor); } }); cm.on("blur", () => { - menu.style.display = 'none' - cm.getWrapperElement().classList.remove('focus') - HtmlContainer.classList.add('focus') -}) + menu.style.display = "none"; + cm.getWrapperElement().classList.remove("focus"); + HtmlContainer.classList.add("focus"); +}); // }}} // EVENT: keydown for suggestions {{{ -const keyForSuggestions = ['Tab', 'Enter', 'Escape'] -cm.on('keydown', (_, e) => { - if (!cm.hasFocus || !keyForSuggestions.includes(e.key) || menu.style.display === 'none') return; +const keyForSuggestions = ["Tab", "Enter", "Escape"]; +cm.on("keydown", (_, e) => { + if ( + !cm.hasFocus || + !keyForSuggestions.includes(e.key) || + menu.style.display === "none" + ) + 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(); // Suggestion when pressing Tab or Shift + Tab - const nextSuggestion = currentSuggestion?.nextSibling ?? menu.querySelector('.container__suggestion:first-child') - const previousSuggestion = currentSuggestion?.previousSibling ?? menu.querySelector('.container__suggestion:last-child') - const focusSuggestion = e.shiftKey ? previousSuggestion : nextSuggestion + const nextSuggestion = + currentSuggestion?.nextSibling ?? + menu.querySelector(".container__suggestion:first-child"); + const previousSuggestion = + currentSuggestion?.previousSibling ?? + menu.querySelector(".container__suggestion:last-child"); + const focusSuggestion = e.shiftKey ? previousSuggestion : nextSuggestion; // Current editor selection state - const anchor = cm.getCursor() + const anchor = cm.getCursor(); 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' }) + case "Tab": + 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() + case "Enter": + currentSuggestion.onclick(); break; - case 'Escape': - menu.style.display = 'none'; + case "Escape": + menu.style.display = "none"; // Focus editor again - setTimeout(() => cm.focus() && cm.setCursor(anchor), 100) + setTimeout(() => cm.focus() && cm.setCursor(anchor), 100); break; } }); -document.onkeydown = (e) => { - if (e.altKey && e.ctrlKey && e.key === 'm') { - toggleEditing() - e.preventDefault() - return null +document.onkeydown = e => { + if (e.altKey && e.ctrlKey && e.key === "m") { + toggleEditing(); + e.preventDefault(); + return null; } if (!cm.hasFocus()) { - if (e.key === 'F1') { - e.preventDefault() - cm.focus() + if (e.key === "F1") { + e.preventDefault(); + cm.focus(); } - if (e.key === 'Tab') { - e.preventDefault() - dumbymap.utils.focusNextMap(e.shiftKey) + if (e.key === "Tab") { + e.preventDefault(); + dumbymap.utils.focusNextMap(e.shiftKey); } - if (e.key === 'x' || e.key === 'X') { - e.preventDefault() - dumbymap.utils.switchToNextLayout(e.shiftKey) + if (e.key === "x" || e.key === "X") { + e.preventDefault(); + dumbymap.utils.switchToNextLayout(e.shiftKey); } - if (e.key === 'n') { - e.preventDefault() - dumbymap.utils.focusNextBlock() + if (e.key === "n") { + e.preventDefault(); + dumbymap.utils.focusNextBlock(); } - if (e.key === 'p') { - e.preventDefault() - dumbymap.utils.focusNextBlock(true) + if (e.key === "p") { + e.preventDefault(); + dumbymap.utils.focusNextBlock(true); } - if (e.key === 'Escape') { - e.preventDefault() - dumbymap.utils.removeBlockFocus() + if (e.key === "Escape") { + e.preventDefault(); + dumbymap.utils.removeBlockFocus(); } } -} +}; // }}} // }}} // Layout Switch {{{ const layoutObserver = new MutationObserver(() => { - const layout = HtmlContainer.getAttribute('data-layout') - if (layout !== 'normal') { - document.body.removeAttribute('data-mode') + const layout = HtmlContainer.getAttribute("data-layout"); + if (layout !== "normal") { + document.body.removeAttribute("data-mode"); } -}) +}); layoutObserver.observe(HtmlContainer, { attributes: true, attributeFilter: ["data-layout"], - attributeOldValue: true + attributeOldValue: true, }); // }}} // ContextMenu {{{ -document.oncontextmenu = (e) => { - if (cm.hasFocus()) return +document.oncontextmenu = e => { + if (cm.hasFocus()) return; - const selection = document.getSelection() - const range = selection.getRangeAt(0) + const selection = document.getSelection(); + const range = selection.getRangeAt(0); if (selection) { - e.preventDefault() - menu.innerHTML = '' - menu.style.cssText = `display: block; left: ${e.clientX + 10}px; top: ${e.clientY + 5}px;` - const addGeoLink = new menuItem.GeoLink({ range }) - menu.appendChild(addGeoLink.createElement()) + e.preventDefault(); + menu.innerHTML = ""; + menu.style.cssText = `display: block; left: ${e.clientX + 10}px; top: ${e.clientY + 5}px;`; + const addGeoLink = new menuItem.GeoLink({ range }); + menu.appendChild(addGeoLink.createElement()); } - menu.appendChild(menuItem.nextMap.bind(dumbymap)()) - menu.appendChild(menuItem.nextBlock.bind(dumbymap)()) - menu.appendChild(menuItem.nextLayout.bind(dumbymap)()) -} - -const actionOutsideMenu = (e) => { - if (menu.style.display === 'none' || cm.hasFocus()) 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.appendChild(menuItem.nextMap.bind(dumbymap)()); + menu.appendChild(menuItem.nextBlock.bind(dumbymap)()); + menu.appendChild(menuItem.nextLayout.bind(dumbymap)()); +}; + +const actionOutsideMenu = e => { + if (menu.style.display === "none" || cm.hasFocus()) 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); // }}} diff --git a/src/utils.mjs b/src/utils.mjs index ead3002..ad9131e 100644 --- a/src/utils.mjs +++ b/src/utils.mjs @@ -18,8 +18,8 @@ export const onRemove = (element, callback) => { } } }); - obs.observe(parent, { childList: true, }); -} + obs.observe(parent, { childList: true }); +}; /** * Animate transition of DOMRect, with Web Animation API @@ -32,18 +32,24 @@ 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: w2, height: h2, left: x2, top: y2 } = element.getBoundingClientRect() + const { width: w1, height: h1, left: x1, top: y1 } = rect; + const { + width: w2, + height: h2, + left: x2, + top: y2, + } = element.getBoundingClientRect(); - const rw = (w1 ?? w2) / w2 - const rh = (h1 ?? h2) / h2 + 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 }) + if ((dx === 0 && dy === 0) || !isFinite(rw) || !isFinite(rh)) { + return element.animate([], { duration: 0 }); } const transform1 = `translate(0, 0) scale(1, 1)`; @@ -51,18 +57,14 @@ export const animateRectTransition = (element, rect, options = {}) => { const keyframes = [ { transform: transform1, opacity: 1 }, { transform: transform2, opacity: 0.3 }, - ] - if (options.resume === true) keyframes.reverse() - - return element.animate( - keyframes, - { - duration: options.duration ?? 500, - easing: 'ease-in-out', - } - ); -} + ]; + if (options.resume === true) keyframes.reverse(); + return element.animate(keyframes, { + duration: options.duration ?? 500, + easing: "ease-in-out", + }); +}; /** * Throttle for function call @@ -74,13 +76,13 @@ export const animateRectTransition = (element, rect, options = {}) => { export function throttle(func, delay) { let timerFlag = null; - return function(...args) { - const context = this - if (timerFlag !== null) return null + return function (...args) { + const context = this; + if (timerFlag !== null) return null; timerFlag = setTimeout( - () => timerFlag = null, - typeof delay === 'function' ? delay.call(context) : delay + () => (timerFlag = null), + typeof delay === "function" ? delay.call(context) : delay, ); return func.call(context, ...args); -- cgit v1.2.3-70-g09d2