aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorHsieh Chin Fan <pham@topo.tw>2024-09-29 21:01:03 +0800
committerHsieh Chin Fan <pham@topo.tw>2024-09-29 21:19:07 +0800
commit27feb1302304eede3cdc58ffde5ce8e0f0019da4 (patch)
tree0db88a3c17f4761470d0b3e39b3ad16e36fbe2c5 /src
parent110d8890e171573adad1fe171a122dfbc89f6faa (diff)
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
Diffstat (limited to 'src')
-rw-r--r--src/MenuItem.mjs101
-rw-r--r--src/css/index.css33
-rw-r--r--src/dumbyUtils.mjs9
-rw-r--r--src/dumbymap.mjs26
-rw-r--r--src/editor.mjs31
5 files changed, 151 insertions, 49 deletions
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 @@
1import { createGeoLink } from './dumbymap'; 1import { createGeoLink } from './dumbymap';
2 2
3export function nextMap() { 3class Item {
4 const element = document.createElement('div'); 4 constructor({ text, innerHTML, onclick }) {
5 element.className = 'menu-item'; 5 this.text = text;
6 element.innerHTML = 'Next Map <span class="info">(Tab)</span>'; 6 this.innerHTML = innerHTML;
7 element.onclick = () => this.utils.focusNextMap(); 7 this.onclick = onclick;
8 }
8 9
9 return element; 10 get element() {
11 const element = document.createElement('div');
12 element.innerHTML = this.innerHTML ? this.innerHTML : this.text;
13 element.classList.add('menu-item');
14 element.onclick = this.onclick;
15 return element;
16 }
10} 17}
11 18
12export function nextBlock() { 19class Folder {
13 const element = document.createElement('div'); 20 constructor({ text, innerHTML, items }) {
14 element.className = 'menu-item'; 21 this.text = text;
15 element.innerHTML = 'Next Block <span class="info">(n)</span>'; 22 this.innerHTML = innerHTML;
16 element.onclick = () => this.utils.focusNextBlock(); 23 this.items = items;
24 this.utils;
25 }
26
27 get element() {
28 const element = document.createElement('div');
29 element.classList.add(this.className);
30 element.className = 'menu-item folder';
31 element.innerHTML = this.innerHTML;
32 element.style.cssText = 'position: relative; overflow: visible;';
33 element.onmouseover = () => {
34 // Prepare submenu
35 this.submenu = document.createElement('div');
36 this.submenu.className = 'sub-menu';
37 this.submenu.style.cssText = `position: absolute; left: 105%; top: 0px;`;
38 this.items.forEach(item => this.submenu.appendChild(item));
17 39
18 return element; 40 // hover effect
41 element.parentElement
42 .querySelectorAll('.sub-menu')
43 .forEach(sub => sub.remove());
44 element.appendChild(this.submenu);
45 };
46 return element;
47 }
19} 48}
20 49
21export function nextLayout() { 50export const pickMapItem = dumbymap =>
22 const element = document.createElement('div'); 51 new Folder({
23 element.className = 'menu-item'; 52 innerHTML: '<span>Focus a Map<span><span class="info">(Tab)</span>',
24 element.innerHTML = 'Next Layout <span class="info">(x)</span>'; 53 items: dumbymap.utils.renderedMaps().map(
25 element.onclick = () => this.utils.switchToNextLayout(); 54 map =>
55 new Item({
56 text: map.id,
57 onclick: () => map.classList.add('focus'),
58 }).element,
59 ),
60 }).element;
26 61
27 return element; 62export const pickBlockItem = dumbymap =>
28} 63 new Folder({
64 innerHTML: '<span>Focus Block<span><span class="info">(n/p)</span>',
65 items: dumbymap.blocks.map(
66 (block, index) =>
67 new Item({
68 text: `Block ${index}`,
69 onclick: () => block.classList.add('focus'),
70 }).element,
71 ),
72 }).element;
73
74export const pickLayoutItem = dumbymap =>
75 new Folder({
76 innerHTML: '<span>Switch Layout<span><span class="info">(x)</span>',
77 items: [
78 new Item({
79 text: 'EDIT',
80 onclick: () => document.body.setAttribute('data-mode', 'editing'),
81 }).element,
82 ...dumbymap.layouts.map(
83 layout =>
84 new Item({
85 text: layout.name,
86 onclick: () =>
87 dumbymap.container.setAttribute('data-layout', layout.name),
88 }).element,
89 ),
90 ],
91 }).element;
29 92
30export class GeoLink { 93export class GeoLink {
31 constructor({ range }) { 94 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 {
52 box-sizing: border-box; 52 box-sizing: border-box;
53 flex-direction: column; 53 flex-direction: column;
54 align-items: stretch; 54 align-items: stretch;
55
56 height: 100%; 55 height: 100%;
57 gap: 0.5em; 56 gap: 0.5em;
58 57
@@ -124,10 +123,9 @@ body {
124 123
125.container__suggestion { 124.container__suggestion {
126 display: flex; 125 display: flex;
126 overflow: hidden;
127 justify-content: space-between; 127 justify-content: space-between;
128 align-items: center; 128 align-items: center;
129
130 overflow: hidden;
131 height: fit-content; 129 height: fit-content;
132 130
133 cursor: pointer; 131 cursor: pointer;
@@ -167,13 +165,16 @@ body {
167 165
168.menu-item { 166.menu-item {
169 display: flex; 167 display: flex;
168 box-sizing: border-box;
170 justify-content: space-between; 169 justify-content: space-between;
171
172 padding: 0.5rem; 170 padding: 0.5rem;
173 171
174 z-index: 9999; 172 z-index: 9999;
173 text-wrap: nowrap;
175 174
176 cursor: pointer; 175 cursor: pointer;
176 border: 2px solid transparent;
177 border-radius: 5px;
177 178
178 &:hover { 179 &:hover {
179 background: rgb(226 232 240); 180 background: rgb(226 232 240);
@@ -181,6 +182,30 @@ body {
181 182
182 .info { 183 .info {
183 color: steelblue; 184 color: steelblue;
185 padding-inline: 1em;
184 font-weight: bold; 186 font-weight: bold;
185 } 187 }
186} 188}
189
190.folder::after {
191 content: '⏵';
192}
193
194.sub-menu {
195 width: fit-content;
196
197 position: absolute;
198 z-index: 100;
199
200 border: 2px solid gray;
201 border-radius: 6px;
202
203 background: white;
204 min-width: 6rem;
205 max-height: 40vh;
206 .menu-item {
207 margin: 0 auto;
208 padding-inline: 0.5em;
209 min-width: 5em;
210 }
211}
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 @@
1export function focusNextMap(reverse = false) { 1export function focusNextMap(reverse = false) {
2 const renderedList = Array.from( 2 const renderedList = this.utils.renderedMaps();
3 this.htmlHolder.querySelectorAll('[data-render=fulfilled]'), 3
4 );
5 const mapNum = renderedList.length; 4 const mapNum = renderedList.length;
6 if (mapNum === 0) return; 5 if (mapNum === 0) return;
7 6
8 // Get current focused map element 7 // Get current focused map element
9 const currentFocus = this.container.querySelector('.mapclay.focus'); 8 const currentFocus = this.container.querySelector('.mapclay.focus');
10 9
11 // Remove class name of focus for ALL candidates
12 // This may trigger animation
13 renderedList.forEach(ele => ele.classList.remove('focus'));
14
15 // Get next existing map element 10 // Get next existing map element
16 const padding = reverse ? -1 : 1; 11 const padding = reverse ? -1 : 1;
17 let nextIndex = currentFocus 12 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 }) => {
153 const showcase = document.createElement('div'); 153 const showcase = document.createElement('div');
154 container.appendChild(showcase); 154 container.appendChild(showcase);
155 showcase.classList.add('Showcase'); 155 showcase.classList.add('Showcase');
156 const renderMaps = []; 156 const renderPromises = [];
157 157
158 const dumbymap = { 158 const dumbymap = {
159 layouts, 159 layouts,
@@ -161,8 +161,11 @@ export const generateMaps = (container, { delay, mapCallback }) => {
161 htmlHolder, 161 htmlHolder,
162 showcase, 162 showcase,
163 blocks, 163 blocks,
164 renderMaps: renderMaps,
165 utils: { 164 utils: {
165 renderedMaps: () =>
166 Array.from(
167 container.querySelectorAll('.mapclay[data-render=fulfilled]'),
168 ),
166 focusNextMap: throttle(utils.focusNextMap, utils.focusDelay), 169 focusNextMap: throttle(utils.focusNextMap, utils.focusDelay),
167 switchToNextLayout: throttle(utils.switchToNextLayout, 300), 170 switchToNextLayout: throttle(utils.switchToNextLayout, 300),
168 focusNextBlock: utils.focusNextBlock, 171 focusNextBlock: utils.focusNextBlock,
@@ -259,10 +262,6 @@ export const generateMaps = (container, { delay, mapCallback }) => {
259 ); 262 );
260 }; 263 };
261 //}}} 264 //}}}
262 // Draggable Blocks {{{
263 // Add draggable part for blocks
264
265 // }}}
266 // CSS observer {{{ 265 // CSS observer {{{
267 // Focus Map {{{ 266 // Focus Map {{{
268 // Set focusArea 267 // Set focusArea
@@ -282,6 +281,13 @@ export const generateMaps = (container, { delay, mapCallback }) => {
282 visibilityProperty: true, 281 visibilityProperty: true,
283 }); 282 });
284 283
284 if (focus) {
285 dumbymap.utils
286 .renderedMaps()
287 .filter(map => map !== target)
288 .forEach(map => map.classList.remove('focus'));
289 }
290
285 if (shouldBeInShowcase) { 291 if (shouldBeInShowcase) {
286 if (showcase.contains(target)) return; 292 if (showcase.contains(target)) return;
287 293
@@ -448,7 +454,7 @@ export const generateMaps = (container, { delay, mapCallback }) => {
448 // FIXME HACK use MutationObserver for animation 454 // FIXME HACK use MutationObserver for animation
449 if (!target.animations) target.animations = Promise.resolve(); 455 if (!target.animations) target.animations = Promise.resolve();
450 target.animations = target.animations.then(async () => { 456 target.animations = target.animations.then(async () => {
451 await new Promise(resolve => setTimeout(resolve, 150)); 457 await new Promise(resolve => setTimeout(resolve, 100));
452 target.setAttribute('data-report', passNum); 458 target.setAttribute('data-report', passNum);
453 }); 459 });
454 }; 460 };
@@ -505,9 +511,9 @@ export const generateMaps = (container, { delay, mapCallback }) => {
505 // Render maps with delay 511 // Render maps with delay
506 const timer = setTimeout( 512 const timer = setTimeout(
507 () => 513 () =>
508 render(target, configList).forEach(renderMap => { 514 render(target, configList).forEach(renderPromise => {
509 renderMaps.push(renderMap); 515 renderPromises.push(renderPromise);
510 renderMap.then(afterMapRendered); 516 renderPromise.then(afterMapRendered);
511 }), 517 }),
512 delay ?? 1000, 518 delay ?? 1000,
513 ); 519 );
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');
10const textArea = document.querySelector('.editor textarea'); 10const textArea = document.querySelector('.editor textarea');
11let dumbymap; 11let dumbymap;
12 12
13const toggleEditing = () => { 13new MutationObserver(() => {
14 if (document.body.getAttribute('data-mode') === 'editing') { 14 if (document.body.getAttribute('data-mode') === 'editing') {
15 document.body.removeAttribute('data-mode'); 15 HtmlContainer.setAttribute('data-layout', 'normal');
16 } else {
17 document.body.setAttribute('data-mode', 'editing');
18 } 16 }
19 HtmlContainer.setAttribute('data-layout', 'normal'); 17}).observe(document.body, {
18 attributes: true,
19 attributeFilter: ['data-mode'],
20});
21const toggleEditing = () => {
22 const mode = document.body.getAttribute('data-mode');
23 document.body.setAttribute('data-mode', mode === 'editing' ? '' : 'editing');
20}; 24};
21// }}} 25// }}}
22// Set up EasyMDE {{{ 26// Set up EasyMDE {{{
@@ -274,6 +278,15 @@ window.onhashchange = () => {
274const menu = document.createElement('div'); 278const menu = document.createElement('div');
275menu.id = 'menu'; 279menu.id = 'menu';
276menu.onclick = () => (menu.style.display = 'none'); 280menu.onclick = () => (menu.style.display = 'none');
281new MutationObserver(() => {
282 if (menu.style.display === 'none') {
283 menu.style.cssText = '';
284 menu.replaceChildren();
285 }
286}).observe(menu, {
287 attributes: true,
288 attributeFilter: ['style'],
289});
277document.body.append(menu); 290document.body.append(menu);
278 291
279const rendererOptions = {}; 292const rendererOptions = {};
@@ -657,13 +670,13 @@ document.oncontextmenu = e => {
657 if (selection) { 670 if (selection) {
658 e.preventDefault(); 671 e.preventDefault();
659 menu.innerHTML = ''; 672 menu.innerHTML = '';
660 menu.style.cssText = `display: block; left: ${e.clientX + 10}px; top: ${e.clientY + 5}px;`;
661 const addGeoLink = new menuItem.GeoLink({ range }); 673 const addGeoLink = new menuItem.GeoLink({ range });
662 menu.appendChild(addGeoLink.createElement()); 674 menu.appendChild(addGeoLink.createElement());
663 } 675 }
664 menu.appendChild(menuItem.nextMap.bind(dumbymap)()); 676 menu.style.cssText = `overflow: visible; display: block; left: ${e.clientX + 10}px; top: ${e.clientY + 5}px;`;
665 menu.appendChild(menuItem.nextBlock.bind(dumbymap)()); 677 menu.appendChild(menuItem.pickMapItem(dumbymap));
666 menu.appendChild(menuItem.nextLayout.bind(dumbymap)()); 678 menu.appendChild(menuItem.pickBlockItem(dumbymap));
679 menu.appendChild(menuItem.pickLayoutItem(dumbymap));
667}; 680};
668 681
669const actionOutsideMenu = e => { 682const actionOutsideMenu = e => {