From 4dd0fa87859e6ffa1b230eedd1388098a8ba81a1 Mon Sep 17 00:00:00 2001 From: Hsieh Chin Fan Date: Thu, 17 Oct 2024 12:40:50 +0800 Subject: feat: set "sticky" as one of default layouts * move function addDraggable() into module scope * showcase in sticky layout is draggable now * add query paramerter for "use" * FIX: set initialLayout after observer is set --- addon/index.mjs | 5 +- src/Layout.mjs | 179 ++++++++++++++++++++++++++++++++------------------- src/css/dumbymap.css | 56 ++++++++++------ src/dumbymap.mjs | 15 ++--- src/editor.mjs | 2 +- 5 files changed, 155 insertions(+), 102 deletions(-) diff --git a/addon/index.mjs b/addon/index.mjs index b5d71ba..77716b6 100644 --- a/addon/index.mjs +++ b/addon/index.mjs @@ -1,4 +1,5 @@ const url = new URL(window.location) +const use = url.searchParams.get('use') if (url.host === 'www.ptt.cc') { const content = document.querySelector('#main-content') Array.from(content.childNodes) @@ -22,7 +23,7 @@ const addBlocks = blockSelector : undefined const simpleRender = window.mapclay.renderWith(config => ({ - use: 'Leaflet', + use: use ?? 'Leaflet', width: '100%', height: '200px', XYZ: 'https://tile.openstreetmap.jp/styles/osm-bright/512/{z}/{x}/{y}.png', @@ -36,7 +37,7 @@ const simpleRender = window.mapclay.renderWith(config => ({ window.generateMaps(document.querySelector('main') ?? document.body, { crs: url.searchParams.get('crs') ?? 'EPSG:4326', addBlocks, - initialLayout: '', + initialLayout: 'sticky', render: simpleRender, autoMap: true, }) diff --git a/src/Layout.mjs b/src/Layout.mjs index 485b513..f79c70e 100644 --- a/src/Layout.mjs +++ b/src/Layout.mjs @@ -85,6 +85,83 @@ export class SideBySide extends Layout { } } +/** + * addDraggable. + * + * @param {HTMLElement} element + */ +const addDraggable = (element, { snap, left, top } = {}) => { + element.classList.add('draggable-block') + + // Make sure current element always on top + const siblings = Array.from( + element.parentElement?.querySelectorAll(':scope > *') ?? [], + ) + let popTimer = null + const onmouseover = () => { + popTimer = setTimeout(() => { + siblings.forEach(e => e.style.removeProperty('z-index')) + element.style.zIndex = '9001' + }, 200) + } + const onmouseout = () => { + clearTimeout(popTimer) + } + element.addEventListener('mouseover', onmouseover) + element.addEventListener('mouseout', onmouseout) + + // Add draggable part + const draggablePart = document.createElement('div') + element.appendChild(draggablePart) + draggablePart.className = 'draggable-part' + draggablePart.innerHTML = '
\u2630
' + + // Add draggable instance + const draggable = new PlainDraggable(element, { + left, + top, + handle: draggablePart, + snap, + }) + + // FIXME use pure CSS to hide utils + const utils = element.querySelector('.utils') + draggable.onDragStart = () => { + if (utils) utils.style.display = 'none' + element.classList.add('drag') + } + + draggable.onDragEnd = () => { + if (utils) utils.style = '' + element.classList.remove('drag') + element.style.zIndex = '9000' + } + + // Reposition draggable instance when resized + const resizeObserver = new window.ResizeObserver(() => { + draggable?.position() + }) + resizeObserver.observe(element) + + // Callback for remove + onRemove(element, () => { + resizeObserver.disconnect() + }) + + new window.MutationObserver(() => { + if (!element.classList.contains('draggable-block') && draggable) { + element.removeEventListener('mouseover', onmouseover) + element.removeEventListener('mouseout', onmouseout) + resizeObserver.disconnect() + } + }).observe(element, { + attributes: true, + attributeFilter: ['class'], + }) + + return draggable +} + /** * Overlay Layout, Showcase occupies viewport, and HTML content becomes draggable blocks * @@ -102,70 +179,6 @@ export class Overlay extends Layout { element.dataset.top = top } - /** - * addDraggable. - * - * @param {HTMLElement} element - */ - addDraggable = element => { - // Make sure current element always on top - 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) - } - element.onmouseout = () => { - clearTimeout(popTimer) - } - - // Add draggable part - const draggablePart = document.createElement('div') - element.appendChild(draggablePart) - draggablePart.className = 'draggable-part' - draggablePart.innerHTML = '
\u2630
' - - // Add draggable instance - const { left, top } = element.getBoundingClientRect() - const draggable = new PlainDraggable(element, { - top, - left, - handle: draggablePart, - snap: { x: { step: 20 }, y: { step: 20 } }, - }) - - // FIXME use pure CSS to hide utils - const utils = element.querySelector('.utils') - draggable.onDragStart = () => { - utils.style.display = 'none' - element.classList.add('drag') - } - - draggable.onDragEnd = () => { - utils.style = '' - element.classList.remove('drag') - element.style.zIndex = '9000' - } - - // Reposition draggable instance when resized - new window.ResizeObserver(() => { - try { - draggable.position() - } catch (err) { - console.warn(err) - } - }).observe(element) - - // Callback for remove - onRemove(element, () => { - draggable.remove() - }) - } - /** * enterHandler. * @@ -182,7 +195,7 @@ export class Overlay extends Layout { } // Create draggable blocks and set each position by previous one - let [left, top] = [20, 20] + const [left, top] = [20, 20] blocks.forEach(block => { const originLeft = Number(block.dataset.left) const originTop = Number(block.dataset.top) @@ -210,8 +223,8 @@ export class Overlay extends Layout { wrapper.style.left = left + 'px' wrapper.style.top = top + 'px' htmlHolder.appendChild(wrapper) - const { width } = wrapper.getBoundingClientRect() - left += width + 30 + const rect = wrapper.getBoundingClientRect() + left += rect.width + 30 if (left > window.innerWidth) { top += 200 left = left % window.innerWidth @@ -222,7 +235,14 @@ export class Overlay extends Layout { wrapper, { left: originLeft, top: originTop }, { resume: true, duration: 300 }, - ).finished.finally(() => this.addDraggable(wrapper)) + ).finished.finally(() => addDraggable(wrapper, { + left: rect.left, + top: rect.top, + snap: { + x: { step: 20 }, + y: { step: 20 }, + }, + })) // Trivial case: // This hack make sure utils remains at the same place even when wrapper resized @@ -271,3 +291,26 @@ export class Overlay extends Layout { blocks.forEach(resumeFromDraggable) } } + +/** + * Sticky Layout, Showcase is draggable and stick to viewport + * + * @extends {Layout} + */ +export class Sticky extends Layout { + draggable = document.createElement('div') + + enterHandler = ({ showcase }) => { + showcase.replaceWith(this.draggable) + this.draggable.appendChild(showcase) + this.draggableInstance = addDraggable(this.draggable) + const rect = this.draggable.getBoundingClientRect() + this.draggable.style.cssText = `left: ${window.innerWidth - rect.width - 20}px; top: ${window.innerHeight - rect.height - 20}px;` + } + + leaveHandler = ({ showcase }) => { + this.draggableInstance?.remove() + this.draggable.replaceWith(showcase) + this.draggable.querySelectorAll(':scope > :not(.mapclay)').forEach(e => e.remove()) + } +} diff --git a/src/css/dumbymap.css b/src/css/dumbymap.css index c0bb1a9..cc161fa 100644 --- a/src/css/dumbymap.css +++ b/src/css/dumbymap.css @@ -141,7 +141,7 @@ root { background-repeat: no-repeat; background-position: right 2px top 2px; - color: #555; + color: #555 !important; background-size: 12px 12px; &.geolink { @@ -166,7 +166,7 @@ root { } } -.Dumby:not([data-layout=""]) .dumby-block { +.DumbyMap.Dumby[data-layout] .dumby-block { padding: 1rem 1rem 1rem 2rem; position: relative; @@ -495,7 +495,7 @@ root { } } -.Dumby[data-layout='normal'] { +.DumbyMap.Dumby[data-layout='normal'] { max-width: 60em; &::after { @@ -595,6 +595,35 @@ root { } } +.Dumby[data-layout='sticky'] { + .SemanticHtml { + max-width: 60em; + margin: 0 auto; + } + + .draggable-block { + position: fixed; + + &::before { + display: none; + } + } + + .Showcase { + display: block; + overflow: hidden; + width: 20vw; + min-width: 10vw; + height: 200px; + min-height: 100px; + resize: both; + + .mapclay { + border-radius: 4px !important; + } + } +} + .utils { display: flex; padding-block: 1rem; @@ -642,6 +671,7 @@ root { .draggable-block { overflow: visible; width: fit-content; + height: fit-content; position: absolute; @@ -659,7 +689,7 @@ root { opacity: 0; pointer-events: auto; - &:has(.dumby-block.focus) { + &:has(.dumby-block.focus, .mapclay.focus) { visibility: visible; opacity: 1; } @@ -730,7 +760,7 @@ root { position: absolute; left: 0; top: 0; - z-index: 1; + z-index: 2; border-top-left-radius: 0.3rem; border-top-right-radius: 0.3rem; } @@ -782,22 +812,6 @@ root { font-weight: bold; } -.Dumby[data-layout='sticky'] { - max-width: 80em; - - .Showcase { - display: block; - width: 20vw; - height: 40vh; - - position: absolute; - right: 20px; - bottom: 20px; - - background: red; - } -} - .dumby-block details { display: block; width: fit-content; diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs index b612367..13f4574 100644 --- a/src/dumbymap.mjs +++ b/src/dumbymap.mjs @@ -5,7 +5,7 @@ import MarkdownItFrontMatter from 'markdown-it-front-matter' import MarkdownItInjectLinenumbers from 'markdown-it-inject-linenumbers' import * as mapclay from 'mapclay' import { replaceTextNodes, onRemove, animateRectTransition, throttle, shiftByWindow } from './utils' -import { Layout, SideBySide, Overlay } from './Layout' +import { Layout, SideBySide, Overlay, Sticky } from './Layout' import * as utils from './dumbyUtils' import * as menuItem from './MenuItem' import PlainModal from 'plain-modal' @@ -23,6 +23,7 @@ const defaultLayouts = [ new Layout({ name: 'normal' }), new SideBySide({ name: 'side-by-side' }), new Overlay({ name: 'overlay' }), + new Sticky({ name: 'sticky' }), ] /** Cache across every dumbymap generation */ @@ -169,13 +170,12 @@ export const generateMaps = (container, { /** Prepare Contaner */ container.classList.add('Dumby') delete container.dataset.layout - container.dataset.layout = initialLayout ?? defaultLayouts[0].name /** Prepare Semantic HTML part and blocks of contents inside */ const htmlHolder = container.querySelector('.SemanticHtml') ?? Array.from(container.children).find(e => e.id?.includes('main') || e.className.includes('main')) ?? Array.from(container.children).sort((a, b) => a.textContent.length < b.textContent.length).at(0) - htmlHolder.classList.add('.SemanticHtml') + htmlHolder.classList.add('SemanticHtml') const blocks = addBlocks(htmlHolder) blocks.forEach(b => { @@ -280,13 +280,7 @@ export const generateMaps = (container, { const mutation = mutations.at(-1) const target = mutation.target const focus = target.classList.contains('focus') - const shouldBeInShowcase = - focus && - showcase.checkVisibility({ - contentVisibilityAuto: true, - opacityProperty: true, - visibilityProperty: true, - }) + const shouldBeInShowcase = focus && showcase.checkVisibility() if (focus) { dumbymap.utils @@ -393,6 +387,7 @@ export const generateMaps = (container, { }) onRemove(htmlHolder, () => layoutObserver.disconnect()) + container.dataset.layout = initialLayout ?? defaultLayouts[0].name /** * afterMapRendered. callback of each map rendered diff --git a/src/editor.mjs b/src/editor.mjs index d56ad4b..2854373 100644 --- a/src/editor.mjs +++ b/src/editor.mjs @@ -24,7 +24,7 @@ const initialLayout = pageParams.get('layout') /** Variables: dumbymap and editor **/ const context = document.querySelector('[data-mode]') const textArea = document.querySelector('.editor textarea') -const dumbyContainer = document.querySelector('.DumbyMap') +const dumbyContainer = document.querySelector('.Dumby') dumbyContainer.dataset.scrollLine = '' /** Watch: DumbyMap */ new window.MutationObserver(mutations => { -- cgit v1.2.3-70-g09d2