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/dumbymap.mjs | 591 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 306 insertions(+), 285 deletions(-) (limited to 'src/dumbymap.mjs') 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); +}; -- cgit v1.2.3-70-g09d2