From 27feb1302304eede3cdc58ffde5ce8e0f0019da4 Mon Sep 17 00:00:00 2001 From: Hsieh Chin Fan Date: Sun, 29 Sep 2024 21:01:03 +0800 Subject: feat: add submenu for map/block/layout switching * Add general classes into MenuItem * Use MutationObserver for data-mode * Automatically unfocus other maps when one is focused TODO: * hover effect on submenu item doesn't work, why? * shorcuts hint in selector ".folder.menu-item" not looks great --- src/MenuItem.mjs | 101 +++++++++++++++++++++++++++++++++++++++++++---------- src/css/index.css | 33 ++++++++++++++--- src/dumbyUtils.mjs | 9 ++--- src/dumbymap.mjs | 26 ++++++++------ src/editor.mjs | 31 +++++++++++----- 5 files changed, 151 insertions(+), 49 deletions(-) (limited to 'src') diff --git a/src/MenuItem.mjs b/src/MenuItem.mjs index 734c313..e974f9e 100644 --- a/src/MenuItem.mjs +++ b/src/MenuItem.mjs @@ -1,31 +1,94 @@ 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(); +class Item { + constructor({ text, innerHTML, onclick }) { + this.text = text; + this.innerHTML = innerHTML; + this.onclick = onclick; + } - return element; + get element() { + const element = document.createElement('div'); + element.innerHTML = this.innerHTML ? this.innerHTML : this.text; + element.classList.add('menu-item'); + element.onclick = this.onclick; + return element; + } } -export function nextBlock() { - const element = document.createElement('div'); - element.className = 'menu-item'; - element.innerHTML = 'Next Block (n)'; - element.onclick = () => this.utils.focusNextBlock(); +class Folder { + constructor({ text, innerHTML, items }) { + this.text = text; + this.innerHTML = innerHTML; + this.items = items; + this.utils; + } + + get element() { + const element = document.createElement('div'); + element.classList.add(this.className); + element.className = 'menu-item folder'; + element.innerHTML = this.innerHTML; + element.style.cssText = 'position: relative; overflow: visible;'; + element.onmouseover = () => { + // Prepare submenu + this.submenu = document.createElement('div'); + this.submenu.className = 'sub-menu'; + this.submenu.style.cssText = `position: absolute; left: 105%; top: 0px;`; + this.items.forEach(item => this.submenu.appendChild(item)); - return element; + // hover effect + element.parentElement + .querySelectorAll('.sub-menu') + .forEach(sub => sub.remove()); + element.appendChild(this.submenu); + }; + return element; + } } -export function nextLayout() { - const element = document.createElement('div'); - element.className = 'menu-item'; - element.innerHTML = 'Next Layout (x)'; - element.onclick = () => this.utils.switchToNextLayout(); +export const pickMapItem = dumbymap => + new Folder({ + innerHTML: 'Focus a Map(Tab)', + items: dumbymap.utils.renderedMaps().map( + map => + new Item({ + text: map.id, + onclick: () => map.classList.add('focus'), + }).element, + ), + }).element; - return element; -} +export const pickBlockItem = dumbymap => + new Folder({ + innerHTML: 'Focus Block(n/p)', + items: dumbymap.blocks.map( + (block, index) => + new Item({ + text: `Block ${index}`, + onclick: () => block.classList.add('focus'), + }).element, + ), + }).element; + +export const pickLayoutItem = dumbymap => + new Folder({ + innerHTML: 'Switch Layout(x)', + items: [ + new Item({ + text: 'EDIT', + onclick: () => document.body.setAttribute('data-mode', 'editing'), + }).element, + ...dumbymap.layouts.map( + layout => + new Item({ + text: layout.name, + onclick: () => + dumbymap.container.setAttribute('data-layout', layout.name), + }).element, + ), + ], + }).element; export class GeoLink { constructor({ range }) { diff --git a/src/css/index.css b/src/css/index.css index 221ee69..3133c5f 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -52,7 +52,6 @@ body { box-sizing: border-box; flex-direction: column; align-items: stretch; - height: 100%; gap: 0.5em; @@ -124,10 +123,9 @@ body { .container__suggestion { display: flex; + overflow: hidden; justify-content: space-between; align-items: center; - - overflow: hidden; height: fit-content; cursor: pointer; @@ -167,13 +165,16 @@ body { .menu-item { display: flex; + box-sizing: border-box; justify-content: space-between; - padding: 0.5rem; z-index: 9999; + text-wrap: nowrap; cursor: pointer; + border: 2px solid transparent; + border-radius: 5px; &:hover { background: rgb(226 232 240); @@ -181,6 +182,30 @@ body { .info { color: steelblue; + padding-inline: 1em; font-weight: bold; } } + +.folder::after { + content: '⏵'; +} + +.sub-menu { + width: fit-content; + + position: absolute; + z-index: 100; + + border: 2px solid gray; + border-radius: 6px; + + background: white; + min-width: 6rem; + max-height: 40vh; + .menu-item { + margin: 0 auto; + padding-inline: 0.5em; + min-width: 5em; + } +} diff --git a/src/dumbyUtils.mjs b/src/dumbyUtils.mjs index cfac00b..140d671 100644 --- a/src/dumbyUtils.mjs +++ b/src/dumbyUtils.mjs @@ -1,17 +1,12 @@ export function focusNextMap(reverse = false) { - const renderedList = Array.from( - this.htmlHolder.querySelectorAll('[data-render=fulfilled]'), - ); + const renderedList = this.utils.renderedMaps(); + const mapNum = renderedList.length; if (mapNum === 0) return; // Get current focused map element 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')); - // Get next existing map element const padding = reverse ? -1 : 1; let nextIndex = currentFocus diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs index dc22021..faa0621 100644 --- a/src/dumbymap.mjs +++ b/src/dumbymap.mjs @@ -153,7 +153,7 @@ export const generateMaps = (container, { delay, mapCallback }) => { const showcase = document.createElement('div'); container.appendChild(showcase); showcase.classList.add('Showcase'); - const renderMaps = []; + const renderPromises = []; const dumbymap = { layouts, @@ -161,8 +161,11 @@ export const generateMaps = (container, { delay, mapCallback }) => { htmlHolder, showcase, blocks, - renderMaps: renderMaps, utils: { + renderedMaps: () => + Array.from( + container.querySelectorAll('.mapclay[data-render=fulfilled]'), + ), focusNextMap: throttle(utils.focusNextMap, utils.focusDelay), switchToNextLayout: throttle(utils.switchToNextLayout, 300), focusNextBlock: utils.focusNextBlock, @@ -259,10 +262,6 @@ export const generateMaps = (container, { delay, mapCallback }) => { ); }; //}}} - // Draggable Blocks {{{ - // Add draggable part for blocks - - // }}} // CSS observer {{{ // Focus Map {{{ // Set focusArea @@ -282,6 +281,13 @@ export const generateMaps = (container, { delay, mapCallback }) => { visibilityProperty: true, }); + if (focus) { + dumbymap.utils + .renderedMaps() + .filter(map => map !== target) + .forEach(map => map.classList.remove('focus')); + } + if (shouldBeInShowcase) { if (showcase.contains(target)) return; @@ -448,7 +454,7 @@ export const generateMaps = (container, { delay, mapCallback }) => { // FIXME HACK use MutationObserver for animation if (!target.animations) target.animations = Promise.resolve(); target.animations = target.animations.then(async () => { - await new Promise(resolve => setTimeout(resolve, 150)); + await new Promise(resolve => setTimeout(resolve, 100)); target.setAttribute('data-report', passNum); }); }; @@ -505,9 +511,9 @@ export const generateMaps = (container, { delay, mapCallback }) => { // Render maps with delay const timer = setTimeout( () => - render(target, configList).forEach(renderMap => { - renderMaps.push(renderMap); - renderMap.then(afterMapRendered); + render(target, configList).forEach(renderPromise => { + renderPromises.push(renderPromise); + renderPromise.then(afterMapRendered); }), delay ?? 1000, ); diff --git a/src/editor.mjs b/src/editor.mjs index f965b56..9946686 100644 --- a/src/editor.mjs +++ b/src/editor.mjs @@ -10,13 +10,17 @@ const HtmlContainer = document.querySelector('.DumbyMap'); const textArea = document.querySelector('.editor textarea'); let dumbymap; -const toggleEditing = () => { +new MutationObserver(() => { if (document.body.getAttribute('data-mode') === 'editing') { - document.body.removeAttribute('data-mode'); - } else { - document.body.setAttribute('data-mode', 'editing'); + HtmlContainer.setAttribute('data-layout', 'normal'); } - HtmlContainer.setAttribute('data-layout', 'normal'); +}).observe(document.body, { + attributes: true, + attributeFilter: ['data-mode'], +}); +const toggleEditing = () => { + const mode = document.body.getAttribute('data-mode'); + document.body.setAttribute('data-mode', mode === 'editing' ? '' : 'editing'); }; // }}} // Set up EasyMDE {{{ @@ -274,6 +278,15 @@ window.onhashchange = () => { const menu = document.createElement('div'); menu.id = 'menu'; menu.onclick = () => (menu.style.display = 'none'); +new MutationObserver(() => { + if (menu.style.display === 'none') { + menu.style.cssText = ''; + menu.replaceChildren(); + } +}).observe(menu, { + attributes: true, + attributeFilter: ['style'], +}); document.body.append(menu); const rendererOptions = {}; @@ -657,13 +670,13 @@ document.oncontextmenu = e => { 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()); } - menu.appendChild(menuItem.nextMap.bind(dumbymap)()); - menu.appendChild(menuItem.nextBlock.bind(dumbymap)()); - menu.appendChild(menuItem.nextLayout.bind(dumbymap)()); + menu.style.cssText = `overflow: visible; display: block; left: ${e.clientX + 10}px; top: ${e.clientY + 5}px;`; + menu.appendChild(menuItem.pickMapItem(dumbymap)); + menu.appendChild(menuItem.pickBlockItem(dumbymap)); + menu.appendChild(menuItem.pickLayoutItem(dumbymap)); }; const actionOutsideMenu = e => { -- cgit v1.2.3-70-g09d2