diff options
author | Hsieh Chin Fan <pham@topo.tw> | 2024-10-02 10:12:46 +0800 |
---|---|---|
committer | Hsieh Chin Fan <pham@topo.tw> | 2024-10-02 10:12:46 +0800 |
commit | 1c7e77b8546ac32b8176ab54dd06ede6ba7deb55 (patch) | |
tree | ca0d50e7b8b3dcbd2cc8891786dca32f1face931 | |
parent | b7e898bc9022a6d7caef705ee2764c139ec86299 (diff) |
style: switch to standardjs
-rw-r--r-- | eslint.config.js | 36 | ||||
-rw-r--r-- | package.json | 14 | ||||
-rw-r--r-- | rollup.config.js | 54 | ||||
-rw-r--r-- | src/Layout.mjs | 231 | ||||
-rw-r--r-- | src/MenuItem.mjs | 248 | ||||
-rw-r--r-- | src/dumbyUtils.mjs | 130 | ||||
-rw-r--r-- | src/dumbymap.mjs | 469 | ||||
-rw-r--r-- | src/editor.mjs | 548 | ||||
-rw-r--r-- | src/utils.mjs | 67 |
9 files changed, 895 insertions, 902 deletions
diff --git a/eslint.config.js b/eslint.config.js index 7ac6782..3bca729 100644 --- a/eslint.config.js +++ b/eslint.config.js | |||
@@ -1,16 +1,16 @@ | |||
1 | import globals from "globals"; | 1 | import globals from 'globals' |
2 | import js from "@eslint/js"; | 2 | import js from '@eslint/js' |
3 | import importPlugin from "eslint-plugin-import"; | 3 | import importPlugin from 'eslint-plugin-import' |
4 | import promisePlugin from "eslint-plugin-promise"; | 4 | import promisePlugin from 'eslint-plugin-promise' |
5 | import nodePlugin from "eslint-plugin-node"; | 5 | import nodePlugin from 'eslint-plugin-node' |
6 | import jsdoc from 'eslint-plugin-jsdoc'; | 6 | import jsdoc from 'eslint-plugin-jsdoc' |
7 | 7 | ||
8 | export default [ | 8 | export default [ |
9 | js.configs.recommended, | 9 | js.configs.recommended, |
10 | { | 10 | { |
11 | languageOptions: { | 11 | languageOptions: { |
12 | ecmaVersion: 2022, | 12 | ecmaVersion: 2022, |
13 | sourceType: "module", | 13 | sourceType: 'module', |
14 | globals: { | 14 | globals: { |
15 | ...globals.browser, | 15 | ...globals.browser, |
16 | ...globals.node | 16 | ...globals.node |
@@ -19,31 +19,31 @@ export default [ | |||
19 | plugins: { | 19 | plugins: { |
20 | import: importPlugin, | 20 | import: importPlugin, |
21 | promise: promisePlugin, | 21 | promise: promisePlugin, |
22 | node: nodePlugin, | 22 | node: nodePlugin |
23 | }, | 23 | }, |
24 | rules: { | 24 | rules: { |
25 | 'no-unused-vars': ['warn', { | 25 | 'no-unused-vars': ['warn', { |
26 | 'varsIgnorePattern': '^_', | 26 | varsIgnorePattern: '^_', |
27 | "argsIgnorePattern": "^_", | 27 | argsIgnorePattern: '^_', |
28 | "caughtErrorsIgnorePattern": "^ignore|_", | 28 | caughtErrorsIgnorePattern: '^ignore|_' |
29 | }], | 29 | }], |
30 | 'import/no-unresolved': 'error', | 30 | 'import/no-unresolved': 'error', |
31 | 'no-console': ["error", { allow: ["info", "warn", "error"] }], | 31 | 'no-console': ['error', { allow: ['info', 'warn', 'error'] }], |
32 | 'eqeqeq': ['error', 'always'], | 32 | eqeqeq: ['error', 'always'], |
33 | // 'curly': ['warn', 'multi'], | 33 | // 'curly': ['warn', 'multi'], |
34 | 'prefer-const': 'error', | 34 | 'prefer-const': 'error', |
35 | 'no-var': 'error', | 35 | 'no-var': 'error', |
36 | 'array-callback-return': 'error', | 36 | 'array-callback-return': 'error', |
37 | 'no-unexpected-multiline': 'warn', | 37 | 'no-unexpected-multiline': 'warn' |
38 | }, | 38 | } |
39 | }, | 39 | }, |
40 | { | 40 | { |
41 | files: ['src/*js'], | 41 | files: ['src/*js'], |
42 | plugins: { | 42 | plugins: { |
43 | jsdoc, | 43 | jsdoc |
44 | }, | 44 | }, |
45 | rules: { | 45 | rules: { |
46 | 'jsdoc/require-description': 'warn' | 46 | 'jsdoc/require-description': 'warn' |
47 | } | 47 | } |
48 | }, | 48 | } |
49 | ]; | 49 | ] |
diff --git a/package.json b/package.json index 9798d45..918a710 100644 --- a/package.json +++ b/package.json | |||
@@ -25,26 +25,20 @@ | |||
25 | "build-resources": "cp node_modules/mapclay/assets/default.yml dist; cp node_modules/easymde/dist/easymde.min.js dist; cp node_modules/easymde/dist/easymde.min.css dist/css", | 25 | "build-resources": "cp node_modules/mapclay/assets/default.yml dist; cp node_modules/easymde/dist/easymde.min.js dist; cp node_modules/easymde/dist/easymde.min.css dist/css", |
26 | "server": "live-server --port=8080 --ignore='**/src/**js' --wait=2000 --no-browser --cors", | 26 | "server": "live-server --port=8080 --ignore='**/src/**js' --wait=2000 --no-browser --cors", |
27 | "dev": "npm run server", | 27 | "dev": "npm run server", |
28 | "lint": "npx eslint src", | 28 | "lint": "npx standard", |
29 | "style": "scripts/stylelint.sh", | 29 | "style": "scripts/stylelint.sh", |
30 | "prepack": "npm run lint && npm run style && npm run build", | 30 | "prepack": "npm run lint && npm run style && npm run build", |
31 | "postpack": "rm -rf dist/css dist/renderers; ln -sf `pwd`/src/css dist; cp node_modules/easymde/dist/easymde.min.css src/css; ln -sf `pwd`/mapclay/dist/renderers dist" | 31 | "postpack": "rm -rf dist/css dist/renderers; ln -sf `pwd`/src/css dist; cp node_modules/easymde/dist/easymde.min.css src/css; ln -sf `pwd`/mapclay/dist/renderers dist" |
32 | }, | 32 | }, |
33 | "devDependencies": { | 33 | "devDependencies": { |
34 | "@eslint/compat": "^1.1.1", | ||
35 | "@eslint/js": "^9.11.1", | ||
36 | "@rollup/plugin-alias": "^5.1.1", | 34 | "@rollup/plugin-alias": "^5.1.1", |
37 | "@rollup/plugin-commonjs": "^26.0.3", | 35 | "@rollup/plugin-commonjs": "^26.0.3", |
38 | "@rollup/plugin-node-resolve": "^15.3.0", | 36 | "@rollup/plugin-node-resolve": "^15.3.0", |
39 | "@rollup/plugin-terser": "^0.4.4", | 37 | "@rollup/plugin-terser": "^0.4.4", |
40 | "eslint": "^9.11.1", | 38 | "globals": "^15.10.0", |
41 | "eslint-plugin-import": "^2.30.0", | 39 | "rollup": "^4.23.0", |
42 | "eslint-plugin-jsdoc": "^50.3.0", | ||
43 | "eslint-plugin-node": "^11.1.0", | ||
44 | "eslint-plugin-promise": "^7.1.0", | ||
45 | "globals": "^15.9.0", | ||
46 | "rollup": "^4.22.5", | ||
47 | "rollup-plugin-bundle-stats": "^4.15.1", | 40 | "rollup-plugin-bundle-stats": "^4.15.1", |
41 | "standard": "^17.1.2", | ||
48 | "stylelint": "^16.9.0", | 42 | "stylelint": "^16.9.0", |
49 | "stylelint-config-standard": "^36.0.1", | 43 | "stylelint-config-standard": "^36.0.1", |
50 | "stylelint-order": "^6.0.4" | 44 | "stylelint-order": "^6.0.4" |
diff --git a/rollup.config.js b/rollup.config.js index 75db905..cc42bf0 100644 --- a/rollup.config.js +++ b/rollup.config.js | |||
@@ -1,70 +1,70 @@ | |||
1 | import node from '@rollup/plugin-node-resolve'; | 1 | import node from '@rollup/plugin-node-resolve' |
2 | import commonjs from '@rollup/plugin-commonjs'; | 2 | import commonjs from '@rollup/plugin-commonjs' |
3 | import terser from '@rollup/plugin-terser'; | 3 | import terser from '@rollup/plugin-terser' |
4 | import { existsSync } from 'fs'; | 4 | import { existsSync } from 'fs' |
5 | import { join } from 'path'; | 5 | import { join } from 'path' |
6 | import { bundleStats } from 'rollup-plugin-bundle-stats'; | 6 | import { bundleStats } from 'rollup-plugin-bundle-stats' |
7 | 7 | ||
8 | const production = !process.env.ROLLUP_WATCH; | 8 | const production = !process.env.ROLLUP_WATCH |
9 | 9 | ||
10 | const general = { | 10 | const general = { |
11 | output: [ | 11 | output: [ |
12 | { | 12 | { |
13 | dir: './dist', | 13 | dir: './dist', |
14 | format: 'esm', | 14 | format: 'esm', |
15 | entryFileNames: '[name].mjs', | 15 | entryFileNames: '[name].mjs' |
16 | } | 16 | } |
17 | ], | 17 | ], |
18 | watch: { | 18 | watch: { |
19 | clearScreen: false, | 19 | clearScreen: false, |
20 | include: ["src/**", "mapclay/dist/mapclay.mjs"] | 20 | include: ['src/**', 'mapclay/dist/mapclay.mjs'] |
21 | }, | 21 | }, |
22 | context: "window", | 22 | context: 'window', |
23 | plugins: [ | 23 | plugins: [ |
24 | { | 24 | { |
25 | name: 'watch-mapclay', | 25 | name: 'watch-mapclay', |
26 | buildStart() { | 26 | buildStart () { |
27 | const mapclayPath = join(process.cwd(), 'mapclay', 'dist', 'mapclay.mjs'); | 27 | const mapclayPath = join(process.cwd(), 'mapclay', 'dist', 'mapclay.mjs') |
28 | console.log('Watching:', mapclayPath); | 28 | console.log('Watching:', mapclayPath) |
29 | if (existsSync(mapclayPath)) { | 29 | if (existsSync(mapclayPath)) { |
30 | this.addWatchFile(mapclayPath); | 30 | this.addWatchFile(mapclayPath) |
31 | } else { | 31 | } else { |
32 | console.log('mapclay.mjs not found at:', mapclayPath); | 32 | console.log('mapclay.mjs not found at:', mapclayPath) |
33 | } | 33 | } |
34 | } | 34 | } |
35 | }, | 35 | }, |
36 | { | 36 | { |
37 | name: 'leader-line', | 37 | name: 'leader-line', |
38 | transform(code, id) { | 38 | transform (code, id) { |
39 | if (id.includes('node_modules/leader-line/')) { | 39 | if (id.includes('node_modules/leader-line/')) { |
40 | return `${code}\nexport default LeaderLine;`; | 40 | return `${code}\nexport default LeaderLine;` |
41 | } | 41 | } |
42 | return null; | 42 | return null |
43 | }, | 43 | } |
44 | }, | 44 | }, |
45 | { | 45 | { |
46 | name: 'mapclay', | 46 | name: 'mapclay', |
47 | resolveId(source) { | 47 | resolveId (source) { |
48 | if (source === 'mapclay' && existsSync(join('.', 'mapclay'))) { | 48 | if (source === 'mapclay' && existsSync(join('.', 'mapclay'))) { |
49 | return './mapclay/dist/mapclay.mjs'; | 49 | return './mapclay/dist/mapclay.mjs' |
50 | } | 50 | } |
51 | return null; | 51 | return null |
52 | } | 52 | } |
53 | }, | 53 | }, |
54 | node(), | 54 | node(), |
55 | commonjs(), | 55 | commonjs(), |
56 | production && terser(), | 56 | production && terser(), |
57 | // bundleStats(), | 57 | production && bundleStats() |
58 | ], | 58 | ] |
59 | } | 59 | } |
60 | 60 | ||
61 | export default [ | 61 | export default [ |
62 | { | 62 | { |
63 | input: "src/editor.mjs", | 63 | input: 'src/editor.mjs' |
64 | }, | 64 | }, |
65 | { | 65 | { |
66 | input: "src/dumbymap.mjs", | 66 | input: 'src/dumbymap.mjs' |
67 | }, | 67 | } |
68 | ] | 68 | ] |
69 | .map(config => ({ ...general, ...config })) | 69 | .map(config => ({ ...general, ...config })) |
70 | .filter((config) => production || config.input.match(/editor/)) | 70 | .filter((config) => production || config.input.match(/editor/)) |
diff --git a/src/Layout.mjs b/src/Layout.mjs index 3a835c9..89a48ae 100644 --- a/src/Layout.mjs +++ b/src/Layout.mjs | |||
@@ -1,208 +1,209 @@ | |||
1 | import PlainDraggable from 'plain-draggable'; | 1 | import PlainDraggable from 'plain-draggable' |
2 | import { onRemove, animateRectTransition } from './utils'; | 2 | import { onRemove, animateRectTransition } from './utils' |
3 | 3 | ||
4 | export class Layout { | 4 | export class Layout { |
5 | constructor(options = {}) { | 5 | constructor (options = {}) { |
6 | if (!options.name) throw Error('Layout name is not given'); | 6 | if (!options.name) throw Error('Layout name is not given') |
7 | this.name = options.name; | 7 | this.name = options.name |
8 | this.enterHandler = options.enterHandler; | 8 | this.enterHandler = options.enterHandler |
9 | this.leaveHandler = options.leaveHandler; | 9 | this.leaveHandler = options.leaveHandler |
10 | } | 10 | } |
11 | valueOf = () => this.name; | 11 | |
12 | valueOf = () => this.name | ||
12 | } | 13 | } |
13 | 14 | ||
14 | export class SideBySide extends Layout { | 15 | export class SideBySide extends Layout { |
15 | name = 'side-by-side'; | 16 | name = 'side-by-side' |
16 | 17 | ||
17 | enterHandler = ({ container, htmlHolder, showcase }) => { | 18 | enterHandler = ({ container, htmlHolder, showcase }) => { |
18 | const bar = document.createElement('div'); | 19 | const bar = document.createElement('div') |
19 | bar.className = 'bar'; | 20 | bar.className = 'bar' |
20 | bar.innerHTML = '<div class="bar-handle"></div>'; | 21 | bar.innerHTML = '<div class="bar-handle"></div>' |
21 | const handle = bar.querySelector('.bar-handle'); | 22 | const handle = bar.querySelector('.bar-handle') |
22 | container.appendChild(bar); | 23 | container.appendChild(bar) |
23 | 24 | ||
24 | // Resize views by value | 25 | // Resize views by value |
25 | const resizeByLeft = left => { | 26 | const resizeByLeft = left => { |
26 | htmlHolder.style.width = left + 'px'; | 27 | htmlHolder.style.width = left + 'px' |
27 | showcase.style.width = | 28 | showcase.style.width = |
28 | parseFloat(getComputedStyle(container).width) - left + 'px'; | 29 | parseFloat(window.getComputedStyle(container).width) - left + 'px' |
29 | }; | 30 | } |
30 | 31 | ||
31 | const draggable = new PlainDraggable(bar, { | 32 | const draggable = new PlainDraggable(bar, { |
32 | handle: handle, | 33 | handle, |
33 | containment: { left: '25%', top: 0, right: '75%', height: 0 }, | 34 | containment: { left: '25%', top: 0, right: '75%', height: 0 } |
34 | }); | 35 | }) |
35 | draggable.draggableCursor = 'grab'; | 36 | draggable.draggableCursor = 'grab' |
36 | 37 | ||
37 | draggable.onDrag = pos => { | 38 | draggable.onDrag = pos => { |
38 | handle.style.transform = 'unset'; | 39 | handle.style.transform = 'unset' |
39 | resizeByLeft(pos.left); | 40 | resizeByLeft(pos.left) |
40 | }; | 41 | } |
41 | draggable.onDragEnd = _ => { | 42 | draggable.onDragEnd = _ => { |
42 | handle.removeAttribute('style'); | 43 | handle.removeAttribute('style') |
43 | }; | 44 | } |
44 | 45 | ||
45 | onRemove(bar, () => draggable.remove()); | 46 | onRemove(bar, () => draggable.remove()) |
46 | }; | 47 | } |
47 | 48 | ||
48 | leaveHandler = ({ container }) => { | 49 | leaveHandler = ({ container }) => { |
49 | container.querySelector('.bar')?.remove(); | 50 | container.querySelector('.bar')?.remove() |
50 | }; | 51 | } |
51 | } | 52 | } |
52 | 53 | ||
53 | export class Overlay extends Layout { | 54 | export class Overlay extends Layout { |
54 | name = 'overlay'; | 55 | name = 'overlay' |
55 | 56 | ||
56 | saveLeftTopAsData = element => { | 57 | saveLeftTopAsData = element => { |
57 | const { left, top } = element.getBoundingClientRect(); | 58 | const { left, top } = element.getBoundingClientRect() |
58 | element.setAttribute('data-left', left); | 59 | element.setAttribute('data-left', left) |
59 | element.setAttribute('data-top', top); | 60 | element.setAttribute('data-top', top) |
60 | }; | 61 | } |
61 | 62 | ||
62 | addDraggable = element => { | 63 | addDraggable = element => { |
63 | // Make sure current element always on top | 64 | // Make sure current element always on top |
64 | const siblings = Array.from( | 65 | const siblings = Array.from( |
65 | element.parentElement?.querySelectorAll(':scope > *') ?? [], | 66 | element.parentElement?.querySelectorAll(':scope > *') ?? [] |
66 | ); | 67 | ) |
67 | let popTimer = null; | 68 | let popTimer = null |
68 | element.onmouseover = () => { | 69 | element.onmouseover = () => { |
69 | popTimer = setTimeout(() => { | 70 | popTimer = setTimeout(() => { |
70 | siblings.forEach(e => e.style.removeProperty('z-index')); | 71 | siblings.forEach(e => e.style.removeProperty('z-index')) |
71 | element.style.zIndex = '9001'; | 72 | element.style.zIndex = '9001' |
72 | }, 200); | 73 | }, 200) |
73 | }; | 74 | } |
74 | element.onmouseout = () => { | 75 | element.onmouseout = () => { |
75 | clearTimeout(popTimer); | 76 | clearTimeout(popTimer) |
76 | }; | 77 | } |
77 | 78 | ||
78 | // Add draggable part | 79 | // Add draggable part |
79 | const draggablePart = document.createElement('div'); | 80 | const draggablePart = document.createElement('div') |
80 | element.appendChild(draggablePart); | 81 | element.appendChild(draggablePart) |
81 | draggablePart.className = 'draggable-part'; | 82 | draggablePart.className = 'draggable-part' |
82 | draggablePart.innerHTML = '<div class="handle">\u2630</div>'; | 83 | draggablePart.innerHTML = '<div class="handle">\u2630</div>' |
83 | 84 | ||
84 | // Add draggable instance | 85 | // Add draggable instance |
85 | const { left, top } = element.getBoundingClientRect(); | 86 | const { left, top } = element.getBoundingClientRect() |
86 | const draggable = new PlainDraggable(element, { | 87 | const draggable = new PlainDraggable(element, { |
87 | top: top, | 88 | top, |
88 | left: left, | 89 | left, |
89 | handle: draggablePart, | 90 | handle: draggablePart, |
90 | snap: { x: { step: 20 }, y: { step: 20 } }, | 91 | snap: { x: { step: 20 }, y: { step: 20 } } |
91 | }); | 92 | }) |
92 | 93 | ||
93 | // FIXME use pure CSS to hide utils | 94 | // FIXME use pure CSS to hide utils |
94 | const utils = element.querySelector('.utils'); | 95 | const utils = element.querySelector('.utils') |
95 | draggable.onDragStart = () => { | 96 | draggable.onDragStart = () => { |
96 | utils.style.display = 'none'; | 97 | utils.style.display = 'none' |
97 | element.classList.add('drag'); | 98 | element.classList.add('drag') |
98 | }; | 99 | } |
99 | 100 | ||
100 | draggable.onDragEnd = () => { | 101 | draggable.onDragEnd = () => { |
101 | utils.style = ''; | 102 | utils.style = '' |
102 | element.classList.remove('drag'); | 103 | element.classList.remove('drag') |
103 | element.style.zIndex = '9000'; | 104 | element.style.zIndex = '9000' |
104 | }; | 105 | } |
105 | 106 | ||
106 | // Reposition draggable instance when resized | 107 | // Reposition draggable instance when resized |
107 | new ResizeObserver(() => { | 108 | new window.ResizeObserver(() => { |
108 | try { | 109 | try { |
109 | draggable.position(); | 110 | draggable.position() |
110 | } catch (_) { | 111 | } catch (err) { |
111 | null; | 112 | console.warn(err) |
112 | } | 113 | } |
113 | }).observe(element); | 114 | }).observe(element) |
114 | 115 | ||
115 | // Callback for remove | 116 | // Callback for remove |
116 | onRemove(element, () => { | 117 | onRemove(element, () => { |
117 | draggable.remove(); | 118 | draggable.remove() |
118 | }); | 119 | }) |
119 | }; | 120 | } |
120 | 121 | ||
121 | enterHandler = ({ htmlHolder, blocks }) => { | 122 | enterHandler = ({ htmlHolder, blocks }) => { |
122 | // FIXME It is weird rect from this method and this scope are different... | 123 | // FIXME It is weird rect from this method and this scope are different... |
123 | blocks.forEach(this.saveLeftTopAsData); | 124 | blocks.forEach(this.saveLeftTopAsData) |
124 | 125 | ||
125 | // Create draggable blocks and set each position by previous one | 126 | // Create draggable blocks and set each position by previous one |
126 | let [left, top] = [20, 20]; | 127 | let [left, top] = [20, 20] |
127 | blocks.forEach(block => { | 128 | blocks.forEach(block => { |
128 | const originLeft = Number(block.getAttribute('data-left')); | 129 | const originLeft = Number(block.getAttribute('data-left')) |
129 | const originTop = Number(block.getAttribute('data-top')); | 130 | const originTop = Number(block.getAttribute('data-top')) |
130 | 131 | ||
131 | // Create draggable block | 132 | // Create draggable block |
132 | const wrapper = document.createElement('div'); | 133 | const wrapper = document.createElement('div') |
133 | wrapper.classList.add('draggable-block'); | 134 | wrapper.classList.add('draggable-block') |
134 | wrapper.innerHTML = ` | 135 | wrapper.innerHTML = ` |
135 | <div class="utils"> | 136 | <div class="utils"> |
136 | <div id="close">\u274C</div> | 137 | <div id="close">\u274C</div> |
137 | <div id="plus-font-size" ">\u2795</div> | 138 | <div id="plus-font-size" ">\u2795</div> |
138 | <div id="minus-font-size">\u2796</div> | 139 | <div id="minus-font-size">\u2796</div> |
139 | </div> | 140 | </div> |
140 | `; | 141 | ` |
141 | wrapper.title = 'Middle-click to hide block'; | 142 | wrapper.title = 'Middle-click to hide block' |
142 | wrapper.onmouseup = e => { | 143 | wrapper.onmouseup = e => { |
143 | // Hide block with middle click | 144 | // Hide block with middle click |
144 | if (e.button === 1) { | 145 | if (e.button === 1) { |
145 | wrapper.classList.add('hide'); | 146 | wrapper.classList.add('hide') |
146 | } | 147 | } |
147 | }; | 148 | } |
148 | 149 | ||
149 | // Set DOMRect for wrapper | 150 | // Set DOMRect for wrapper |
150 | wrapper.appendChild(block); | 151 | wrapper.appendChild(block) |
151 | wrapper.style.left = left + 'px'; | 152 | wrapper.style.left = left + 'px' |
152 | wrapper.style.top = top + 'px'; | 153 | wrapper.style.top = top + 'px' |
153 | htmlHolder.appendChild(wrapper); | 154 | htmlHolder.appendChild(wrapper) |
154 | const { width } = wrapper.getBoundingClientRect(); | 155 | const { width } = wrapper.getBoundingClientRect() |
155 | left += width + 30; | 156 | left += width + 30 |
156 | if (left > window.innerWidth) { | 157 | if (left > window.innerWidth) { |
157 | top += 200; | 158 | top += 200 |
158 | left = left % window.innerWidth; | 159 | left = left % window.innerWidth |
159 | } | 160 | } |
160 | 161 | ||
161 | // Animation for DOMRect | 162 | // Animation for DOMRect |
162 | animateRectTransition( | 163 | animateRectTransition( |
163 | wrapper, | 164 | wrapper, |
164 | { left: originLeft, top: originTop }, | 165 | { left: originLeft, top: originTop }, |
165 | { resume: true, duration: 300 }, | 166 | { resume: true, duration: 300 } |
166 | ).finished.finally(() => this.addDraggable(wrapper)); | 167 | ).finished.finally(() => this.addDraggable(wrapper)) |
167 | 168 | ||
168 | // Trivial case: | 169 | // Trivial case: |
169 | // This hack make sure utils remains at the same place even when wrapper resized | 170 | // This hack make sure utils remains at the same place even when wrapper resized |
170 | // Prevent DOMRect changes when user clicking plus/minus button many times | 171 | // Prevent DOMRect changes when user clicking plus/minus button many times |
171 | const utils = wrapper.querySelector('.utils'); | 172 | const utils = wrapper.querySelector('.utils') |
172 | utils.onmouseover = () => { | 173 | utils.onmouseover = () => { |
173 | const { left, top } = utils.getBoundingClientRect(); | 174 | const { left, top } = utils.getBoundingClientRect() |
174 | utils.style.cssText = `visibility: visible; z-index: 9000; position: fixed; transition: unset; left: ${left}px; top: ${top}px;`; | 175 | utils.style.cssText = `visibility: visible; z-index: 9000; position: fixed; transition: unset; left: ${left}px; top: ${top}px;` |
175 | document.body.appendChild(utils); | 176 | document.body.appendChild(utils) |
176 | }; | 177 | } |
177 | utils.onmouseout = () => { | 178 | utils.onmouseout = () => { |
178 | wrapper.appendChild(utils); | 179 | wrapper.appendChild(utils) |
179 | utils.removeAttribute('style'); | 180 | utils.removeAttribute('style') |
180 | }; | 181 | } |
181 | 182 | ||
182 | // Close button | 183 | // Close button |
183 | wrapper.querySelector('#close').onclick = () => { | 184 | wrapper.querySelector('#close').onclick = () => { |
184 | wrapper.classList.add('hide'); | 185 | wrapper.classList.add('hide') |
185 | utils.removeAttribute('style'); | 186 | utils.removeAttribute('style') |
186 | }; | 187 | } |
187 | // Plus/Minus font-size of content | 188 | // Plus/Minus font-size of content |
188 | wrapper.querySelector('#plus-font-size').onclick = () => { | 189 | wrapper.querySelector('#plus-font-size').onclick = () => { |
189 | const fontSize = parseFloat(getComputedStyle(block).fontSize) / 16; | 190 | const fontSize = parseFloat(window.getComputedStyle(block).fontSize) / 16 |
190 | block.style.fontSize = `${fontSize + 0.2}rem`; | 191 | block.style.fontSize = `${fontSize + 0.2}rem` |
191 | }; | 192 | } |
192 | wrapper.querySelector('#minus-font-size').onclick = () => { | 193 | wrapper.querySelector('#minus-font-size').onclick = () => { |
193 | const fontSize = parseFloat(getComputedStyle(block).fontSize) / 16; | 194 | const fontSize = parseFloat(window.getComputedStyle(block).fontSize) / 16 |
194 | block.style.fontSize = `${fontSize - 0.2}rem`; | 195 | block.style.fontSize = `${fontSize - 0.2}rem` |
195 | }; | 196 | } |
196 | }); | 197 | }) |
197 | }; | 198 | } |
198 | 199 | ||
199 | leaveHandler = ({ htmlHolder, blocks }) => { | 200 | leaveHandler = ({ htmlHolder, blocks }) => { |
200 | const resumeFromDraggable = block => { | 201 | const resumeFromDraggable = block => { |
201 | const draggableContainer = block.closest('.draggable-block'); | 202 | const draggableContainer = block.closest('.draggable-block') |
202 | if (!draggableContainer) return; | 203 | if (!draggableContainer) return |
203 | htmlHolder.appendChild(block); | 204 | htmlHolder.appendChild(block) |
204 | draggableContainer.remove(); | 205 | draggableContainer.remove() |
205 | }; | 206 | } |
206 | blocks.forEach(resumeFromDraggable); | 207 | blocks.forEach(resumeFromDraggable) |
207 | }; | 208 | } |
208 | } | 209 | } |
diff --git a/src/MenuItem.mjs b/src/MenuItem.mjs index 2bf54c2..e1cb582 100644 --- a/src/MenuItem.mjs +++ b/src/MenuItem.mjs | |||
@@ -1,44 +1,44 @@ | |||
1 | class Item extends HTMLDivElement { | 1 | class Item extends window.HTMLDivElement { |
2 | constructor({ text, innerHTML, onclick, style }) { | 2 | constructor ({ text, innerHTML, onclick, style }) { |
3 | super(); | 3 | super() |
4 | this.innerHTML = innerHTML ?? text; | 4 | this.innerHTML = innerHTML ?? text |
5 | this.onclick = onclick; | 5 | this.onclick = onclick |
6 | this.classList.add('menu-item'); | 6 | this.classList.add('menu-item') |
7 | this.style.cssText = style; | 7 | this.style.cssText = style |
8 | 8 | ||
9 | this.onmouseover = () => { | 9 | this.onmouseover = () => { |
10 | this.parentElement | 10 | this.parentElement |
11 | .querySelectorAll('.sub-menu') | 11 | .querySelectorAll('.sub-menu') |
12 | .forEach(sub => sub.remove()); | 12 | .forEach(sub => sub.remove()) |
13 | } | 13 | } |
14 | } | 14 | } |
15 | } | 15 | } |
16 | window.customElements.define('menu-item', Item, { extends: 'div' }); | 16 | window.customElements.define('menu-item', Item, { extends: 'div' }) |
17 | 17 | ||
18 | class Folder extends HTMLDivElement { | 18 | class Folder extends window.HTMLDivElement { |
19 | constructor({ text, innerHTML, items }) { | 19 | constructor ({ text, innerHTML, items }) { |
20 | super(); | 20 | super() |
21 | this.innerHTML = innerHTML ?? text; | 21 | this.innerHTML = innerHTML ?? text |
22 | this.classList.add('folder', 'menu-item'); | 22 | this.classList.add('folder', 'menu-item') |
23 | this.items = items; | 23 | this.items = items |
24 | this.onmouseover = () => { | 24 | this.onmouseover = () => { |
25 | if (this.querySelector('.sub-menu')) return; | 25 | if (this.querySelector('.sub-menu')) return |
26 | // Prepare submenu | 26 | // Prepare submenu |
27 | const submenu = document.createElement('div'); | 27 | const submenu = document.createElement('div') |
28 | submenu.className = 'sub-menu'; | 28 | submenu.className = 'sub-menu' |
29 | submenu.style.cssText = `position: absolute; left: 105%; top: 0px;`; | 29 | submenu.style.cssText = 'position: absolute; left: 105%; top: 0px;' |
30 | this.items.forEach(item => submenu.appendChild(item)); | 30 | this.items.forEach(item => submenu.appendChild(item)) |
31 | submenu.onmouseleave = () => submenu.remove() | 31 | submenu.onmouseleave = () => submenu.remove() |
32 | 32 | ||
33 | // hover effect | 33 | // hover effect |
34 | this.parentElement | 34 | this.parentElement |
35 | .querySelectorAll('.sub-menu') | 35 | .querySelectorAll('.sub-menu') |
36 | .forEach(sub => sub.remove()); | 36 | .forEach(sub => sub.remove()) |
37 | this.appendChild(submenu); | 37 | this.appendChild(submenu) |
38 | }; | 38 | } |
39 | } | 39 | } |
40 | } | 40 | } |
41 | window.customElements.define('menu-folder', Folder, { extends: 'div' }); | 41 | window.customElements.define('menu-folder', Folder, { extends: 'div' }) |
42 | 42 | ||
43 | export const pickMapItem = ({ utils }) => | 43 | export const pickMapItem = ({ utils }) => |
44 | new Folder({ | 44 | new Folder({ |
@@ -48,12 +48,12 @@ export const pickMapItem = ({ utils }) => | |||
48 | new Item({ | 48 | new Item({ |
49 | text: map.id, | 49 | text: map.id, |
50 | onclick: () => { | 50 | onclick: () => { |
51 | map.classList.add('focus'); | 51 | map.classList.add('focus') |
52 | map.scrollIntoView({ behavior: 'smooth' }); | 52 | map.scrollIntoView({ behavior: 'smooth' }) |
53 | }, | 53 | } |
54 | }), | 54 | }) |
55 | ), | 55 | ) |
56 | }); | 56 | }) |
57 | 57 | ||
58 | export const pickBlockItem = ({ blocks, utils }) => | 58 | export const pickBlockItem = ({ blocks, utils }) => |
59 | new Folder({ | 59 | new Folder({ |
@@ -68,12 +68,12 @@ export const pickBlockItem = ({ blocks, utils }) => | |||
68 | ?.textContent.substring(0, 15) | 68 | ?.textContent.substring(0, 15) |
69 | .concat(' ...'), | 69 | .concat(' ...'), |
70 | onclick: () => { | 70 | onclick: () => { |
71 | block.classList.add('focus'); | 71 | block.classList.add('focus') |
72 | utils.scrollToBlock(block); | 72 | utils.scrollToBlock(block) |
73 | }, | 73 | } |
74 | }), | 74 | }) |
75 | ), | 75 | ) |
76 | }); | 76 | }) |
77 | 77 | ||
78 | export const pickLayoutItem = ({ container, layouts }) => | 78 | export const pickLayoutItem = ({ container, layouts }) => |
79 | new Folder({ | 79 | new Folder({ |
@@ -82,78 +82,78 @@ export const pickLayoutItem = ({ container, layouts }) => | |||
82 | new Item({ | 82 | new Item({ |
83 | text: 'EDIT', | 83 | text: 'EDIT', |
84 | onclick: () => | 84 | onclick: () => |
85 | container.closest('[data-mode]').setAttribute('data-mode', 'editing'), | 85 | container.closest('[data-mode]').setAttribute('data-mode', 'editing') |
86 | }), | 86 | }), |
87 | ...layouts.map( | 87 | ...layouts.map( |
88 | layout => | 88 | layout => |
89 | new Item({ | 89 | new Item({ |
90 | text: layout.name, | 90 | text: layout.name, |
91 | onclick: () => container.setAttribute('data-layout', layout.name), | 91 | onclick: () => container.setAttribute('data-layout', layout.name) |
92 | }), | 92 | }) |
93 | ), | 93 | ), |
94 | new Item({ | 94 | new Item({ |
95 | innerHTML: '<a href="https://github.com/outdoorsafetylab/dumbymap#layouts" class="external" style="display: block; padding: 0.5rem;">More...</a>', | 95 | innerHTML: '<a href="https://github.com/outdoorsafetylab/dumbymap#layouts" class="external" style="display: block; padding: 0.5rem;">More...</a>', |
96 | style: 'padding: 0;' | 96 | style: 'padding: 0;' |
97 | }), | 97 | }) |
98 | ], | 98 | ] |
99 | }); | 99 | }) |
100 | 100 | ||
101 | export const addGeoLink = ({ utils }, range) => | 101 | export const addGeoLink = ({ utils }, range) => |
102 | new Item({ | 102 | new Item({ |
103 | text: 'Add GeoLink', | 103 | text: 'Add GeoLink', |
104 | onclick: () => { | 104 | onclick: () => { |
105 | const content = range.toString(); | 105 | const content = range.toString() |
106 | // FIXME Apply geolink only on matching sub-range | 106 | // FIXME Apply geolink only on matching sub-range |
107 | const match = content.match(/(^\D*[\d.]+)\D+([\d.]+)\D*$/); | 107 | const match = content.match(/(^\D*[\d.]+)\D+([\d.]+)\D*$/) |
108 | if (!match) return false; | 108 | if (!match) return false |
109 | 109 | ||
110 | const [x, y] = match.slice(1); | 110 | const [x, y] = match.slice(1) |
111 | const anchor = document.createElement('a'); | 111 | const anchor = document.createElement('a') |
112 | anchor.textContent = content; | 112 | anchor.textContent = content |
113 | // FIXME apply WGS84 | 113 | // FIXME apply WGS84 |
114 | anchor.href = `geo:${y},${x}?xy=${x},${y}`; | 114 | anchor.href = `geo:${y},${x}?xy=${x},${y}` |
115 | 115 | ||
116 | // FIXME | 116 | // FIXME |
117 | if (utils.createGeoLink(anchor)) { | 117 | if (utils.createGeoLink(anchor)) { |
118 | range.deleteContents(); | 118 | range.deleteContents() |
119 | range.insertNode(anchor); | 119 | range.insertNode(anchor) |
120 | } | 120 | } |
121 | }, | 121 | } |
122 | }); | 122 | }) |
123 | 123 | ||
124 | export class Suggestion { | 124 | export class Suggestion { |
125 | constructor({ text, replace }) { | 125 | constructor ({ text, replace }) { |
126 | this.text = text; | 126 | this.text = text |
127 | this.replace = replace; | 127 | this.replace = replace |
128 | } | 128 | } |
129 | 129 | ||
130 | createElement(codemirror) { | 130 | createElement (codemirror) { |
131 | const option = document.createElement('div'); | 131 | const option = document.createElement('div') |
132 | if (this.text.startsWith('<')) { | 132 | if (this.text.startsWith('<')) { |
133 | option.innerHTML = this.text; | 133 | option.innerHTML = this.text |
134 | } else { | 134 | } else { |
135 | option.innerText = this.text; | 135 | option.innerText = this.text |
136 | } | 136 | } |
137 | option.classList.add('container__suggestion'); | 137 | option.classList.add('container__suggestion') |
138 | option.onmouseover = () => { | 138 | option.onmouseover = () => { |
139 | Array.from(option.parentElement?.children)?.forEach(s => | 139 | Array.from(option.parentElement?.children)?.forEach(s => |
140 | s.classList.remove('focus'), | 140 | s.classList.remove('focus') |
141 | ); | 141 | ) |
142 | option.classList.add('focus'); | 142 | option.classList.add('focus') |
143 | }; | 143 | } |
144 | option.onmouseout = () => { | 144 | option.onmouseout = () => { |
145 | option.classList.remove('focus'); | 145 | option.classList.remove('focus') |
146 | }; | 146 | } |
147 | option.onclick = () => { | 147 | option.onclick = () => { |
148 | const anchor = codemirror.getCursor(); | 148 | const anchor = codemirror.getCursor() |
149 | codemirror.setSelection(anchor, { ...anchor, ch: 0 }); | 149 | codemirror.setSelection(anchor, { ...anchor, ch: 0 }) |
150 | codemirror.replaceSelection(this.replace); | 150 | codemirror.replaceSelection(this.replace) |
151 | codemirror.focus(); | 151 | codemirror.focus() |
152 | const newAnchor = { ...anchor, ch: this.replace.length }; | 152 | const newAnchor = { ...anchor, ch: this.replace.length } |
153 | codemirror.setCursor(newAnchor); | 153 | codemirror.setCursor(newAnchor) |
154 | }; | 154 | } |
155 | 155 | ||
156 | return option; | 156 | return option |
157 | } | 157 | } |
158 | } | 158 | } |
159 | 159 | ||
@@ -161,100 +161,100 @@ export const renderResults = ({ modal, modalContent }, map) => | |||
161 | new Item({ | 161 | new Item({ |
162 | text: 'Render Results', | 162 | text: 'Render Results', |
163 | onclick: () => { | 163 | onclick: () => { |
164 | modal.open(); | 164 | modal.open() |
165 | modal.overlayBlur = 3; | 165 | modal.overlayBlur = 3 |
166 | modal.closeByEscKey = false; | 166 | modal.closeByEscKey = false |
167 | // HACK find another way to override inline style | 167 | // HACK find another way to override inline style |
168 | document.querySelector('.plainmodal-overlay-force').style.position = | 168 | document.querySelector('.plainmodal-overlay-force').style.position = |
169 | 'relative'; | 169 | 'relative' |
170 | 170 | ||
171 | modalContent.innerHTML = ''; | 171 | modalContent.innerHTML = '' |
172 | const sourceCode = document.createElement('div') | 172 | const sourceCode = document.createElement('div') |
173 | sourceCode.innerHTML = `<a href="${map.renderer.url ?? map.renderer.use}">Source Code</a>` | 173 | sourceCode.innerHTML = `<a href="${map.renderer.url ?? map.renderer.use}">Source Code</a>` |
174 | modalContent.appendChild(sourceCode) | 174 | modalContent.appendChild(sourceCode) |
175 | const printDetails = result => { | 175 | const printDetails = result => { |
176 | const funcBody = result.func.toString(); | 176 | const funcBody = result.func.toString() |
177 | const loc = funcBody.split('\n').length; | 177 | const loc = funcBody.split('\n').length |
178 | const color = | 178 | const color = |
179 | { | 179 | { |
180 | success: 'green', | 180 | success: 'green', |
181 | fail: 'red', | 181 | fail: 'red', |
182 | skip: 'black', | 182 | skip: 'black', |
183 | stop: 'yellow', | 183 | stop: 'yellow' |
184 | }[result.state] ?? 'black'; | 184 | }[result.state] ?? 'black' |
185 | printObject( | 185 | printObject( |
186 | result, | 186 | result, |
187 | modalContent, | 187 | modalContent, |
188 | `${result.func.name} <span style='float: right;'>${loc}LOC\x20\x20\x20<span style='display: inline-block; width: 100px; color: ${color};'>${result.state}</span></span>`, | 188 | `${result.func.name} <span style='float: right;'>${loc}LOC\x20\x20\x20<span style='display: inline-block; width: 100px; color: ${color};'>${result.state}</span></span>` |
189 | ); | 189 | ) |
190 | }; | 190 | } |
191 | 191 | ||
192 | // Add contents about prepare steps | 192 | // Add contents about prepare steps |
193 | const prepareHeading = document.createElement('h3'); | 193 | const prepareHeading = document.createElement('h3') |
194 | prepareHeading.textContent = 'Prepare Steps'; | 194 | prepareHeading.textContent = 'Prepare Steps' |
195 | modalContent.appendChild(prepareHeading); | 195 | modalContent.appendChild(prepareHeading) |
196 | const prepareSteps = map.renderer.results.filter( | 196 | const prepareSteps = map.renderer.results.filter( |
197 | r => r.type === 'prepare', | 197 | r => r.type === 'prepare' |
198 | ); | 198 | ) |
199 | prepareSteps.forEach(printDetails); | 199 | prepareSteps.forEach(printDetails) |
200 | 200 | ||
201 | // Add contents about render steps | 201 | // Add contents about render steps |
202 | const renderHeading = document.createElement('h3'); | 202 | const renderHeading = document.createElement('h3') |
203 | renderHeading.textContent = 'Render Steps'; | 203 | renderHeading.textContent = 'Render Steps' |
204 | modalContent.appendChild(renderHeading); | 204 | modalContent.appendChild(renderHeading) |
205 | const renderSteps = map.renderer.results.filter(r => r.type === 'render'); | 205 | const renderSteps = map.renderer.results.filter(r => r.type === 'render') |
206 | renderSteps.forEach(printDetails); | 206 | renderSteps.forEach(printDetails) |
207 | }, | 207 | } |
208 | }); | 208 | }) |
209 | 209 | ||
210 | function printObject(obj, parentElement, name = null) { | 210 | function printObject (obj, parentElement, name = null) { |
211 | // Create <details> and <summary> inside | 211 | // Create <details> and <summary> inside |
212 | const detailsEle = document.createElement('details'); | 212 | const detailsEle = document.createElement('details') |
213 | const details = name ?? (obj instanceof Error ? obj.name : Object.values(obj)[0]); | 213 | const details = name ?? (obj instanceof Error ? obj.name : Object.values(obj)[0]) |
214 | detailsEle.innerHTML = `<summary>${details}</summary>`; | 214 | detailsEle.innerHTML = `<summary>${details}</summary>` |
215 | parentElement.appendChild(detailsEle); | 215 | parentElement.appendChild(detailsEle) |
216 | 216 | ||
217 | detailsEle.onclick = () => { | 217 | detailsEle.onclick = () => { |
218 | // Don't add items if it has contents | 218 | // Don't add items if it has contents |
219 | if (detailsEle.querySelector(':scope > :not(summary)')) return; | 219 | if (detailsEle.querySelector(':scope > :not(summary)')) return |
220 | 220 | ||
221 | if (obj instanceof Error) { | 221 | if (obj instanceof Error) { |
222 | // Handle Error objects specially | 222 | // Handle Error objects specially |
223 | const errorProps = ['name', 'message', 'stack', ...Object.keys(obj)]; | 223 | const errorProps = ['name', 'message', 'stack', ...Object.keys(obj)] |
224 | errorProps.forEach(key => { | 224 | errorProps.forEach(key => { |
225 | const value = obj[key]; | 225 | const value = obj[key] |
226 | const valueString = key === 'stack' ? `<pre>${value}</pre>` : value; | 226 | const valueString = key === 'stack' ? `<pre>${value}</pre>` : value |
227 | const propertyElement = document.createElement('p'); | 227 | const propertyElement = document.createElement('p') |
228 | propertyElement.innerHTML = `<strong>${key}</strong>: ${valueString}`; | 228 | propertyElement.innerHTML = `<strong>${key}</strong>: ${valueString}` |
229 | detailsEle.appendChild(propertyElement); | 229 | detailsEle.appendChild(propertyElement) |
230 | }); | 230 | }) |
231 | } else { | 231 | } else { |
232 | // Handle regular objects | 232 | // Handle regular objects |
233 | Object.entries(obj).forEach(([key, value]) => { | 233 | Object.entries(obj).forEach(([key, value]) => { |
234 | if (typeof value === 'object' && value !== null) { | 234 | if (typeof value === 'object' && value !== null) { |
235 | printObject(value, detailsEle, key); | 235 | printObject(value, detailsEle, key) |
236 | } else { | 236 | } else { |
237 | const valueString = | 237 | const valueString = |
238 | typeof value === 'function' | 238 | typeof value === 'function' |
239 | ? `<pre>${value}</pre>` | 239 | ? `<pre>${value}</pre>` |
240 | : value ?? typeof value; | 240 | : value ?? typeof value |
241 | const propertyElement = document.createElement('p'); | 241 | const propertyElement = document.createElement('p') |
242 | propertyElement.innerHTML = `<strong>${key}</strong>: ${valueString}`; | 242 | propertyElement.innerHTML = `<strong>${key}</strong>: ${valueString}` |
243 | detailsEle.appendChild(propertyElement); | 243 | detailsEle.appendChild(propertyElement) |
244 | } | 244 | } |
245 | }); | 245 | }) |
246 | } | 246 | } |
247 | }; | 247 | } |
248 | } | 248 | } |
249 | 249 | ||
250 | export const toggleBlockFocus = block => | 250 | export const toggleBlockFocus = block => |
251 | new Item({ | 251 | new Item({ |
252 | text: 'Toggle Focus', | 252 | text: 'Toggle Focus', |
253 | onclick: () => block.classList.toggle('focus'), | 253 | onclick: () => block.classList.toggle('focus') |
254 | }); | 254 | }) |
255 | 255 | ||
256 | export const toggleMapFocus = map => | 256 | export const toggleMapFocus = map => |
257 | new Item({ | 257 | new Item({ |
258 | text: 'Toggle Focus', | 258 | text: 'Toggle Focus', |
259 | onclick: () => map.classList.toggle('focus'), | 259 | onclick: () => map.classList.toggle('focus') |
260 | }); | 260 | }) |
diff --git a/src/dumbyUtils.mjs b/src/dumbyUtils.mjs index 2a9b787..7bf26c8 100644 --- a/src/dumbyUtils.mjs +++ b/src/dumbyUtils.mjs | |||
@@ -1,62 +1,62 @@ | |||
1 | import LeaderLine from 'leader-line'; | 1 | import LeaderLine from 'leader-line' |
2 | 2 | ||
3 | export function focusNextMap(reverse = false) { | 3 | export function focusNextMap (reverse = false) { |
4 | const renderedList = this.utils.renderedMaps(); | 4 | const renderedList = this.utils.renderedMaps() |
5 | const index = renderedList.findIndex(e => e.classList.contains('focus')); | 5 | const index = renderedList.findIndex(e => e.classList.contains('focus')) |
6 | const nextIndex = | 6 | const nextIndex = |
7 | index === -1 ? 0 : (index + (reverse ? -1 : 1)) % renderedList.length; | 7 | index === -1 ? 0 : (index + (reverse ? -1 : 1)) % renderedList.length |
8 | 8 | ||
9 | const nextMap = renderedList.at(nextIndex); | 9 | const nextMap = renderedList.at(nextIndex) |
10 | nextMap.classList.add('focus'); | 10 | nextMap.classList.add('focus') |
11 | } | 11 | } |
12 | 12 | ||
13 | export function focusNextBlock(reverse = false) { | 13 | export function focusNextBlock (reverse = false) { |
14 | const blocks = this.blocks.filter(b => | 14 | const blocks = this.blocks.filter(b => |
15 | b.checkVisibility({ | 15 | b.checkVisibility({ |
16 | contentVisibilityAuto: true, | 16 | contentVisibilityAuto: true, |
17 | opacityProperty: true, | 17 | opacityProperty: true, |
18 | visibilityProperty: true, | 18 | visibilityProperty: true |
19 | }), | 19 | }) |
20 | ); | 20 | ) |
21 | const index = blocks.findIndex(e => e.classList.contains('focus')); | 21 | const index = blocks.findIndex(e => e.classList.contains('focus')) |
22 | const nextIndex = | 22 | const nextIndex = |
23 | index === -1 ? 0 : (index + (reverse ? -1 : 1)) % blocks.length; | 23 | index === -1 ? 0 : (index + (reverse ? -1 : 1)) % blocks.length |
24 | 24 | ||
25 | blocks.forEach(b => b.classList.remove('focus')); | 25 | blocks.forEach(b => b.classList.remove('focus')) |
26 | const nextBlock = blocks.at(nextIndex); | 26 | const nextBlock = blocks.at(nextIndex) |
27 | nextBlock?.classList?.add('focus'); | 27 | nextBlock?.classList?.add('focus') |
28 | scrollToBlock(nextBlock); | 28 | scrollToBlock(nextBlock) |
29 | } | 29 | } |
30 | 30 | ||
31 | // Consider block is bigger then viewport height | 31 | // Consider block is bigger then viewport height |
32 | export const scrollToBlock = block => { | 32 | export const scrollToBlock = block => { |
33 | const parentRect = block.parentElement.getBoundingClientRect(); | 33 | const parentRect = block.parentElement.getBoundingClientRect() |
34 | const scrollBlock = | 34 | const scrollBlock = |
35 | block.getBoundingClientRect().height > parentRect.height * 0.8 | 35 | block.getBoundingClientRect().height > parentRect.height * 0.8 |
36 | ? 'nearest' | 36 | ? 'nearest' |
37 | : 'center'; | 37 | : 'center' |
38 | block.scrollIntoView({ behavior: 'smooth', block: scrollBlock }); | 38 | block.scrollIntoView({ behavior: 'smooth', block: scrollBlock }) |
39 | }; | 39 | } |
40 | 40 | ||
41 | export function focusDelay() { | 41 | export function focusDelay () { |
42 | return window.getComputedStyle(this.showcase).display === 'none' ? 50 : 300; | 42 | return window.window.getComputedStyle(this.showcase).display === 'none' ? 50 : 300 |
43 | } | 43 | } |
44 | 44 | ||
45 | export function switchToNextLayout(reverse = false) { | 45 | export function switchToNextLayout (reverse = false) { |
46 | const layouts = this.layouts; | 46 | const layouts = this.layouts |
47 | const currentLayoutName = this.container.getAttribute('data-layout'); | 47 | const currentLayoutName = this.container.getAttribute('data-layout') |
48 | const currentIndex = layouts.map(l => l.name).indexOf(currentLayoutName); | 48 | const currentIndex = layouts.map(l => l.name).indexOf(currentLayoutName) |
49 | const padding = reverse ? -1 : 1; | 49 | const padding = reverse ? -1 : 1 |
50 | const nextIndex = | 50 | const nextIndex = |
51 | currentIndex === -1 | 51 | currentIndex === -1 |
52 | ? 0 | 52 | ? 0 |
53 | : (currentIndex + padding + layouts.length) % layouts.length; | 53 | : (currentIndex + padding + layouts.length) % layouts.length |
54 | const nextLayout = layouts[nextIndex]; | 54 | const nextLayout = layouts[nextIndex] |
55 | this.container.setAttribute('data-layout', nextLayout.name); | 55 | this.container.setAttribute('data-layout', nextLayout.name) |
56 | } | 56 | } |
57 | 57 | ||
58 | export function removeBlockFocus() { | 58 | export function removeBlockFocus () { |
59 | this.blocks.forEach(b => b.classList.remove('focus')); | 59 | this.blocks.forEach(b => b.classList.remove('focus')) |
60 | } | 60 | } |
61 | 61 | ||
62 | /** | 62 | /** |
@@ -66,31 +66,31 @@ export function removeBlockFocus() { | |||
66 | * @returns {Boolean} ture is link is created, false if coordinates are invalid | 66 | * @returns {Boolean} ture is link is created, false if coordinates are invalid |
67 | */ | 67 | */ |
68 | export const createGeoLink = (link, callback = null) => { | 68 | export const createGeoLink = (link, callback = null) => { |
69 | const url = new URL(link.href); | 69 | const url = new URL(link.href) |
70 | const xyInParams = url.searchParams.get('xy'); | 70 | const xyInParams = url.searchParams.get('xy') |
71 | const xy = xyInParams | 71 | const xy = xyInParams |
72 | ? xyInParams.split(',')?.map(Number) | 72 | ? xyInParams.split(',')?.map(Number) |
73 | : url?.href | 73 | : url?.href |
74 | ?.match(/^geo:([0-9.,]+)/) | 74 | ?.match(/^geo:([0-9.,]+)/) |
75 | ?.at(1) | 75 | ?.at(1) |
76 | ?.split(',') | 76 | ?.split(',') |
77 | ?.reverse() | 77 | ?.reverse() |
78 | ?.map(Number); | 78 | ?.map(Number) |
79 | 79 | ||
80 | if (!xy || isNaN(xy[0]) || isNaN(xy[1])) return false; | 80 | if (!xy || isNaN(xy[0]) || isNaN(xy[1])) return false |
81 | 81 | ||
82 | // Geo information in link | 82 | // Geo information in link |
83 | link.url = url; | 83 | link.url = url |
84 | link.xy = xy; | 84 | link.xy = xy |
85 | link.classList.add('with-leader-line', 'geolink'); | 85 | link.classList.add('with-leader-line', 'geolink') |
86 | link.targets = link.url.searchParams.get('id')?.split(',') ?? null; | 86 | link.targets = link.url.searchParams.get('id')?.split(',') ?? null |
87 | 87 | ||
88 | // LeaderLine | 88 | // LeaderLine |
89 | link.lines = []; | 89 | link.lines = [] |
90 | callback?.call(this, link); | 90 | callback?.call(this, link) |
91 | 91 | ||
92 | return true; | 92 | return true |
93 | }; | 93 | } |
94 | 94 | ||
95 | /** | 95 | /** |
96 | * CreateDocLink. | 96 | * CreateDocLink. |
@@ -98,30 +98,30 @@ export const createGeoLink = (link, callback = null) => { | |||
98 | * @param {HTMLElement} Elements contains anchor elements for doclinks | 98 | * @param {HTMLElement} Elements contains anchor elements for doclinks |
99 | */ | 99 | */ |
100 | export const createDocLink = link => { | 100 | export const createDocLink = link => { |
101 | link.classList.add('with-leader-line', 'doclink'); | 101 | link.classList.add('with-leader-line', 'doclink') |
102 | link.lines = []; | 102 | link.lines = [] |
103 | 103 | ||
104 | link.onmouseover = () => { | 104 | link.onmouseover = () => { |
105 | const label = decodeURIComponent(link.href.split('#')[1]); | 105 | const label = decodeURIComponent(link.href.split('#')[1]) |
106 | const selector = link.title.split('=>')[1] ?? '#' + label; | 106 | const selector = link.title.split('=>')[1] ?? '#' + label |
107 | const target = document.querySelector(selector); | 107 | const target = document.querySelector(selector) |
108 | if (!target?.checkVisibility()) return; | 108 | if (!target?.checkVisibility()) return |
109 | 109 | ||
110 | const line = new LeaderLine({ | 110 | const line = new LeaderLine({ |
111 | start: link, | 111 | start: link, |
112 | end: target, | 112 | end: target, |
113 | middleLabel: LeaderLine.pathLabel({ | 113 | middleLabel: LeaderLine.pathLabel({ |
114 | text: label, | 114 | text: label, |
115 | fontWeight: 'bold', | 115 | fontWeight: 'bold' |
116 | }), | 116 | }), |
117 | hide: true, | 117 | hide: true, |
118 | path: 'magnet', | 118 | path: 'magnet' |
119 | }); | 119 | }) |
120 | link.lines.push(line); | 120 | link.lines.push(line) |
121 | line.show('draw', { duration: 300 }); | 121 | line.show('draw', { duration: 300 }) |
122 | }; | 122 | } |
123 | link.onmouseout = () => { | 123 | link.onmouseout = () => { |
124 | link.lines.forEach(line => line.remove()); | 124 | link.lines.forEach(line => line.remove()) |
125 | link.lines.length = 0; | 125 | link.lines.length = 0 |
126 | }; | 126 | } |
127 | }; | 127 | } |
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs index 2efc350..fa88f3a 100644 --- a/src/dumbymap.mjs +++ b/src/dumbymap.mjs | |||
@@ -1,53 +1,53 @@ | |||
1 | import MarkdownIt from 'markdown-it'; | 1 | import MarkdownIt from 'markdown-it' |
2 | import MarkdownItAnchor from 'markdown-it-anchor'; | 2 | import MarkdownItAnchor from 'markdown-it-anchor' |
3 | import MarkdownItFootnote from 'markdown-it-footnote'; | 3 | import MarkdownItFootnote from 'markdown-it-footnote' |
4 | import MarkdownItFrontMatter from 'markdown-it-front-matter'; | 4 | import MarkdownItFrontMatter from 'markdown-it-front-matter' |
5 | import MarkdownItTocDoneRight from 'markdown-it-toc-done-right'; | 5 | import MarkdownItTocDoneRight from 'markdown-it-toc-done-right' |
6 | import LeaderLine from 'leader-line'; | 6 | import LeaderLine from 'leader-line' |
7 | import { renderWith, defaultAliases, parseConfigsFromYaml } from 'mapclay'; | 7 | import { renderWith, defaultAliases, parseConfigsFromYaml } from 'mapclay' |
8 | import { onRemove, animateRectTransition, throttle } from './utils'; | 8 | import { onRemove, animateRectTransition, throttle } from './utils' |
9 | import { Layout, SideBySide, Overlay } from './Layout'; | 9 | import { Layout, SideBySide, Overlay } from './Layout' |
10 | import * as utils from './dumbyUtils'; | 10 | import * as utils from './dumbyUtils' |
11 | import * as menuItem from './MenuItem'; | 11 | import * as menuItem from './MenuItem' |
12 | import { default as PlainModal } from 'plain-modal'; | 12 | import PlainModal from 'plain-modal' |
13 | 13 | ||
14 | const docLinkSelector = 'a[href^="#"][title^="=>"]'; | 14 | const docLinkSelector = 'a[href^="#"][title^="=>"]' |
15 | const geoLinkSelector = 'a[href^="geo:"]'; | 15 | const geoLinkSelector = 'a[href^="geo:"]' |
16 | 16 | ||
17 | const layouts = [ | 17 | const layouts = [ |
18 | new Layout({ name: 'normal' }), | 18 | new Layout({ name: 'normal' }), |
19 | new SideBySide({ name: 'side-by-side' }), | 19 | new SideBySide({ name: 'side-by-side' }), |
20 | new Overlay({ name: 'overlay' }), | 20 | new Overlay({ name: 'overlay' }) |
21 | ]; | 21 | ] |
22 | const mapCache = {}; | 22 | const mapCache = {} |
23 | 23 | ||
24 | export const markdown2HTML = (container, mdContent) => { | 24 | export const markdown2HTML = (container, mdContent) => { |
25 | // Render: Markdown -> HTML {{{ | 25 | // Render: Markdown -> HTML {{{ |
26 | container.replaceChildren(); | 26 | container.replaceChildren() |
27 | 27 | ||
28 | container.innerHTML = '<div class="SemanticHtml"></div>'; | 28 | container.innerHTML = '<div class="SemanticHtml"></div>' |
29 | const htmlHolder = container.querySelector('.SemanticHtml'); | 29 | const htmlHolder = container.querySelector('.SemanticHtml') |
30 | 30 | ||
31 | const md = MarkdownIt({ | 31 | const md = MarkdownIt({ |
32 | html: true, | 32 | html: true, |
33 | breaks: true, | 33 | breaks: true |
34 | }) | 34 | }) |
35 | .use(MarkdownItAnchor, { | 35 | .use(MarkdownItAnchor, { |
36 | permalink: MarkdownItAnchor.permalink.linkInsideHeader({ | 36 | permalink: MarkdownItAnchor.permalink.linkInsideHeader({ |
37 | placement: 'before', | 37 | placement: 'before' |
38 | }), | 38 | }) |
39 | }) | 39 | }) |
40 | .use(MarkdownItFootnote) | 40 | .use(MarkdownItFootnote) |
41 | .use(MarkdownItFrontMatter) | 41 | .use(MarkdownItFrontMatter) |
42 | .use(MarkdownItTocDoneRight); | 42 | .use(MarkdownItTocDoneRight) |
43 | 43 | ||
44 | // FIXME A better way to generate blocks | 44 | // FIXME A better way to generate blocks |
45 | md.renderer.rules.dumby_block_open = () => '<div>'; | 45 | md.renderer.rules.dumby_block_open = () => '<div>' |
46 | md.renderer.rules.dumby_block_close = () => '</div>'; | 46 | md.renderer.rules.dumby_block_close = () => '</div>' |
47 | 47 | ||
48 | md.core.ruler.before('block', 'dumby_block', state => { | 48 | md.core.ruler.before('block', 'dumby_block', state => { |
49 | state.tokens.push(new state.Token('dumby_block_open', '', 1)); | 49 | state.tokens.push(new state.Token('dumby_block_open', '', 1)) |
50 | }); | 50 | }) |
51 | 51 | ||
52 | // Add close tag for block with more than 2 empty lines | 52 | // Add close tag for block with more than 2 empty lines |
53 | md.block.ruler.before('table', 'dumby_block', (state, startLine) => { | 53 | md.block.ruler.before('table', 'dumby_block', (state, startLine) => { |
@@ -56,39 +56,39 @@ export const markdown2HTML = (container, mdContent) => { | |||
56 | state.src[state.bMarks[startLine - 2]] === '\n' && | 56 | state.src[state.bMarks[startLine - 2]] === '\n' && |
57 | state.tokens.at(-1).type !== 'list_item_open' // Quick hack for not adding tag after "::marker" for <li> | 57 | state.tokens.at(-1).type !== 'list_item_open' // Quick hack for not adding tag after "::marker" for <li> |
58 | ) { | 58 | ) { |
59 | state.push('dumby_block_close', '', -1); | 59 | state.push('dumby_block_close', '', -1) |
60 | state.push('dumby_block_open', '', 1); | 60 | state.push('dumby_block_open', '', 1) |
61 | } | 61 | } |
62 | }); | 62 | }) |
63 | 63 | ||
64 | md.core.ruler.after('block', 'dumby_block', state => { | 64 | md.core.ruler.after('block', 'dumby_block', state => { |
65 | state.tokens.push(new state.Token('dumby_block_close', '', -1)); | 65 | state.tokens.push(new state.Token('dumby_block_close', '', -1)) |
66 | }); | 66 | }) |
67 | 67 | ||
68 | const contentWithToc = '${toc}\n\n\n' + mdContent; | 68 | const contentWithToc = '${toc}\n\n\n' + mdContent // eslint-disable-line |
69 | htmlHolder.innerHTML = md.render(contentWithToc); | 69 | htmlHolder.innerHTML = md.render(contentWithToc) |
70 | 70 | ||
71 | // TODO Do this in markdown-it | 71 | // TODO Do this in markdown-it |
72 | const blocks = htmlHolder.querySelectorAll(':scope > div:not(:has(nav))'); | 72 | const blocks = htmlHolder.querySelectorAll(':scope > div:not(:has(nav))') |
73 | blocks.forEach(b => { | 73 | blocks.forEach(b => { |
74 | b.classList.add('dumby-block'); | 74 | b.classList.add('dumby-block') |
75 | b.setAttribute('data-total', blocks.length); | 75 | b.setAttribute('data-total', blocks.length) |
76 | }); | 76 | }) |
77 | 77 | ||
78 | return container; | 78 | return container |
79 | //}}} | 79 | // }}} |
80 | }; | 80 | } |
81 | export const generateMaps = (container, { delay, mapCallback }) => { | 81 | export const generateMaps = (container, { delay, mapCallback }) => { |
82 | container.classList.add('Dumby'); | 82 | container.classList.add('Dumby') |
83 | const htmlHolder = container.querySelector('.SemanticHtml') ?? container; | 83 | const htmlHolder = container.querySelector('.SemanticHtml') ?? container |
84 | const blocks = Array.from(htmlHolder.querySelectorAll('.dumby-block')); | 84 | const blocks = Array.from(htmlHolder.querySelectorAll('.dumby-block')) |
85 | const showcase = document.createElement('div'); | 85 | const showcase = document.createElement('div') |
86 | container.appendChild(showcase); | 86 | container.appendChild(showcase) |
87 | showcase.classList.add('Showcase'); | 87 | showcase.classList.add('Showcase') |
88 | const renderPromises = []; | 88 | const renderPromises = [] |
89 | const modalContent = document.createElement('div'); | 89 | const modalContent = document.createElement('div') |
90 | container.appendChild(modalContent); | 90 | container.appendChild(modalContent) |
91 | const modal = new PlainModal(modalContent); | 91 | const modal = new PlainModal(modalContent) |
92 | 92 | ||
93 | const dumbymap = { | 93 | const dumbymap = { |
94 | layouts, | 94 | layouts, |
@@ -102,50 +102,50 @@ export const generateMaps = (container, { delay, mapCallback }) => { | |||
102 | ...utils, | 102 | ...utils, |
103 | renderedMaps: () => | 103 | renderedMaps: () => |
104 | Array.from( | 104 | Array.from( |
105 | container.querySelectorAll('.mapclay[data-render=fulfilled]'), | 105 | container.querySelectorAll('.mapclay[data-render=fulfilled]') |
106 | ), | 106 | ), |
107 | focusNextMap: throttle(utils.focusNextMap, utils.focusDelay), | 107 | focusNextMap: throttle(utils.focusNextMap, utils.focusDelay), |
108 | switchToNextLayout: throttle(utils.switchToNextLayout, 300), | 108 | switchToNextLayout: throttle(utils.switchToNextLayout, 300) |
109 | }, | 109 | } |
110 | }; | 110 | } |
111 | Object.entries(dumbymap.utils).forEach(([util, func]) => { | 111 | Object.entries(dumbymap.utils).forEach(([util, func]) => { |
112 | dumbymap.utils[util] = func.bind(dumbymap); | 112 | dumbymap.utils[util] = func.bind(dumbymap) |
113 | }); | 113 | }) |
114 | 114 | ||
115 | // LeaderLine {{{ | 115 | // LeaderLine {{{ |
116 | 116 | ||
117 | Array.from(container.querySelectorAll(docLinkSelector)).filter( | 117 | Array.from(container.querySelectorAll(docLinkSelector)).filter( |
118 | utils.createDocLink, | 118 | utils.createDocLink |
119 | ); | 119 | ) |
120 | 120 | ||
121 | // Get anchors with "geo:" scheme | 121 | // Get anchors with "geo:" scheme |
122 | htmlHolder.anchors = []; | 122 | htmlHolder.anchors = [] |
123 | const geoLinkCallback = link => { | 123 | const geoLinkCallback = link => { |
124 | link.onmouseover = () => addLeaderLines(link); | 124 | link.onmouseover = () => addLeaderLines(link) |
125 | link.onmouseout = () => removeLeaderLines(link); | 125 | link.onmouseout = () => removeLeaderLines(link) |
126 | link.onclick = event => { | 126 | link.onclick = event => { |
127 | event.preventDefault(); | 127 | event.preventDefault() |
128 | htmlHolder.anchors | 128 | htmlHolder.anchors |
129 | .filter(isAnchorPointedBy(link)) | 129 | .filter(isAnchorPointedBy(link)) |
130 | .forEach(updateMapByMarker(link.xy)); | 130 | .forEach(updateMapByMarker(link.xy)) |
131 | // TODO Just hide leader line and show it again | 131 | // TODO Just hide leader line and show it again |
132 | removeLeaderLines(link); | 132 | removeLeaderLines(link) |
133 | }; | 133 | } |
134 | }; | 134 | } |
135 | const geoLinks = Array.from( | 135 | const geoLinks = Array.from( |
136 | container.querySelectorAll(geoLinkSelector), | 136 | container.querySelectorAll(geoLinkSelector) |
137 | ).filter(l => utils.createGeoLink(l, geoLinkCallback)); | 137 | ).filter(l => utils.createGeoLink(l, geoLinkCallback)) |
138 | 138 | ||
139 | const isAnchorPointedBy = link => anchor => { | 139 | const isAnchorPointedBy = link => anchor => { |
140 | const mapContainer = anchor.closest('.mapclay'); | 140 | const mapContainer = anchor.closest('.mapclay') |
141 | const isTarget = !link.targets || link.targets.includes(mapContainer.id); | 141 | const isTarget = !link.targets || link.targets.includes(mapContainer.id) |
142 | return anchor.title === link.url.pathname && isTarget; | 142 | return anchor.title === link.url.pathname && isTarget |
143 | }; | 143 | } |
144 | 144 | ||
145 | const isAnchorVisible = anchor => { | 145 | const isAnchorVisible = anchor => { |
146 | const mapContainer = anchor.closest('.mapclay'); | 146 | const mapContainer = anchor.closest('.mapclay') |
147 | return insideWindow(anchor) && insideParent(anchor, mapContainer); | 147 | return insideWindow(anchor) && insideParent(anchor, mapContainer) |
148 | }; | 148 | } |
149 | 149 | ||
150 | const drawLeaderLine = link => anchor => { | 150 | const drawLeaderLine = link => anchor => { |
151 | const line = new LeaderLine({ | 151 | const line = new LeaderLine({ |
@@ -153,223 +153,222 @@ export const generateMaps = (container, { delay, mapCallback }) => { | |||
153 | end: anchor, | 153 | end: anchor, |
154 | hide: true, | 154 | hide: true, |
155 | middleLabel: link.url.searchParams.get('text'), | 155 | middleLabel: link.url.searchParams.get('text'), |
156 | path: 'magnet', | 156 | path: 'magnet' |
157 | }); | 157 | }) |
158 | line.show('draw', { duration: 300 }); | 158 | line.show('draw', { duration: 300 }) |
159 | return line; | 159 | return line |
160 | }; | 160 | } |
161 | 161 | ||
162 | const addLeaderLines = link => { | 162 | const addLeaderLines = link => { |
163 | link.lines = htmlHolder.anchors | 163 | link.lines = htmlHolder.anchors |
164 | .filter(isAnchorPointedBy(link)) | 164 | .filter(isAnchorPointedBy(link)) |
165 | .filter(isAnchorVisible) | 165 | .filter(isAnchorVisible) |
166 | .map(drawLeaderLine(link)); | 166 | .map(drawLeaderLine(link)) |
167 | }; | 167 | } |
168 | 168 | ||
169 | const removeLeaderLines = link => { | 169 | const removeLeaderLines = link => { |
170 | if (!link.lines) return; | 170 | if (!link.lines) return |
171 | link.lines.forEach(line => line.remove()); | 171 | link.lines.forEach(line => line.remove()) |
172 | link.lines = []; | 172 | link.lines = [] |
173 | }; | 173 | } |
174 | 174 | ||
175 | const updateMapByMarker = xy => marker => { | 175 | const updateMapByMarker = xy => marker => { |
176 | const renderer = marker.closest('.mapclay')?.renderer; | 176 | const renderer = marker.closest('.mapclay')?.renderer |
177 | renderer.updateCamera({ center: xy }, true); | 177 | renderer.updateCamera({ center: xy }, true) |
178 | }; | 178 | } |
179 | 179 | ||
180 | const insideWindow = element => { | 180 | const insideWindow = element => { |
181 | const rect = element.getBoundingClientRect(); | 181 | const rect = element.getBoundingClientRect() |
182 | return ( | 182 | return ( |
183 | rect.left > 0 && | 183 | rect.left > 0 && |
184 | rect.right < window.innerWidth + rect.width && | 184 | rect.right < window.innerWidth + rect.width && |
185 | rect.top > 0 && | 185 | rect.top > 0 && |
186 | rect.bottom < window.innerHeight + rect.height | 186 | rect.bottom < window.innerHeight + rect.height |
187 | ); | 187 | ) |
188 | }; | 188 | } |
189 | 189 | ||
190 | const insideParent = (childElement, parentElement) => { | 190 | const insideParent = (childElement, parentElement) => { |
191 | const childRect = childElement.getBoundingClientRect(); | 191 | const childRect = childElement.getBoundingClientRect() |
192 | const parentRect = parentElement.getBoundingClientRect(); | 192 | const parentRect = parentElement.getBoundingClientRect() |
193 | const offset = 20; | 193 | const offset = 20 |
194 | 194 | ||
195 | return ( | 195 | return ( |
196 | childRect.left > parentRect.left + offset && | 196 | childRect.left > parentRect.left + offset && |
197 | childRect.right < parentRect.right - offset && | 197 | childRect.right < parentRect.right - offset && |
198 | childRect.top > parentRect.top + offset && | 198 | childRect.top > parentRect.top + offset && |
199 | childRect.bottom < parentRect.bottom - offset | 199 | childRect.bottom < parentRect.bottom - offset |
200 | ); | 200 | ) |
201 | }; | 201 | } |
202 | //}}} | 202 | // }}} |
203 | // CSS observer {{{ | 203 | // CSS observer {{{ |
204 | // Focus Map {{{ | 204 | // Focus Map {{{ |
205 | // Set focusArea | 205 | // Set focusArea |
206 | 206 | ||
207 | const mapFocusObserver = () => | 207 | const mapFocusObserver = () => |
208 | new MutationObserver(mutations => { | 208 | new window.MutationObserver(mutations => { |
209 | const mutation = mutations.at(-1); | 209 | const mutation = mutations.at(-1) |
210 | const target = mutation.target; | 210 | const target = mutation.target |
211 | const focus = target.classList.contains('focus'); | 211 | const focus = target.classList.contains('focus') |
212 | const shouldBeInShowcase = | 212 | const shouldBeInShowcase = |
213 | focus && | 213 | focus && |
214 | showcase.checkVisibility({ | 214 | showcase.checkVisibility({ |
215 | contentVisibilityAuto: true, | 215 | contentVisibilityAuto: true, |
216 | opacityProperty: true, | 216 | opacityProperty: true, |
217 | visibilityProperty: true, | 217 | visibilityProperty: true |
218 | }); | 218 | }) |
219 | 219 | ||
220 | if (focus) { | 220 | if (focus) { |
221 | dumbymap.utils | 221 | dumbymap.utils |
222 | .renderedMaps() | 222 | .renderedMaps() |
223 | .filter(map => map.id !== target.id) | 223 | .filter(map => map.id !== target.id) |
224 | .forEach(map => map.classList.remove('focus')); | 224 | .forEach(map => map.classList.remove('focus')) |
225 | } | 225 | } |
226 | 226 | ||
227 | if (shouldBeInShowcase) { | 227 | if (shouldBeInShowcase) { |
228 | if (showcase.contains(target)) return; | 228 | if (showcase.contains(target)) return |
229 | 229 | ||
230 | // Placeholder for map in Showcase, it should has the same DOMRect | 230 | // Placeholder for map in Showcase, it should has the same DOMRect |
231 | const placeholder = target.cloneNode(true); | 231 | const placeholder = target.cloneNode(true) |
232 | placeholder.removeAttribute('id'); | 232 | placeholder.removeAttribute('id') |
233 | placeholder.classList.remove('mapclay', 'focus'); | 233 | placeholder.classList.remove('mapclay', 'focus') |
234 | target.parentElement.replaceChild(placeholder, target); | 234 | target.parentElement.replaceChild(placeholder, target) |
235 | 235 | ||
236 | // FIXME Maybe use @start-style for CSS | 236 | // FIXME Maybe use @start-style for CSS |
237 | // Trigger CSS transition, if placeholde is the olny chil element in block, | 237 | // Trigger CSS transition, if placeholde is the olny chil element in block, |
238 | // reduce its height to zero. | 238 | // reduce its height to zero. |
239 | // To make sure the original height of placeholder is applied, DOM changes seems needed | 239 | // To make sure the original height of placeholder is applied, DOM changes seems needed |
240 | // then set data-attribute for CSS selector to change height to 0 | 240 | // then set data-attribute for CSS selector to change height to 0 |
241 | placeholder.getBoundingClientRect(); | 241 | placeholder.getBoundingClientRect() |
242 | placeholder.setAttribute('data-placeholder', target.id); | 242 | placeholder.setAttribute('data-placeholder', target.id) |
243 | 243 | ||
244 | // To fit showcase, remove all inline style | 244 | // To fit showcase, remove all inline style |
245 | target.removeAttribute('style'); | 245 | target.removeAttribute('style') |
246 | showcase.appendChild(target); | 246 | showcase.appendChild(target) |
247 | 247 | ||
248 | // Resume rect from Semantic HTML to Showcase, with animation | 248 | // Resume rect from Semantic HTML to Showcase, with animation |
249 | animateRectTransition(target, placeholder.getBoundingClientRect(), { | 249 | animateRectTransition(target, placeholder.getBoundingClientRect(), { |
250 | duration: 300, | 250 | duration: 300, |
251 | resume: true, | 251 | resume: true |
252 | }); | 252 | }) |
253 | } else if (showcase.contains(target)) { | 253 | } else if (showcase.contains(target)) { |
254 | // Check placeholder is inside Semantic HTML | 254 | // Check placeholder is inside Semantic HTML |
255 | const placeholder = htmlHolder.querySelector( | 255 | const placeholder = htmlHolder.querySelector( |
256 | `[data-placeholder="${target.id}"]`, | 256 | `[data-placeholder="${target.id}"]` |
257 | ); | 257 | ) |
258 | if (!placeholder) | 258 | if (!placeholder) { throw Error(`Cannot fine placeholder for map "${target.id}"`) } |
259 | throw Error(`Cannot fine placeholder for map "${target.id}"`); | ||
260 | 259 | ||
261 | // Consider animation may fail, write callback | 260 | // Consider animation may fail, write callback |
262 | const afterAnimation = () => { | 261 | const afterAnimation = () => { |
263 | placeholder.parentElement.replaceChild(target, placeholder); | 262 | placeholder.parentElement.replaceChild(target, placeholder) |
264 | target.style = placeholder.style.cssText; | 263 | target.style = placeholder.style.cssText |
265 | placeholder.remove(); | 264 | placeholder.remove() |
266 | }; | 265 | } |
267 | 266 | ||
268 | // animation from Showcase to placeholder | 267 | // animation from Showcase to placeholder |
269 | animateRectTransition(target, placeholder.getBoundingClientRect(), { | 268 | animateRectTransition(target, placeholder.getBoundingClientRect(), { |
270 | duration: 300, | 269 | duration: 300 |
271 | }).finished.finally(afterAnimation); | 270 | }).finished.finally(afterAnimation) |
272 | } | 271 | } |
273 | }); | 272 | }) |
274 | // }}} | 273 | // }}} |
275 | // Layout {{{ | 274 | // Layout {{{ |
276 | // press key to switch layout | 275 | // press key to switch layout |
277 | 276 | ||
278 | // observe layout change | 277 | // observe layout change |
279 | const layoutObserver = new MutationObserver(mutations => { | 278 | const layoutObserver = new window.MutationObserver(mutations => { |
280 | const mutation = mutations.at(-1); | 279 | const mutation = mutations.at(-1) |
281 | const oldLayout = mutation.oldValue; | 280 | const oldLayout = mutation.oldValue |
282 | const newLayout = container.getAttribute(mutation.attributeName); | 281 | const newLayout = container.getAttribute(mutation.attributeName) |
283 | 282 | ||
284 | // Apply handler for leaving/entering layouts | 283 | // Apply handler for leaving/entering layouts |
285 | if (oldLayout) { | 284 | if (oldLayout) { |
286 | layouts | 285 | layouts |
287 | .find(l => l.name === oldLayout) | 286 | .find(l => l.name === oldLayout) |
288 | ?.leaveHandler?.call(this, dumbymap); | 287 | ?.leaveHandler?.call(this, dumbymap) |
289 | } | 288 | } |
290 | 289 | ||
291 | Object.values(dumbymap) | 290 | Object.values(dumbymap) |
292 | .flat() | 291 | .flat() |
293 | .filter(ele => ele instanceof HTMLElement) | 292 | .filter(ele => ele instanceof window.HTMLElement) |
294 | .forEach(ele => ele.removeAttribute('style')); | 293 | .forEach(ele => ele.removeAttribute('style')) |
295 | 294 | ||
296 | if (newLayout) { | 295 | if (newLayout) { |
297 | layouts | 296 | layouts |
298 | .find(l => l.name === newLayout) | 297 | .find(l => l.name === newLayout) |
299 | ?.enterHandler?.call(this, dumbymap); | 298 | ?.enterHandler?.call(this, dumbymap) |
300 | } | 299 | } |
301 | 300 | ||
302 | // Since layout change may show/hide showcase, the current focused map may need to go into/outside showcase | 301 | // Since layout change may show/hide showcase, the current focused map may need to go into/outside showcase |
303 | // Reset attribute triggers MutationObserver which is observing it | 302 | // Reset attribute triggers MutationObserver which is observing it |
304 | const focusMap = | 303 | const focusMap = |
305 | container.querySelector('.mapclay.focus') ?? | 304 | container.querySelector('.mapclay.focus') ?? |
306 | container.querySelector('.mapclay'); | 305 | container.querySelector('.mapclay') |
307 | focusMap?.classList?.add('focus'); | 306 | focusMap?.classList?.add('focus') |
308 | }); | 307 | }) |
309 | layoutObserver.observe(container, { | 308 | layoutObserver.observe(container, { |
310 | attributes: true, | 309 | attributes: true, |
311 | attributeFilter: ['data-layout'], | 310 | attributeFilter: ['data-layout'], |
312 | attributeOldValue: true, | 311 | attributeOldValue: true, |
313 | characterDataOldValue: true, | 312 | characterDataOldValue: true |
314 | }); | 313 | }) |
315 | 314 | ||
316 | onRemove(htmlHolder, () => layoutObserver.disconnect()); | 315 | onRemove(htmlHolder, () => layoutObserver.disconnect()) |
317 | //}}} | 316 | // }}} |
318 | //}}} | 317 | // }}} |
319 | // Render Maps {{{ | 318 | // Render Maps {{{ |
320 | 319 | ||
321 | const afterMapRendered = renderer => { | 320 | const afterMapRendered = renderer => { |
322 | const mapElement = renderer.target; | 321 | const mapElement = renderer.target |
323 | //FIXME | 322 | // FIXME |
324 | mapElement.renderer = renderer; | 323 | mapElement.renderer = renderer |
325 | mapElement.setAttribute('tabindex', '-1'); | 324 | mapElement.setAttribute('tabindex', '-1') |
326 | if (mapElement.getAttribute('data-render') === 'fulfilled') { | 325 | if (mapElement.getAttribute('data-render') === 'fulfilled') { |
327 | mapCache[mapElement.id] = renderer; | 326 | mapCache[mapElement.id] = renderer |
328 | } | 327 | } |
329 | 328 | ||
330 | // Execute callback from caller | 329 | // Execute callback from caller |
331 | mapCallback?.call(this, mapElement); | 330 | mapCallback?.call(this, mapElement) |
332 | const markers = geoLinks | 331 | const markers = geoLinks |
333 | .filter(link => !link.targets || link.targets.includes(mapElement.id)) | 332 | .filter(link => !link.targets || link.targets.includes(mapElement.id)) |
334 | .map(link => ({ xy: link.xy, title: link.url.pathname })); | 333 | .map(link => ({ xy: link.xy, title: link.url.pathname })) |
335 | 334 | ||
336 | // Add markers with Geolinks | 335 | // Add markers with Geolinks |
337 | renderer.addMarkers(markers); | 336 | renderer.addMarkers(markers) |
338 | mapElement | 337 | mapElement |
339 | .querySelectorAll('.marker') | 338 | .querySelectorAll('.marker') |
340 | .forEach(marker => htmlHolder.anchors.push(marker)); | 339 | .forEach(marker => htmlHolder.anchors.push(marker)) |
341 | 340 | ||
342 | // Work with Mutation Observer | 341 | // Work with Mutation Observer |
343 | const observer = mapFocusObserver(); | 342 | const observer = mapFocusObserver() |
344 | mapFocusObserver().observe(mapElement, { | 343 | mapFocusObserver().observe(mapElement, { |
345 | attributes: true, | 344 | attributes: true, |
346 | attributeFilter: ['class'], | 345 | attributeFilter: ['class'], |
347 | attributeOldValue: true, | 346 | attributeOldValue: true |
348 | }); | 347 | }) |
349 | onRemove(mapElement, () => observer.disconnect()); | 348 | onRemove(mapElement, () => observer.disconnect()) |
350 | }; | 349 | } |
351 | 350 | ||
352 | // Set unique ID for map container | 351 | // Set unique ID for map container |
353 | const mapIdList = []; | 352 | const mapIdList = [] |
354 | const assignMapId = config => { | 353 | const assignMapId = config => { |
355 | let mapId = config.id; | 354 | let mapId = config.id |
356 | if (!mapId) { | 355 | if (!mapId) { |
357 | mapId = config.use?.split('/')?.at(-1); | 356 | mapId = config.use?.split('/')?.at(-1) |
358 | let counter = 1; | 357 | let counter = 1 |
359 | while (!mapId || mapIdList.includes(mapId)) { | 358 | while (!mapId || mapIdList.includes(mapId)) { |
360 | mapId = `${config.use ?? 'unnamed'}-${counter}`; | 359 | mapId = `${config.use ?? 'unnamed'}-${counter}` |
361 | counter++; | 360 | counter++ |
362 | } | 361 | } |
363 | config.id = mapId; | 362 | config.id = mapId |
364 | } | 363 | } |
365 | mapIdList.push(mapId); | 364 | mapIdList.push(mapId) |
366 | return config; | 365 | return config |
367 | }; | 366 | } |
368 | 367 | ||
369 | // Render each code block with "language-map" class | 368 | // Render each code block with "language-map" class |
370 | const elementsWithMapConfig = Array.from( | 369 | const elementsWithMapConfig = Array.from( |
371 | container.querySelectorAll('pre:has(.language-map)') ?? [], | 370 | container.querySelectorAll('pre:has(.language-map)') ?? [] |
372 | ); | 371 | ) |
373 | /** | 372 | /** |
374 | * updateAttributeByStep. | 373 | * updateAttributeByStep. |
375 | * | 374 | * |
@@ -377,21 +376,21 @@ export const generateMaps = (container, { delay, mapCallback }) => { | |||
377 | */ | 376 | */ |
378 | const updateAttributeByStep = ({ results, target, steps }) => { | 377 | const updateAttributeByStep = ({ results, target, steps }) => { |
379 | let passNum = results.filter( | 378 | let passNum = results.filter( |
380 | r => r.type === 'render' && r.state.match(/success|skip/), | 379 | r => r.type === 'render' && r.state.match(/success|skip/) |
381 | ).length; | 380 | ).length |
382 | const total = steps.length; | 381 | const total = steps.length |
383 | passNum += `/${total}`; | 382 | passNum += `/${total}` |
384 | if (results.filter(r => r.type === 'render').length === total) { | 383 | if (results.filter(r => r.type === 'render').length === total) { |
385 | passNum += '\u0020'; | 384 | passNum += '\u0020' |
386 | } | 385 | } |
387 | 386 | ||
388 | // FIXME HACK use MutationObserver for animation | 387 | // FIXME HACK use MutationObserver for animation |
389 | if (!target.animations) target.animations = Promise.resolve(); | 388 | if (!target.animations) target.animations = Promise.resolve() |
390 | target.animations = target.animations.then(async () => { | 389 | target.animations = target.animations.then(async () => { |
391 | await new Promise(resolve => setTimeout(resolve, 100)); | 390 | await new Promise(resolve => setTimeout(resolve, 100)) |
392 | target.setAttribute('data-report', passNum); | 391 | target.setAttribute('data-report', passNum) |
393 | }); | 392 | }) |
394 | }; | 393 | } |
395 | /** | 394 | /** |
396 | * config converter for mapclay.renderWith() | 395 | * config converter for mapclay.renderWith() |
397 | * | 396 | * |
@@ -404,42 +403,42 @@ export const generateMaps = (container, { delay, mapCallback }) => { | |||
404 | ...config, | 403 | ...config, |
405 | aliases: { | 404 | aliases: { |
406 | ...defaultAliases, | 405 | ...defaultAliases, |
407 | ...(config.aliases ?? {}), | 406 | ...(config.aliases ?? {}) |
408 | }, | 407 | }, |
409 | stepCallback: updateAttributeByStep, | 408 | stepCallback: updateAttributeByStep |
410 | }); | 409 | }) |
411 | const render = renderWith(configConverter); | 410 | const render = renderWith(configConverter) |
412 | elementsWithMapConfig.forEach(target => { | 411 | elementsWithMapConfig.forEach(target => { |
413 | // Get text in code block starts with markdown text '```map' | 412 | // Get text in code block starts with markdown text '```map' |
414 | const configText = target | 413 | const configText = target |
415 | .querySelector('.language-map') | 414 | .querySelector('.language-map') |
416 | .textContent // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content | 415 | .textContent // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content |
417 | // replace it by normal space | 416 | // replace it by normal space |
418 | .replace(/\u00A0/g, '\u0020'); | 417 | .replace(/\u00A0/g, '\u0020') |
419 | 418 | ||
420 | let configList = []; | 419 | let configList = [] |
421 | try { | 420 | try { |
422 | configList = parseConfigsFromYaml(configText).map(assignMapId); | 421 | configList = parseConfigsFromYaml(configText).map(assignMapId) |
423 | } catch (_) { | 422 | } catch (_) { |
424 | console.warn('Fail to parse yaml config for element', target); | 423 | console.warn('Fail to parse yaml config for element', target) |
425 | return; | 424 | return |
426 | } | 425 | } |
427 | 426 | ||
428 | // If map in cache has the same ID, just put it into target | 427 | // If map in cache has the same ID, just put it into target |
429 | // So user won't feel anything changes when editing markdown | 428 | // So user won't feel anything changes when editing markdown |
430 | configList.forEach(config => { | 429 | configList.forEach(config => { |
431 | const cache = mapCache[config.id]; | 430 | const cache = mapCache[config.id] |
432 | if (!cache) return; | 431 | if (!cache) return |
433 | 432 | ||
434 | target.appendChild(cache.target); | 433 | target.appendChild(cache.target) |
435 | config.target = cache.target; | 434 | config.target = cache.target |
436 | }); | 435 | }) |
437 | 436 | ||
438 | // trivial: if map cache is applied, do not show yaml text | 437 | // trivial: if map cache is applied, do not show yaml text |
439 | if (target.querySelector('.mapclay')) { | 438 | if (target.querySelector('.mapclay')) { |
440 | target | 439 | target |
441 | .querySelectorAll(':scope > :not([data-render=fulfilled])') | 440 | .querySelectorAll(':scope > :not([data-render=fulfilled])') |
442 | .forEach(e => e.remove()); | 441 | .forEach(e => e.remove()) |
443 | } | 442 | } |
444 | 443 | ||
445 | // TODO Use debounce of user input to decide rendering timing | 444 | // TODO Use debounce of user input to decide rendering timing |
@@ -447,76 +446,76 @@ export const generateMaps = (container, { delay, mapCallback }) => { | |||
447 | const timer = setTimeout( | 446 | const timer = setTimeout( |
448 | () => | 447 | () => |
449 | render(target, configList).forEach(renderPromise => { | 448 | render(target, configList).forEach(renderPromise => { |
450 | renderPromises.push(renderPromise); | 449 | renderPromises.push(renderPromise) |
451 | renderPromise.then(afterMapRendered); | 450 | renderPromise.then(afterMapRendered) |
452 | }), | 451 | }), |
453 | delay ?? 1000, | 452 | delay ?? 1000 |
454 | ); | 453 | ) |
455 | onRemove(htmlHolder, () => { | 454 | onRemove(htmlHolder, () => { |
456 | clearTimeout(timer); | 455 | clearTimeout(timer) |
457 | }); | 456 | }) |
458 | }); | 457 | }) |
459 | // }}} | 458 | // }}} |
460 | // Menu {{{ | 459 | // Menu {{{ |
461 | const menu = document.createElement('div'); | 460 | const menu = document.createElement('div') |
462 | menu.className = 'menu'; | 461 | menu.className = 'menu' |
463 | menu.style.display = 'none'; | 462 | menu.style.display = 'none' |
464 | menu.onclick = () => (menu.style.display = 'none'); | 463 | menu.onclick = () => (menu.style.display = 'none') |
465 | container.appendChild(menu); | 464 | container.appendChild(menu) |
466 | 465 | ||
467 | // Menu Items | 466 | // Menu Items |
468 | container.oncontextmenu = e => { | 467 | container.oncontextmenu = e => { |
469 | menu.replaceChildren(); | 468 | menu.replaceChildren() |
470 | menu.style.display = 'block'; | 469 | menu.style.display = 'block' |
471 | menu.style.cssText = `left: ${e.x - menu.offsetParent.offsetLeft + 10}px; top: ${e.y - menu.offsetParent.offsetTop + 5}px;`; | 470 | menu.style.cssText = `left: ${e.x - menu.offsetParent.offsetLeft + 10}px; top: ${e.y - menu.offsetParent.offsetTop + 5}px;` |
472 | e.preventDefault(); | 471 | e.preventDefault() |
473 | 472 | ||
474 | // GeoLinks | 473 | // GeoLinks |
475 | const selection = document.getSelection(); | 474 | const selection = document.getSelection() |
476 | if (selection.type === 'Range') { | 475 | if (selection.type === 'Range') { |
477 | const range = selection.getRangeAt(0); | 476 | const range = selection.getRangeAt(0) |
478 | menu.appendChild(menuItem.addGeoLink(dumbymap, range)); | 477 | menu.appendChild(menuItem.addGeoLink(dumbymap, range)) |
479 | } | 478 | } |
480 | 479 | ||
481 | // Menu Items for map | 480 | // Menu Items for map |
482 | const map = e.target.closest('.mapclay'); | 481 | const map = e.target.closest('.mapclay') |
483 | if (map?.renderer?.results) { | 482 | if (map?.renderer?.results) { |
484 | // Focus or Print Map Results | 483 | // Focus or Print Map Results |
485 | menu.appendChild(menuItem.toggleMapFocus(map)); | 484 | menu.appendChild(menuItem.toggleMapFocus(map)) |
486 | menu.appendChild(menuItem.renderResults(dumbymap, map)); | 485 | menu.appendChild(menuItem.renderResults(dumbymap, map)) |
487 | } else { | 486 | } else { |
488 | // Toggle block focus | 487 | // Toggle block focus |
489 | const block = e.target.closest('.dumby-block'); | 488 | const block = e.target.closest('.dumby-block') |
490 | if (block) { | 489 | if (block) { |
491 | menu.appendChild(menuItem.toggleBlockFocus(block)); | 490 | menu.appendChild(menuItem.toggleBlockFocus(block)) |
492 | } | 491 | } |
493 | } | 492 | } |
494 | 493 | ||
495 | // Menu Items for map/block/layout | 494 | // Menu Items for map/block/layout |
496 | if (!map || map.closest('.Showcase')) { | 495 | if (!map || map.closest('.Showcase')) { |
497 | menu.appendChild(menuItem.pickMapItem(dumbymap)); | 496 | menu.appendChild(menuItem.pickMapItem(dumbymap)) |
498 | menu.appendChild(menuItem.pickBlockItem(dumbymap)); | 497 | menu.appendChild(menuItem.pickBlockItem(dumbymap)) |
499 | menu.appendChild(menuItem.pickLayoutItem(dumbymap)); | 498 | menu.appendChild(menuItem.pickLayoutItem(dumbymap)) |
500 | } | 499 | } |
501 | }; | 500 | } |
502 | 501 | ||
503 | // Remove menu when click outside | 502 | // Remove menu when click outside |
504 | const actionOutsideMenu = e => { | 503 | const actionOutsideMenu = e => { |
505 | if (menu.style.display === 'none') return; | 504 | if (menu.style.display === 'none') return |
506 | const rect = menu.getBoundingClientRect(); | 505 | const rect = menu.getBoundingClientRect() |
507 | if ( | 506 | if ( |
508 | e.clientX < rect.left || | 507 | e.clientX < rect.left || |
509 | e.clientX > rect.left + rect.width || | 508 | e.clientX > rect.left + rect.width || |
510 | e.clientY < rect.top || | 509 | e.clientY < rect.top || |
511 | e.clientY > rect.top + rect.height | 510 | e.clientY > rect.top + rect.height |
512 | ) { | 511 | ) { |
513 | menu.style.display = 'none'; | 512 | menu.style.display = 'none' |
514 | } | 513 | } |
515 | }; | 514 | } |
516 | document.addEventListener('click', actionOutsideMenu); | 515 | document.addEventListener('click', actionOutsideMenu) |
517 | onRemove(htmlHolder, () => | 516 | onRemove(htmlHolder, () => |
518 | document.removeEventListener('click', actionOutsideMenu), | 517 | document.removeEventListener('click', actionOutsideMenu) |
519 | ); | 518 | ) |
520 | //}}} | 519 | // }}} |
521 | return Object.seal(dumbymap); | 520 | return Object.seal(dumbymap) |
522 | }; | 521 | } |
diff --git a/src/editor.mjs b/src/editor.mjs index 6f529ed..4c50e79 100644 --- a/src/editor.mjs +++ b/src/editor.mjs | |||
@@ -1,44 +1,44 @@ | |||
1 | /*global EasyMDE*/ | 1 | /* global EasyMDE */ |
2 | /*eslint no-undef: "error"*/ | 2 | /* eslint no-undef: "error" */ |
3 | import { markdown2HTML, generateMaps } from './dumbymap'; | 3 | import { markdown2HTML, generateMaps } from './dumbymap' |
4 | import { defaultAliases, parseConfigsFromYaml } from 'mapclay'; | 4 | import { defaultAliases, parseConfigsFromYaml } from 'mapclay' |
5 | import * as menuItem from './MenuItem'; | 5 | import * as menuItem from './MenuItem' |
6 | 6 | ||
7 | // Set up Containers {{{ | 7 | // Set up Containers {{{ |
8 | 8 | ||
9 | const HtmlContainer = document.querySelector('.DumbyMap'); | 9 | const HtmlContainer = document.querySelector('.DumbyMap') |
10 | const textArea = document.querySelector('.editor textarea'); | 10 | const textArea = document.querySelector('.editor textarea') |
11 | let dumbymap; | 11 | let dumbymap |
12 | 12 | ||
13 | new MutationObserver(mutations => { | 13 | new window.MutationObserver(mutations => { |
14 | const mutation = mutations.at(-1); | 14 | const mutation = mutations.at(-1) |
15 | const mode = mutation.target.getAttribute('data-mode'); | 15 | const mode = mutation.target.getAttribute('data-mode') |
16 | const layout = HtmlContainer.getAttribute('data-layout'); | 16 | const layout = HtmlContainer.getAttribute('data-layout') |
17 | if (mode === 'editing' && layout !== 'normal') { | 17 | if (mode === 'editing' && layout !== 'normal') { |
18 | HtmlContainer.setAttribute('data-layout', 'normal'); | 18 | HtmlContainer.setAttribute('data-layout', 'normal') |
19 | } | 19 | } |
20 | }).observe(document.querySelector('[data-mode]'), { | 20 | }).observe(document.querySelector('[data-mode]'), { |
21 | attributes: true, | 21 | attributes: true, |
22 | attributeFilter: ['data-mode'], | 22 | attributeFilter: ['data-mode'], |
23 | attributeOldValue: true, | 23 | attributeOldValue: true |
24 | }); | 24 | }) |
25 | const toggleEditing = () => { | 25 | const toggleEditing = () => { |
26 | const mode = document.body.getAttribute('data-mode'); | 26 | const mode = document.body.getAttribute('data-mode') |
27 | document.body.setAttribute('data-mode', mode === 'editing' ? '' : 'editing'); | 27 | document.body.setAttribute('data-mode', mode === 'editing' ? '' : 'editing') |
28 | }; | 28 | } |
29 | // }}} | 29 | // }}} |
30 | // Set up EasyMDE {{{ | 30 | // Set up EasyMDE {{{ |
31 | 31 | ||
32 | // Content values for editor | 32 | // Content values for editor |
33 | 33 | ||
34 | const defaultContent = | 34 | const defaultContent = |
35 | '## 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'; | 35 | '## 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' |
36 | const editor = new EasyMDE({ | 36 | const editor = new EasyMDE({ |
37 | element: textArea, | 37 | element: textArea, |
38 | initialValue: defaultContent, | 38 | initialValue: defaultContent, |
39 | autosave: { | 39 | autosave: { |
40 | enabled: true, | 40 | enabled: true, |
41 | uniqueId: 'dumbymap', | 41 | uniqueId: 'dumbymap' |
42 | }, | 42 | }, |
43 | indentWithTabs: false, | 43 | indentWithTabs: false, |
44 | lineNumbers: true, | 44 | lineNumbers: true, |
@@ -51,105 +51,105 @@ const editor = new EasyMDE({ | |||
51 | map: 'Ctrl-Alt-M', | 51 | map: 'Ctrl-Alt-M', |
52 | debug: 'Ctrl-Alt-D', | 52 | debug: 'Ctrl-Alt-D', |
53 | toggleUnorderedList: null, | 53 | toggleUnorderedList: null, |
54 | toggleOrderedList: null, | 54 | toggleOrderedList: null |
55 | }, | 55 | }, |
56 | toolbar: [ | 56 | toolbar: [ |
57 | { | 57 | { |
58 | name: 'map', | 58 | name: 'map', |
59 | title: 'Toggle Map Generation', | 59 | title: 'Toggle Map Generation', |
60 | text: '🌏', | 60 | text: '🌏', |
61 | action: () => toggleEditing(), | 61 | action: () => toggleEditing() |
62 | }, | 62 | }, |
63 | { | 63 | { |
64 | name: 'debug', | 64 | name: 'debug', |
65 | title: 'Save content as URL', | 65 | title: 'Save content as URL', |
66 | text: '🤔', | 66 | text: '🤔', |
67 | action: () => { | 67 | action: () => { |
68 | const state = { content: editor.value() }; | 68 | const state = { content: editor.value() } |
69 | window.location.hash = encodeURIComponent(JSON.stringify(state)); | 69 | window.location.hash = encodeURIComponent(JSON.stringify(state)) |
70 | navigator.clipboard.writeText(window.location.href); | 70 | navigator.clipboard.writeText(window.location.href) |
71 | alert('URL copied to clipboard'); | 71 | window.alert('URL copied to clipboard') |
72 | }, | 72 | } |
73 | }, | 73 | }, |
74 | { | 74 | { |
75 | name: 'undo', | 75 | name: 'undo', |
76 | title: 'Undo last editing', | 76 | title: 'Undo last editing', |
77 | text: '\u27F2', | 77 | text: '\u27F2', |
78 | action: EasyMDE.undo, | 78 | action: EasyMDE.undo |
79 | }, | 79 | }, |
80 | { | 80 | { |
81 | name: 'redo', | 81 | name: 'redo', |
82 | text: '\u27F3', | 82 | text: '\u27F3', |
83 | title: 'Redo editing', | 83 | title: 'Redo editing', |
84 | action: EasyMDE.redo, | 84 | action: EasyMDE.redo |
85 | }, | 85 | }, |
86 | '|', | 86 | '|', |
87 | { | 87 | { |
88 | name: 'heading-1', | 88 | name: 'heading-1', |
89 | text: 'H1', | 89 | text: 'H1', |
90 | title: 'Big Heading', | 90 | title: 'Big Heading', |
91 | action: EasyMDE['heading-1'], | 91 | action: EasyMDE['heading-1'] |
92 | }, | 92 | }, |
93 | { | 93 | { |
94 | name: 'heading-2', | 94 | name: 'heading-2', |
95 | text: 'H2', | 95 | text: 'H2', |
96 | title: 'Medium Heading', | 96 | title: 'Medium Heading', |
97 | action: EasyMDE['heading-2'], | 97 | action: EasyMDE['heading-2'] |
98 | }, | 98 | }, |
99 | '|', | 99 | '|', |
100 | { | 100 | { |
101 | name: 'link', | 101 | name: 'link', |
102 | text: '\u{1F517}', | 102 | text: '\u{1F517}', |
103 | title: 'Create Link', | 103 | title: 'Create Link', |
104 | action: EasyMDE.drawLink, | 104 | action: EasyMDE.drawLink |
105 | }, | 105 | }, |
106 | { | 106 | { |
107 | name: 'image', | 107 | name: 'image', |
108 | text: '\u{1F5BC}', | 108 | text: '\u{1F5BC}', |
109 | title: 'Create Image', | 109 | title: 'Create Image', |
110 | action: EasyMDE.drawImage, | 110 | action: EasyMDE.drawImage |
111 | }, | 111 | }, |
112 | '|', | 112 | '|', |
113 | { | 113 | { |
114 | name: 'Bold', | 114 | name: 'Bold', |
115 | text: '\u{1D401}', | 115 | text: '\u{1D401}', |
116 | title: 'Bold', | 116 | title: 'Bold', |
117 | action: EasyMDE.toggleBold, | 117 | action: EasyMDE.toggleBold |
118 | }, | 118 | }, |
119 | { | 119 | { |
120 | name: 'Italic', | 120 | name: 'Italic', |
121 | text: '\u{1D43C}', | 121 | text: '\u{1D43C}', |
122 | title: 'Italic', | 122 | title: 'Italic', |
123 | action: EasyMDE.toggleItalic, | 123 | action: EasyMDE.toggleItalic |
124 | }, | 124 | } |
125 | ], | 125 | ] |
126 | }); | 126 | }) |
127 | 127 | ||
128 | const cm = editor.codemirror; | 128 | const cm = editor.codemirror |
129 | 129 | ||
130 | const getStateFromHash = hash => { | 130 | const getStateFromHash = hash => { |
131 | const hashValue = hash.substring(1); | 131 | const hashValue = hash.substring(1) |
132 | const stateString = decodeURIComponent(hashValue); | 132 | const stateString = decodeURIComponent(hashValue) |
133 | try { | 133 | try { |
134 | return JSON.parse(stateString) ?? {}; | 134 | return JSON.parse(stateString) ?? {} |
135 | } catch (_) { | 135 | } catch (_) { |
136 | return {}; | 136 | return {} |
137 | } | 137 | } |
138 | }; | 138 | } |
139 | 139 | ||
140 | const getContentFromHash = hash => { | 140 | const getContentFromHash = hash => { |
141 | const state = getStateFromHash(hash); | 141 | const state = getStateFromHash(hash) |
142 | return state.content; | 142 | return state.content |
143 | }; | 143 | } |
144 | 144 | ||
145 | const initialState = getStateFromHash(window.location.hash); | 145 | const initialState = getStateFromHash(window.location.hash) |
146 | window.location.hash = ''; | 146 | window.location.hash = '' |
147 | const contentFromHash = initialState.content; | 147 | const contentFromHash = initialState.content |
148 | 148 | ||
149 | // Seems like autosave would overwrite initialValue, set content from hash here | 149 | // Seems like autosave would overwrite initialValue, set content from hash here |
150 | if (contentFromHash) { | 150 | if (contentFromHash) { |
151 | editor.cleanup(); | 151 | editor.cleanup() |
152 | editor.value(contentFromHash); | 152 | editor.value(contentFromHash) |
153 | } | 153 | } |
154 | // }}} | 154 | // }}} |
155 | // Set up logic about editor content {{{ | 155 | // Set up logic about editor content {{{ |
@@ -159,30 +159,30 @@ const afterMapRendered = _ => { | |||
159 | // const lonLat = mapHolder.renderer.unproject([event.x, event.y]) | 159 | // const lonLat = mapHolder.renderer.unproject([event.x, event.y]) |
160 | // // TODO... | 160 | // // TODO... |
161 | // } | 161 | // } |
162 | }; | 162 | } |
163 | markdown2HTML(HtmlContainer, editor.value()); | 163 | markdown2HTML(HtmlContainer, editor.value()) |
164 | dumbymap = generateMaps(HtmlContainer, afterMapRendered); | 164 | dumbymap = generateMaps(HtmlContainer, afterMapRendered) |
165 | 165 | ||
166 | // Quick hack to style lines inside code block | 166 | // Quick hack to style lines inside code block |
167 | const addClassToCodeLines = () => { | 167 | const addClassToCodeLines = () => { |
168 | const lines = cm.getLineHandle(0).parent.lines; | 168 | const lines = cm.getLineHandle(0).parent.lines |
169 | let insideCodeBlock = false; | 169 | let insideCodeBlock = false |
170 | lines.forEach((line, index) => { | 170 | lines.forEach((line, index) => { |
171 | if (line.text.match(/^[\u0060]{3}/)) { | 171 | if (line.text.match(/^[\u0060]{3}/)) { |
172 | insideCodeBlock = !insideCodeBlock; | 172 | insideCodeBlock = !insideCodeBlock |
173 | } else if (insideCodeBlock) { | 173 | } else if (insideCodeBlock) { |
174 | cm.addLineClass(index, 'text', 'inside-code-block'); | 174 | cm.addLineClass(index, 'text', 'inside-code-block') |
175 | } else { | 175 | } else { |
176 | cm.removeLineClass(index, 'text', 'inside-code-block'); | 176 | cm.removeLineClass(index, 'text', 'inside-code-block') |
177 | } | 177 | } |
178 | }); | 178 | }) |
179 | }; | 179 | } |
180 | addClassToCodeLines(); | 180 | addClassToCodeLines() |
181 | 181 | ||
182 | const completeForCodeBlock = change => { | 182 | const completeForCodeBlock = change => { |
183 | const line = change.to.line; | 183 | const line = change.to.line |
184 | if (change.origin === '+input') { | 184 | if (change.origin === '+input') { |
185 | const text = change.text[0]; | 185 | const text = change.text[0] |
186 | 186 | ||
187 | // Completion for YAML doc separator | 187 | // Completion for YAML doc separator |
188 | if ( | 188 | if ( |
@@ -190,21 +190,21 @@ const completeForCodeBlock = change => { | |||
190 | change.to.ch === 0 && | 190 | change.to.ch === 0 && |
191 | insideCodeblockForMap(cm.getCursor()) | 191 | insideCodeblockForMap(cm.getCursor()) |
192 | ) { | 192 | ) { |
193 | cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 1 }); | 193 | cm.setSelection({ line, ch: 0 }, { line, ch: 1 }) |
194 | cm.replaceSelection(text.repeat(3) + '\n'); | 194 | cm.replaceSelection(text.repeat(3) + '\n') |
195 | } | 195 | } |
196 | 196 | ||
197 | // Completion for Code fence | 197 | // Completion for Code fence |
198 | if (text === '`' && change.to.ch === 0) { | 198 | if (text === '`' && change.to.ch === 0) { |
199 | cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 1 }); | 199 | cm.setSelection({ line, ch: 0 }, { line, ch: 1 }) |
200 | cm.replaceSelection(text.repeat(3)); | 200 | cm.replaceSelection(text.repeat(3)) |
201 | const numberOfFences = cm | 201 | const numberOfFences = cm |
202 | .getValue() | 202 | .getValue() |
203 | .split('\n') | 203 | .split('\n') |
204 | .filter(line => line.match(/[\u0060]{3}/)).length; | 204 | .filter(line => line.match(/[\u0060]{3}/)).length |
205 | if (numberOfFences % 2 === 1) { | 205 | if (numberOfFences % 2 === 1) { |
206 | cm.replaceSelection('map\n\n```'); | 206 | cm.replaceSelection('map\n\n```') |
207 | cm.setCursor({ line: line + 1 }); | 207 | cm.setCursor({ line: line + 1 }) |
208 | } | 208 | } |
209 | } | 209 | } |
210 | } | 210 | } |
@@ -212,63 +212,64 @@ const completeForCodeBlock = change => { | |||
212 | // For YAML doc separator, <hr> and code fence | 212 | // For YAML doc separator, <hr> and code fence |
213 | // Auto delete to start of line | 213 | // Auto delete to start of line |
214 | if (change.origin === '+delete') { | 214 | if (change.origin === '+delete') { |
215 | const match = change.removed[0].match(/^[-\u0060]$/)?.at(0); | 215 | const match = change.removed[0].match(/^[-\u0060]$/)?.at(0) |
216 | if (match && cm.getLine(line) === match.repeat(2) && match) { | 216 | if (match && cm.getLine(line) === match.repeat(2) && match) { |
217 | cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 2 }); | 217 | cm.setSelection({ line, ch: 0 }, { line, ch: 2 }) |
218 | cm.replaceSelection(''); | 218 | cm.replaceSelection('') |
219 | } | 219 | } |
220 | } | 220 | } |
221 | }; | 221 | } |
222 | |||
223 | const debounceForMap = (() => { | ||
224 | let timer = null; | ||
225 | 222 | ||
226 | return function (...args) { | 223 | /* Disable debounce temporarily */ |
227 | dumbymap = generateMaps.apply(this, args); | 224 | // const debounceForMap = (() => { |
228 | // clearTimeout(timer); | 225 | // const timer = null |
229 | // timer = setTimeout(() => { | 226 | // |
230 | // dumbymap = generateMaps.apply(this, args) | 227 | // return function (...args) { |
231 | // }, 10); | 228 | // dumbymap = generateMaps.apply(this, args) |
232 | }; | 229 | // clearTimeout(timer); |
233 | })(); | 230 | // timer = setTimeout(() => { |
231 | // dumbymap = generateMaps.apply(this, args) | ||
232 | // }, 10); | ||
233 | // } | ||
234 | // })() | ||
234 | 235 | ||
235 | const updateDumbyMap = () => { | 236 | const updateDumbyMap = () => { |
236 | markdown2HTML(HtmlContainer, editor.value()); | 237 | markdown2HTML(HtmlContainer, editor.value()) |
237 | // TODO Test if generate maps intantly is OK with map cache | 238 | // TODO Test if generate maps intantly is OK with map cache |
238 | // debounceForMap(HtmlContainer, afterMapRendered) | 239 | // debounceForMap(HtmlContainer, afterMapRendered) |
239 | dumbymap = generateMaps(HtmlContainer, afterMapRendered); | 240 | dumbymap = generateMaps(HtmlContainer, afterMapRendered) |
240 | }; | 241 | } |
241 | 242 | ||
242 | updateDumbyMap(); | 243 | updateDumbyMap() |
243 | 244 | ||
244 | // Re-render HTML by editor content | 245 | // Re-render HTML by editor content |
245 | cm.on('change', (_, change) => { | 246 | cm.on('change', (_, change) => { |
246 | updateDumbyMap(); | 247 | updateDumbyMap() |
247 | addClassToCodeLines(); | 248 | addClassToCodeLines() |
248 | completeForCodeBlock(change); | 249 | completeForCodeBlock(change) |
249 | }); | 250 | }) |
250 | 251 | ||
251 | // Set class for focus | 252 | // Set class for focus |
252 | cm.on('focus', () => { | 253 | cm.on('focus', () => { |
253 | cm.getWrapperElement().classList.add('focus'); | 254 | cm.getWrapperElement().classList.add('focus') |
254 | HtmlContainer.classList.remove('focus'); | 255 | HtmlContainer.classList.remove('focus') |
255 | }); | 256 | }) |
256 | 257 | ||
257 | cm.on('beforeChange', (_, change) => { | 258 | cm.on('beforeChange', (_, change) => { |
258 | const line = change.to.line; | 259 | const line = change.to.line |
259 | // Don't allow more content after YAML doc separator | 260 | // Don't allow more content after YAML doc separator |
260 | if (change.origin.match(/^(\+input|paste)$/)) { | 261 | if (change.origin.match(/^(\+input|paste)$/)) { |
261 | if (cm.getLine(line) === '---' && change.text[0] !== '') { | 262 | if (cm.getLine(line) === '---' && change.text[0] !== '') { |
262 | change.cancel(); | 263 | change.cancel() |
263 | } | 264 | } |
264 | } | 265 | } |
265 | }); | 266 | }) |
266 | 267 | ||
267 | // Reload editor content by hash value | 268 | // Reload editor content by hash value |
268 | window.onhashchange = () => { | 269 | window.onhashchange = () => { |
269 | const content = getContentFromHash(window.location.hash); | 270 | const content = getContentFromHash(window.location.hash) |
270 | if (content) editor.value(content); | 271 | if (content) editor.value(content) |
271 | }; | 272 | } |
272 | 273 | ||
273 | // FIXME DEBUGONLY | 274 | // FIXME DEBUGONLY |
274 | // generateMaps(HtmlContainer) | 275 | // generateMaps(HtmlContainer) |
@@ -279,159 +280,159 @@ window.onhashchange = () => { | |||
279 | // }}} | 280 | // }}} |
280 | // Completion in Code Blok {{{ | 281 | // Completion in Code Blok {{{ |
281 | // Elements about suggestions {{{ | 282 | // Elements about suggestions {{{ |
282 | const menu = document.createElement('div'); | 283 | const menu = document.createElement('div') |
283 | menu.className = 'menu editor-menu'; | 284 | menu.className = 'menu editor-menu' |
284 | menu.style.display = 'none'; | 285 | menu.style.display = 'none' |
285 | menu.onclick = () => (menu.style.display = 'none'); | 286 | menu.onclick = () => (menu.style.display = 'none') |
286 | new MutationObserver(() => { | 287 | new window.MutationObserver(() => { |
287 | if (menu.style.display === 'none') { | 288 | if (menu.style.display === 'none') { |
288 | menu.replaceChildren(); | 289 | menu.replaceChildren() |
289 | } | 290 | } |
290 | }).observe(menu, { | 291 | }).observe(menu, { |
291 | attributes: true, | 292 | attributes: true, |
292 | attributeFilter: ['style'], | 293 | attributeFilter: ['style'] |
293 | }); | 294 | }) |
294 | document.body.append(menu); | 295 | document.body.append(menu) |
295 | 296 | ||
296 | const rendererOptions = {}; | 297 | const rendererOptions = {} |
297 | 298 | ||
298 | // }}} | 299 | // }}} |
299 | // Aliases for map options {{{ | 300 | // Aliases for map options {{{ |
300 | const aliasesForMapOptions = {}; | 301 | const aliasesForMapOptions = {} |
301 | const defaultApply = './dist/default.yml'; | 302 | const defaultApply = './dist/default.yml' |
302 | fetch(defaultApply) | 303 | fetch(defaultApply) |
303 | .then(res => res.text()) | 304 | .then(res => res.text()) |
304 | .then(rawText => { | 305 | .then(rawText => { |
305 | const config = parseConfigsFromYaml(rawText)?.at(0); | 306 | const config = parseConfigsFromYaml(rawText)?.at(0) |
306 | Object.assign(aliasesForMapOptions, config.aliases ?? {}); | 307 | Object.assign(aliasesForMapOptions, config.aliases ?? {}) |
307 | }) | 308 | }) |
308 | .catch(err => console.warn(`Fail to get aliases from ${defaultApply}`, err)); | 309 | .catch(err => console.warn(`Fail to get aliases from ${defaultApply}`, err)) |
309 | // }}} | 310 | // }}} |
310 | // FUNCTION: Check if current token is inside code block {{{ | 311 | // FUNCTION: Check if current token is inside code block {{{ |
311 | const insideCodeblockForMap = anchor => { | 312 | const insideCodeblockForMap = anchor => { |
312 | const token = cm.getTokenAt(anchor); | 313 | const token = cm.getTokenAt(anchor) |
313 | const insideCodeBlock = | 314 | const insideCodeBlock = |
314 | token.state.overlay.codeBlock && | 315 | token.state.overlay.codeBlock && |
315 | !cm.getLine(anchor.line).match(/^[\u0060]{3}/); | 316 | !cm.getLine(anchor.line).match(/^[\u0060]{3}/) |
316 | if (!insideCodeBlock) return false; | 317 | if (!insideCodeBlock) return false |
317 | 318 | ||
318 | let line = anchor.line - 1; | 319 | let line = anchor.line - 1 |
319 | while (line >= 0) { | 320 | while (line >= 0) { |
320 | const content = cm.getLine(line); | 321 | const content = cm.getLine(line) |
321 | if (content === '```map') { | 322 | if (content === '```map') { |
322 | return true; | 323 | return true |
323 | } else if (content === '```') { | 324 | } else if (content === '```') { |
324 | return false; | 325 | return false |
325 | } | 326 | } |
326 | line = line - 1; | 327 | line = line - 1 |
327 | } | 328 | } |
328 | return false; | 329 | return false |
329 | }; | 330 | } |
330 | // }}} | 331 | // }}} |
331 | // FUNCTION: Get Renderer by cursor position in code block {{{ | 332 | // FUNCTION: Get Renderer by cursor position in code block {{{ |
332 | const getLineWithRenderer = anchor => { | 333 | const getLineWithRenderer = anchor => { |
333 | const currentLine = anchor.line; | 334 | const currentLine = anchor.line |
334 | if (!cm.getLine) return null; | 335 | if (!cm.getLine) return null |
335 | 336 | ||
336 | const match = line => cm.getLine(line).match(/^use: /); | 337 | const match = line => cm.getLine(line).match(/^use: /) |
337 | 338 | ||
338 | if (match(currentLine)) return currentLine; | 339 | if (match(currentLine)) return currentLine |
339 | 340 | ||
340 | // Look backward/forward for pattern of used renderer: /use: .+/ | 341 | // Look backward/forward for pattern of used renderer: /use: .+/ |
341 | let pl = currentLine - 1; | 342 | let pl = currentLine - 1 |
342 | while (pl > 0 && insideCodeblockForMap(anchor)) { | 343 | while (pl > 0 && insideCodeblockForMap(anchor)) { |
343 | const text = cm.getLine(pl); | 344 | const text = cm.getLine(pl) |
344 | if (match(pl)) { | 345 | if (match(pl)) { |
345 | return pl; | 346 | return pl |
346 | } else if (text.match(/^---|^[\u0060]{3}/)) { | 347 | } else if (text.match(/^---|^[\u0060]{3}/)) { |
347 | break; | 348 | break |
348 | } | 349 | } |
349 | pl = pl - 1; | 350 | pl = pl - 1 |
350 | } | 351 | } |
351 | 352 | ||
352 | let nl = currentLine + 1; | 353 | let nl = currentLine + 1 |
353 | while (insideCodeblockForMap(anchor)) { | 354 | while (insideCodeblockForMap(anchor)) { |
354 | const text = cm.getLine(nl); | 355 | const text = cm.getLine(nl) |
355 | if (match(nl)) { | 356 | if (match(nl)) { |
356 | return nl; | 357 | return nl |
357 | } else if (text.match(/^---|^[\u0060]{3}/)) { | 358 | } else if (text.match(/^---|^[\u0060]{3}/)) { |
358 | return null; | 359 | return null |
359 | } | 360 | } |
360 | nl = nl + 1; | 361 | nl = nl + 1 |
361 | } | 362 | } |
362 | 363 | ||
363 | return null; | 364 | return null |
364 | }; | 365 | } |
365 | // }}} | 366 | // }}} |
366 | // FUNCTION: Return suggestions for valid options {{{ | 367 | // FUNCTION: Return suggestions for valid options {{{ |
367 | const getSuggestionsForOptions = (optionTyped, validOptions) => { | 368 | const getSuggestionsForOptions = (optionTyped, validOptions) => { |
368 | let suggestOptions = []; | 369 | let suggestOptions = [] |
369 | 370 | ||
370 | const matchedOptions = validOptions.filter(o => | 371 | const matchedOptions = validOptions.filter(o => |
371 | o.valueOf().toLowerCase().includes(optionTyped.toLowerCase()), | 372 | o.valueOf().toLowerCase().includes(optionTyped.toLowerCase()) |
372 | ); | 373 | ) |
373 | 374 | ||
374 | if (matchedOptions.length > 0) { | 375 | if (matchedOptions.length > 0) { |
375 | suggestOptions = matchedOptions; | 376 | suggestOptions = matchedOptions |
376 | } else { | 377 | } else { |
377 | suggestOptions = validOptions; | 378 | suggestOptions = validOptions |
378 | } | 379 | } |
379 | 380 | ||
380 | return suggestOptions.map( | 381 | return suggestOptions.map( |
381 | o => | 382 | o => |
382 | new menuItem.Suggestion({ | 383 | new menuItem.Suggestion({ |
383 | text: `<span>${o.valueOf()}</span><span class='info' title="${o.desc ?? ''}">ⓘ</span>`, | 384 | text: `<span>${o.valueOf()}</span><span class='info' title="${o.desc ?? ''}">ⓘ</span>`, |
384 | replace: `${o.valueOf()}: `, | 385 | replace: `${o.valueOf()}: ` |
385 | }), | 386 | }) |
386 | ); | 387 | ) |
387 | }; | 388 | } |
388 | // }}} | 389 | // }}} |
389 | // FUNCTION: Return suggestion for example of option value {{{ | 390 | // FUNCTION: Return suggestion for example of option value {{{ |
390 | const getSuggestionFromMapOption = option => { | 391 | const getSuggestionFromMapOption = option => { |
391 | if (!option.example) return null; | 392 | if (!option.example) return null |
392 | 393 | ||
393 | const text = option.example_desc | 394 | const text = option.example_desc |
394 | ? `<span>${option.example_desc}</span><span class="truncate"style="color: gray">${option.example}</span>` | 395 | ? `<span>${option.example_desc}</span><span class="truncate"style="color: gray">${option.example}</span>` |
395 | : `<span>${option.example}</span>`; | 396 | : `<span>${option.example}</span>` |
396 | 397 | ||
397 | return new menuItem.Suggestion({ | 398 | return new menuItem.Suggestion({ |
398 | text: text, | 399 | text, |
399 | replace: `${option.valueOf()}: ${option.example ?? ''}`, | 400 | replace: `${option.valueOf()}: ${option.example ?? ''}` |
400 | }); | 401 | }) |
401 | }; | 402 | } |
402 | // }}} | 403 | // }}} |
403 | // FUNCTION: Return suggestions from aliases {{{ | 404 | // FUNCTION: Return suggestions from aliases {{{ |
404 | const getSuggestionsFromAliases = option => | 405 | const getSuggestionsFromAliases = option => |
405 | Object.entries(aliasesForMapOptions[option.valueOf()] ?? {})?.map(record => { | 406 | Object.entries(aliasesForMapOptions[option.valueOf()] ?? {})?.map(record => { |
406 | const [alias, value] = record; | 407 | const [alias, value] = record |
407 | const valueString = JSON.stringify(value).replaceAll('"', ''); | 408 | const valueString = JSON.stringify(value).replaceAll('"', '') |
408 | return new menuItem.Suggestion({ | 409 | return new menuItem.Suggestion({ |
409 | text: `<span>${alias}</span><span class="truncate" style="color: gray">${valueString}</span>`, | 410 | text: `<span>${alias}</span><span class="truncate" style="color: gray">${valueString}</span>`, |
410 | replace: `${option.valueOf()}: ${valueString}`, | 411 | replace: `${option.valueOf()}: ${valueString}` |
411 | }); | 412 | }) |
412 | }) ?? []; | 413 | }) ?? [] |
413 | // }}} | 414 | // }}} |
414 | // FUCNTION: Handler for map codeblock {{{ | 415 | // FUCNTION: Handler for map codeblock {{{ |
415 | const handleTypingInCodeBlock = anchor => { | 416 | const handleTypingInCodeBlock = anchor => { |
416 | const text = cm.getLine(anchor.line); | 417 | const text = cm.getLine(anchor.line) |
417 | if (text.match(/^\s\+$/) && text.length % 2 !== 0) { | 418 | if (text.match(/^\s\+$/) && text.length % 2 !== 0) { |
418 | // TODO Completion for even number of spaces | 419 | // TODO Completion for even number of spaces |
419 | } else if (text.match(/^-/)) { | 420 | } else if (text.match(/^-/)) { |
420 | // TODO Completion for YAML doc separator | 421 | // TODO Completion for YAML doc separator |
421 | } else { | 422 | } else { |
422 | const suggestions = getSuggestions(anchor); | 423 | const suggestions = getSuggestions(anchor) |
423 | addSuggestions(anchor, suggestions); | 424 | addSuggestions(anchor, suggestions) |
424 | } | 425 | } |
425 | }; | 426 | } |
426 | // }}} | 427 | // }}} |
427 | // FUNCTION: get suggestions by current input {{{ | 428 | // FUNCTION: get suggestions by current input {{{ |
428 | const getSuggestions = anchor => { | 429 | const getSuggestions = anchor => { |
429 | const text = cm.getLine(anchor.line); | 430 | const text = cm.getLine(anchor.line) |
430 | 431 | ||
431 | // Clear marks on text | 432 | // Clear marks on text |
432 | cm.findMarks({ ...anchor, ch: 0 }, { ...anchor, ch: text.length }).forEach( | 433 | cm.findMarks({ ...anchor, ch: 0 }, { ...anchor, ch: text.length }).forEach( |
433 | m => m.clear(), | 434 | m => m.clear() |
434 | ); | 435 | ) |
435 | 436 | ||
436 | // Mark user input invalid by case | 437 | // Mark user input invalid by case |
437 | const markInputIsInvalid = () => | 438 | const markInputIsInvalid = () => |
@@ -440,231 +441,230 @@ const getSuggestions = anchor => { | |||
440 | .markText( | 441 | .markText( |
441 | { ...anchor, ch: 0 }, | 442 | { ...anchor, ch: 0 }, |
442 | { ...anchor, ch: text.length }, | 443 | { ...anchor, ch: text.length }, |
443 | { className: 'invalid-input' }, | 444 | { className: 'invalid-input' } |
444 | ); | 445 | ) |
445 | 446 | ||
446 | // Check if "use: <renderer>" is set | 447 | // Check if "use: <renderer>" is set |
447 | const lineWithRenderer = getLineWithRenderer(anchor); | 448 | const lineWithRenderer = getLineWithRenderer(anchor) |
448 | const renderer = lineWithRenderer | 449 | const renderer = lineWithRenderer |
449 | ? cm.getLine(lineWithRenderer).split(' ')[1] | 450 | ? cm.getLine(lineWithRenderer).split(' ')[1] |
450 | : null; | 451 | : null |
451 | if (renderer && anchor.line !== lineWithRenderer) { | 452 | if (renderer && anchor.line !== lineWithRenderer) { |
452 | // Do not check properties | 453 | // Do not check properties |
453 | if (text.startsWith(' ')) return []; | 454 | if (text.startsWith(' ')) return [] |
454 | 455 | ||
455 | // If no valid options for current used renderer, go get it! | 456 | // If no valid options for current used renderer, go get it! |
456 | const validOptions = rendererOptions[renderer]; | 457 | const validOptions = rendererOptions[renderer] |
457 | if (!validOptions) { | 458 | if (!validOptions) { |
458 | // Get list of valid options for current renderer | 459 | // Get list of valid options for current renderer |
459 | const rendererUrl = defaultAliases.use[renderer]?.value; | 460 | const rendererUrl = defaultAliases.use[renderer]?.value |
460 | import(rendererUrl) | 461 | import(rendererUrl) |
461 | .then(rendererModule => { | 462 | .then(rendererModule => { |
462 | rendererOptions[renderer] = rendererModule.default.validOptions; | 463 | rendererOptions[renderer] = rendererModule.default.validOptions |
463 | const currentAnchor = cm.getCursor(); | 464 | const currentAnchor = cm.getCursor() |
464 | if (insideCodeblockForMap(currentAnchor)) { | 465 | if (insideCodeblockForMap(currentAnchor)) { |
465 | handleTypingInCodeBlock(currentAnchor); | 466 | handleTypingInCodeBlock(currentAnchor) |
466 | } | 467 | } |
467 | }) | 468 | }) |
468 | .catch(_ => { | 469 | .catch(_ => { |
469 | markInputIsInvalid(lineWithRenderer); | 470 | markInputIsInvalid(lineWithRenderer) |
470 | console.warn( | 471 | console.warn( |
471 | `Fail to get valid options from Renderer typed: ${renderer}`, | 472 | `Fail to get valid options from Renderer typed: ${renderer}` |
472 | ); | 473 | ) |
473 | }); | 474 | }) |
474 | return []; | 475 | return [] |
475 | } | 476 | } |
476 | 477 | ||
477 | // If input is "key:value" (no space left after colon), then it is invalid | 478 | // If input is "key:value" (no space left after colon), then it is invalid |
478 | const isKeyFinished = text.includes(':'); | 479 | const isKeyFinished = text.includes(':') |
479 | const isValidKeyValue = text.match(/^[^:]+:\s+/); | 480 | const isValidKeyValue = text.match(/^[^:]+:\s+/) |
480 | if (isKeyFinished && !isValidKeyValue) { | 481 | if (isKeyFinished && !isValidKeyValue) { |
481 | markInputIsInvalid(); | 482 | markInputIsInvalid() |
482 | return []; | 483 | return [] |
483 | } | 484 | } |
484 | 485 | ||
485 | // If user is typing option | 486 | // If user is typing option |
486 | const keyTyped = text.split(':')[0].trim(); | 487 | const keyTyped = text.split(':')[0].trim() |
487 | if (!isKeyFinished) { | 488 | if (!isKeyFinished) { |
488 | markInputIsInvalid(); | 489 | markInputIsInvalid() |
489 | return getSuggestionsForOptions(keyTyped, validOptions); | 490 | return getSuggestionsForOptions(keyTyped, validOptions) |
490 | } | 491 | } |
491 | 492 | ||
492 | // If user is typing value | 493 | // If user is typing value |
493 | const matchedOption = validOptions.find(o => o.name === keyTyped); | 494 | const matchedOption = validOptions.find(o => o.name === keyTyped) |
494 | if (isKeyFinished && !matchedOption) { | 495 | if (isKeyFinished && !matchedOption) { |
495 | markInputIsInvalid(); | 496 | markInputIsInvalid() |
496 | } | 497 | } |
497 | 498 | ||
498 | if (isKeyFinished && matchedOption) { | 499 | if (isKeyFinished && matchedOption) { |
499 | const valueTyped = text.substring(text.indexOf(':') + 1).trim(); | 500 | const valueTyped = text.substring(text.indexOf(':') + 1).trim() |
500 | const isValidValue = matchedOption.isValid(valueTyped); | 501 | const isValidValue = matchedOption.isValid(valueTyped) |
501 | if (!valueTyped) { | 502 | if (!valueTyped) { |
502 | return [ | 503 | return [ |
503 | getSuggestionFromMapOption(matchedOption), | 504 | getSuggestionFromMapOption(matchedOption), |
504 | ...getSuggestionsFromAliases(matchedOption), | 505 | ...getSuggestionsFromAliases(matchedOption) |
505 | ].filter(s => s instanceof menuItem.Suggestion); | 506 | ].filter(s => s instanceof menuItem.Suggestion) |
506 | } | 507 | } |
507 | if (valueTyped && !isValidValue) { | 508 | if (valueTyped && !isValidValue) { |
508 | markInputIsInvalid(); | 509 | markInputIsInvalid() |
509 | return []; | 510 | return [] |
510 | } | 511 | } |
511 | } | 512 | } |
512 | } else { | 513 | } else { |
513 | // Suggestion for "use" | 514 | // Suggestion for "use" |
514 | const rendererSuggestions = Object.entries(defaultAliases.use) | 515 | const rendererSuggestions = Object.entries(defaultAliases.use) |
515 | .filter(([renderer]) => { | 516 | .filter(([renderer]) => { |
516 | const suggestion = `use: ${renderer}`; | 517 | const suggestion = `use: ${renderer}` |
517 | const suggestionPattern = suggestion.replace(' ', '').toLowerCase(); | 518 | const suggestionPattern = suggestion.replace(' ', '').toLowerCase() |
518 | const textPattern = text.replace(' ', '').toLowerCase(); | 519 | const textPattern = text.replace(' ', '').toLowerCase() |
519 | return suggestion !== text && suggestionPattern.includes(textPattern); | 520 | return suggestion !== text && suggestionPattern.includes(textPattern) |
520 | }) | 521 | }) |
521 | .map( | 522 | .map( |
522 | ([renderer, info]) => | 523 | ([renderer, info]) => |
523 | new menuItem.Suggestion({ | 524 | new menuItem.Suggestion({ |
524 | text: `<span>use: ${renderer}</span><span class='info' title="${info.desc}">ⓘ</span>`, | 525 | text: `<span>use: ${renderer}</span><span class='info' title="${info.desc}">ⓘ</span>`, |
525 | replace: `use: ${renderer}`, | 526 | replace: `use: ${renderer}` |
526 | }), | 527 | }) |
527 | ); | 528 | ) |
528 | return rendererSuggestions.length > 0 ? rendererSuggestions : []; | 529 | return rendererSuggestions.length > 0 ? rendererSuggestions : [] |
529 | } | 530 | } |
530 | return []; | 531 | return [] |
531 | }; | 532 | } |
532 | // }}} | 533 | // }}} |
533 | // {{{ FUNCTION: Show element about suggestions | 534 | // {{{ FUNCTION: Show element about suggestions |
534 | const addSuggestions = (anchor, suggestions) => { | 535 | const addSuggestions = (anchor, suggestions) => { |
535 | if (suggestions.length === 0) { | 536 | if (suggestions.length === 0) { |
536 | menu.style.display = 'none'; | 537 | menu.style.display = 'none' |
537 | return; | 538 | return |
538 | } else { | 539 | } else { |
539 | menu.style.display = 'block'; | 540 | menu.style.display = 'block' |
540 | } | 541 | } |
541 | 542 | ||
542 | menu.innerHTML = ''; | 543 | menu.innerHTML = '' |
543 | suggestions | 544 | suggestions |
544 | .map(s => s.createElement(cm)) | 545 | .map(s => s.createElement(cm)) |
545 | .forEach(option => menu.appendChild(option)); | 546 | .forEach(option => menu.appendChild(option)) |
546 | 547 | ||
547 | const widgetAnchor = document.createElement('div'); | 548 | const widgetAnchor = document.createElement('div') |
548 | cm.addWidget(anchor, widgetAnchor, true); | 549 | cm.addWidget(anchor, widgetAnchor, true) |
549 | const rect = widgetAnchor.getBoundingClientRect(); | 550 | const rect = widgetAnchor.getBoundingClientRect() |
550 | menu.style.left = `calc(${rect.left}px + 2rem)`; | 551 | menu.style.left = `calc(${rect.left}px + 2rem)` |
551 | menu.style.top = `calc(${rect.bottom}px + 1rem)`; | 552 | menu.style.top = `calc(${rect.bottom}px + 1rem)` |
552 | menu.style.maxWidth = `calc(${window.innerWidth}px - ${rect.x}px - 3rem)`; | 553 | menu.style.maxWidth = `calc(${window.innerWidth}px - ${rect.x}px - 3rem)` |
553 | menu.style.display = 'block'; | 554 | menu.style.display = 'block' |
554 | }; | 555 | } |
555 | // }}} | 556 | // }}} |
556 | // EVENT: Suggests for current selection {{{ | 557 | // EVENT: Suggests for current selection {{{ |
557 | // FIXME Dont show suggestion when selecting multiple chars | 558 | // FIXME Dont show suggestion when selecting multiple chars |
558 | cm.on('cursorActivity', _ => { | 559 | cm.on('cursorActivity', _ => { |
559 | menu.style.display = 'none'; | 560 | menu.style.display = 'none' |
560 | const anchor = cm.getCursor(); | 561 | const anchor = cm.getCursor() |
561 | 562 | ||
562 | if (insideCodeblockForMap(anchor)) { | 563 | if (insideCodeblockForMap(anchor)) { |
563 | handleTypingInCodeBlock(anchor); | 564 | handleTypingInCodeBlock(anchor) |
564 | } | 565 | } |
565 | }); | 566 | }) |
566 | cm.on('blur', () => { | 567 | cm.on('blur', () => { |
567 | if (menu.checkVisibility()) { | 568 | if (menu.checkVisibility()) { |
568 | cm.focus() | 569 | cm.focus() |
569 | } else { | 570 | } else { |
570 | cm.getWrapperElement().classList.remove('focus'); | 571 | cm.getWrapperElement().classList.remove('focus') |
571 | HtmlContainer.classList.add('focus'); | 572 | HtmlContainer.classList.add('focus') |
572 | } | 573 | } |
573 | }); | 574 | }) |
574 | // }}} | 575 | // }}} |
575 | // EVENT: keydown for suggestions {{{ | 576 | // EVENT: keydown for suggestions {{{ |
576 | const keyForSuggestions = ['Tab', 'Enter', 'Escape']; | 577 | const keyForSuggestions = ['Tab', 'Enter', 'Escape'] |
577 | cm.on('keydown', (_, e) => { | 578 | cm.on('keydown', (_, e) => { |
578 | if ( | 579 | if ( |
579 | !cm.hasFocus || | 580 | !cm.hasFocus || |
580 | !keyForSuggestions.includes(e.key) || | 581 | !keyForSuggestions.includes(e.key) || |
581 | menu.style.display === 'none' | 582 | menu.style.display === 'none' |
582 | ) | 583 | ) { return } |
583 | return; | ||
584 | 584 | ||
585 | // Directly add a newline when no suggestion is selected | 585 | // Directly add a newline when no suggestion is selected |
586 | const currentSuggestion = menu.querySelector('.container__suggestion.focus'); | 586 | const currentSuggestion = menu.querySelector('.container__suggestion.focus') |
587 | if (!currentSuggestion && e.key === 'Enter') return; | 587 | if (!currentSuggestion && e.key === 'Enter') return |
588 | 588 | ||
589 | // Override default behavior | 589 | // Override default behavior |
590 | e.preventDefault(); | 590 | e.preventDefault() |
591 | 591 | ||
592 | // Suggestion when pressing Tab or Shift + Tab | 592 | // Suggestion when pressing Tab or Shift + Tab |
593 | const nextSuggestion = | 593 | const nextSuggestion = |
594 | currentSuggestion?.nextSibling ?? | 594 | currentSuggestion?.nextSibling ?? |
595 | menu.querySelector('.container__suggestion:first-child'); | 595 | menu.querySelector('.container__suggestion:first-child') |
596 | const previousSuggestion = | 596 | const previousSuggestion = |
597 | currentSuggestion?.previousSibling ?? | 597 | currentSuggestion?.previousSibling ?? |
598 | menu.querySelector('.container__suggestion:last-child'); | 598 | menu.querySelector('.container__suggestion:last-child') |
599 | const focusSuggestion = e.shiftKey ? previousSuggestion : nextSuggestion; | 599 | const focusSuggestion = e.shiftKey ? previousSuggestion : nextSuggestion |
600 | 600 | ||
601 | // Current editor selection state | 601 | // Current editor selection state |
602 | switch (e.key) { | 602 | switch (e.key) { |
603 | case 'Tab': | 603 | case 'Tab': |
604 | Array.from(menu.children).forEach(s => s.classList.remove('focus')); | 604 | Array.from(menu.children).forEach(s => s.classList.remove('focus')) |
605 | focusSuggestion.classList.add('focus'); | 605 | focusSuggestion.classList.add('focus') |
606 | focusSuggestion.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); | 606 | focusSuggestion.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) |
607 | break; | 607 | break |
608 | case 'Enter': | 608 | case 'Enter': |
609 | currentSuggestion.onclick(); | 609 | currentSuggestion.onclick() |
610 | break; | 610 | break |
611 | case 'Escape': | 611 | case 'Escape': |
612 | if (!menu.checkVisibility()) break; | 612 | if (!menu.checkVisibility()) break |
613 | // HACK delay menu display change for blur event, mark cm focus should keep | 613 | // HACK delay menu display change for blur event, mark cm focus should keep |
614 | setTimeout(() => (menu.style.display = 'none'), 50); | 614 | setTimeout(() => (menu.style.display = 'none'), 50) |
615 | break; | 615 | break |
616 | } | 616 | } |
617 | }); | 617 | }) |
618 | 618 | ||
619 | document.onkeydown = e => { | 619 | document.onkeydown = e => { |
620 | if (e.altKey && e.ctrlKey && e.key === 'm') { | 620 | if (e.altKey && e.ctrlKey && e.key === 'm') { |
621 | toggleEditing(); | 621 | toggleEditing() |
622 | e.preventDefault(); | 622 | e.preventDefault() |
623 | return null; | 623 | return null |
624 | } | 624 | } |
625 | 625 | ||
626 | if (!cm.hasFocus()) { | 626 | if (!cm.hasFocus()) { |
627 | if (e.key === 'F1') { | 627 | if (e.key === 'F1') { |
628 | e.preventDefault(); | 628 | e.preventDefault() |
629 | cm.focus(); | 629 | cm.focus() |
630 | } | 630 | } |
631 | if (e.key === 'Tab') { | 631 | if (e.key === 'Tab') { |
632 | e.preventDefault(); | 632 | e.preventDefault() |
633 | dumbymap.utils.focusNextMap(e.shiftKey); | 633 | dumbymap.utils.focusNextMap(e.shiftKey) |
634 | } | 634 | } |
635 | if (e.key === 'x' || e.key === 'X') { | 635 | if (e.key === 'x' || e.key === 'X') { |
636 | e.preventDefault(); | 636 | e.preventDefault() |
637 | dumbymap.utils.switchToNextLayout(e.shiftKey); | 637 | dumbymap.utils.switchToNextLayout(e.shiftKey) |
638 | } | 638 | } |
639 | if (e.key === 'n') { | 639 | if (e.key === 'n') { |
640 | e.preventDefault(); | 640 | e.preventDefault() |
641 | dumbymap.utils.focusNextBlock(); | 641 | dumbymap.utils.focusNextBlock() |
642 | } | 642 | } |
643 | if (e.key === 'p') { | 643 | if (e.key === 'p') { |
644 | e.preventDefault(); | 644 | e.preventDefault() |
645 | dumbymap.utils.focusNextBlock(true); | 645 | dumbymap.utils.focusNextBlock(true) |
646 | } | 646 | } |
647 | if (e.key === 'Escape') { | 647 | if (e.key === 'Escape') { |
648 | e.preventDefault(); | 648 | e.preventDefault() |
649 | dumbymap.utils.removeBlockFocus(); | 649 | dumbymap.utils.removeBlockFocus() |
650 | } | 650 | } |
651 | } | 651 | } |
652 | }; | 652 | } |
653 | 653 | ||
654 | // }}} | 654 | // }}} |
655 | // }}} | 655 | // }}} |
656 | // Layout Switch {{{ | 656 | // Layout Switch {{{ |
657 | new MutationObserver(mutaions => { | 657 | new window.MutationObserver(mutaions => { |
658 | const mutation = mutaions.at(-1); | 658 | const mutation = mutaions.at(-1) |
659 | const layout = HtmlContainer.getAttribute('data-layout'); | 659 | const layout = HtmlContainer.getAttribute('data-layout') |
660 | if (layout !== 'normal' || mutation.oldValue === 'normal') { | 660 | if (layout !== 'normal' || mutation.oldValue === 'normal') { |
661 | document.body.setAttribute('data-mode', ''); | 661 | document.body.setAttribute('data-mode', '') |
662 | } | 662 | } |
663 | }).observe(HtmlContainer, { | 663 | }).observe(HtmlContainer, { |
664 | attributes: true, | 664 | attributes: true, |
665 | attributeFilter: ['data-layout'], | 665 | attributeFilter: ['data-layout'], |
666 | attributeOldValue: true, | 666 | attributeOldValue: true |
667 | }); | 667 | }) |
668 | // }}} | 668 | // }}} |
669 | 669 | ||
670 | // vim: sw=2 ts=2 foldmethod=marker foldmarker={{{,}}} | 670 | // vim: sw=2 ts=2 foldmethod=marker foldmarker={{{,}}} |
diff --git a/src/utils.mjs b/src/utils.mjs index 3824672..d9ed2d7 100644 --- a/src/utils.mjs +++ b/src/utils.mjs | |||
@@ -5,21 +5,21 @@ | |||
5 | * @param {Function} callback | 5 | * @param {Function} callback |
6 | */ | 6 | */ |
7 | export const onRemove = (element, callback) => { | 7 | export const onRemove = (element, callback) => { |
8 | const parent = element.parentNode; | 8 | const parent = element.parentNode |
9 | if (!parent) throw new Error('The node must already be attached'); | 9 | if (!parent) throw new Error('The node must already be attached') |
10 | 10 | ||
11 | const obs = new MutationObserver(mutations => { | 11 | const obs = new window.MutationObserver(mutations => { |
12 | for (const mutation of mutations) { | 12 | for (const mutation of mutations) { |
13 | for (const el of mutation.removedNodes) { | 13 | for (const el of mutation.removedNodes) { |
14 | if (el === element) { | 14 | if (el === element) { |
15 | obs.disconnect(); | 15 | obs.disconnect() |
16 | callback(); | 16 | callback() |
17 | } | 17 | } |
18 | } | 18 | } |
19 | } | 19 | } |
20 | }); | 20 | }) |
21 | obs.observe(parent, { childList: true }); | 21 | obs.observe(parent, { childList: true }) |
22 | }; | 22 | } |
23 | 23 | ||
24 | /** | 24 | /** |
25 | * Animate transition of DOMRect, with Web Animation API | 25 | * Animate transition of DOMRect, with Web Animation API |
@@ -32,39 +32,38 @@ export const onRemove = (element, callback) => { | |||
32 | * @returns {Animation} https://developer.mozilla.org/en-US/docs/Web/API/Animation | 32 | * @returns {Animation} https://developer.mozilla.org/en-US/docs/Web/API/Animation |
33 | */ | 33 | */ |
34 | export const animateRectTransition = (element, rect, options = {}) => { | 34 | export const animateRectTransition = (element, rect, options = {}) => { |
35 | if (!element.parentElement) | 35 | if (!element.parentElement) { throw new Error('The node must already be attached') } |
36 | throw new Error('The node must already be attached'); | ||
37 | 36 | ||
38 | const { width: w1, height: h1, left: x1, top: y1 } = rect; | 37 | const { width: w1, height: h1, left: x1, top: y1 } = rect |
39 | const { | 38 | const { |
40 | width: w2, | 39 | width: w2, |
41 | height: h2, | 40 | height: h2, |
42 | left: x2, | 41 | left: x2, |
43 | top: y2, | 42 | top: y2 |
44 | } = element.getBoundingClientRect(); | 43 | } = element.getBoundingClientRect() |
45 | 44 | ||
46 | const rw = (w1 ?? w2) / w2; | 45 | const rw = (w1 ?? w2) / w2 |
47 | const rh = (h1 ?? h2) / h2; | 46 | const rh = (h1 ?? h2) / h2 |
48 | const dx = x1 - x2; | 47 | const dx = x1 - x2 |
49 | const dy = y1 - y2; | 48 | const dy = y1 - y2 |
50 | 49 | ||
51 | if ((dx === 0 && dy === 0) || !isFinite(rw) || !isFinite(rh)) { | 50 | if ((dx === 0 && dy === 0) || !isFinite(rw) || !isFinite(rh)) { |
52 | return element.animate([], { duration: 0 }); | 51 | return element.animate([], { duration: 0 }) |
53 | } | 52 | } |
54 | 53 | ||
55 | const transform1 = `translate(0, 0) scale(1, 1)`; | 54 | const transform1 = 'translate(0, 0) scale(1, 1)' |
56 | const transform2 = `translate(${dx}px, ${dy}px) scale(${rw}, ${rh})`; | 55 | const transform2 = `translate(${dx}px, ${dy}px) scale(${rw}, ${rh})` |
57 | const keyframes = [ | 56 | const keyframes = [ |
58 | { transform: transform1, opacity: 1 }, | 57 | { transform: transform1, opacity: 1 }, |
59 | { transform: transform2, opacity: 0.3 }, | 58 | { transform: transform2, opacity: 0.3 } |
60 | ]; | 59 | ] |
61 | if (options.resume === true) keyframes.reverse(); | 60 | if (options.resume === true) keyframes.reverse() |
62 | 61 | ||
63 | return element.animate(keyframes, { | 62 | return element.animate(keyframes, { |
64 | duration: options.duration ?? 500, | 63 | duration: options.duration ?? 500, |
65 | easing: 'ease-in-out', | 64 | easing: 'ease-in-out' |
66 | }); | 65 | }) |
67 | }; | 66 | } |
68 | 67 | ||
69 | /** | 68 | /** |
70 | * Throttle for function call | 69 | * Throttle for function call |
@@ -73,18 +72,18 @@ export const animateRectTransition = (element, rect, options = {}) => { | |||
73 | * @param {Number} delay milliseconds | 72 | * @param {Number} delay milliseconds |
74 | * @returns {Any} return value of function call, or null if throttled | 73 | * @returns {Any} return value of function call, or null if throttled |
75 | */ | 74 | */ |
76 | export function throttle(func, delay) { | 75 | export function throttle (func, delay) { |
77 | let timerFlag = null; | 76 | let timerFlag = null |
78 | 77 | ||
79 | return function (...args) { | 78 | return function (...args) { |
80 | const context = this; | 79 | const context = this |
81 | if (timerFlag !== null) return null; | 80 | if (timerFlag !== null) return null |
82 | 81 | ||
83 | timerFlag = setTimeout( | 82 | timerFlag = setTimeout( |
84 | () => (timerFlag = null), | 83 | () => (timerFlag = null), |
85 | typeof delay === 'function' ? delay.call(context) : delay, | 84 | typeof delay === 'function' ? delay.call(context) : delay |
86 | ); | 85 | ) |
87 | 86 | ||
88 | return func.call(context, ...args); | 87 | return func.call(context, ...args) |
89 | }; | 88 | } |
90 | } | 89 | } |