|
1 | 1 | <script> |
2 | | - import { mount, onMount, tick, unmount } from 'svelte'; |
| 2 | + import { onMount, tick } from 'svelte'; |
3 | 3 | import Icon from './Icon.svelte'; |
4 | | - import { SvelteMap } from 'svelte/reactivity'; |
5 | 4 |
|
6 | | - let { |
7 | | - /** @type import('./public.d.ts').ResolvedConfig */ |
8 | | - config |
9 | | - } = $props(); |
10 | | - let open = $state(false); // Default to closed |
| 5 | + /** @type {{ config: import('./public.d.ts').ResolvedConfig }} */ |
| 6 | + let { config } = $props(); |
| 7 | + let open = $state(false); |
11 | 8 |
|
12 | | - /** @type {import('svelte').Component} */ |
13 | | - let ActiveComponent = $state(null); |
| 9 | + /** @type {import('svelte').Component | undefined} */ |
| 10 | + let ActiveComponent = $state(); |
14 | 11 | /** @type {HTMLElement} */ |
15 | | - let toolbar; |
| 12 | + let toolbarSelector; |
16 | 13 | /** @type {HTMLElement} */ |
17 | 14 | let toolbarPanels; |
| 15 | + /** @type {HTMLElement} */ |
| 16 | + let toolbarTools; |
18 | 17 |
|
19 | 18 | let dragOffsetX = 0; |
20 | 19 | let dragOffsetY = 0; |
| 20 | + let toolbarScreenOffset = 20; |
| 21 | +
|
| 22 | + /** @type {import('./public').Config['position']} */ |
| 23 | + let computedPosition = config.position; |
21 | 24 |
|
22 | 25 | onMount(() => { |
23 | | - toolbar.style.right = '20px'; |
24 | | - toolbar.style.bottom = '20px'; |
25 | | - recalculate_toolbar_panel_position(); |
| 26 | + computedPosition = config.position; |
| 27 | + layout_selector(); |
| 28 | + }); |
| 29 | +
|
| 30 | + $effect(() => { |
| 31 | + computedPosition = config.position; |
| 32 | + layout_selector(); |
| 33 | + layout_toolbar(); |
26 | 34 | }); |
27 | 35 |
|
| 36 | + function layout_selector() { |
| 37 | + const rect = toolbarSelector.getBoundingClientRect(); |
| 38 | +
|
| 39 | + let x = 0; |
| 40 | + let y = 0; |
| 41 | +
|
| 42 | + switch (computedPosition) { |
| 43 | + case 'top-left': |
| 44 | + x = toolbarScreenOffset; |
| 45 | + y = toolbarScreenOffset; |
| 46 | + break; |
| 47 | +
|
| 48 | + case 'top-right': |
| 49 | + x = window.innerWidth - rect.width - toolbarScreenOffset; |
| 50 | + y = toolbarScreenOffset; |
| 51 | + break; |
| 52 | +
|
| 53 | + case 'bottom-right': |
| 54 | + x = window.innerWidth - rect.width - toolbarScreenOffset; |
| 55 | + y = window.innerHeight - rect.height - toolbarScreenOffset; |
| 56 | + break; |
| 57 | +
|
| 58 | + case 'bottom-left': |
| 59 | + x = toolbarScreenOffset; |
| 60 | + y = window.innerHeight - rect.height - toolbarScreenOffset; |
| 61 | + break; |
| 62 | +
|
| 63 | + default: |
| 64 | + break; |
| 65 | + } |
| 66 | +
|
| 67 | + toolbarSelector.style.left = x + 'px'; |
| 68 | + toolbarSelector.style.top = y + 'px'; |
| 69 | + } |
| 70 | +
|
| 71 | + function layout_toolbar() { |
| 72 | + const toolbarSelectorRect = toolbarSelector.getBoundingClientRect(); |
| 73 | + const toolbarToolsRect = toolbarTools.getBoundingClientRect(); |
| 74 | + const toolbarPanelsRect = toolbarPanels.getBoundingClientRect(); |
| 75 | +
|
| 76 | + switch (computedPosition) { |
| 77 | + case 'top-left': |
| 78 | + toolbarTools.style.top = toolbarSelector.style.top; |
| 79 | + toolbarTools.style.left = |
| 80 | + parseFloat(toolbarSelector.style.left) + toolbarSelectorRect.width + 'px'; |
| 81 | +
|
| 82 | + toolbarPanels.style.top = |
| 83 | + parseFloat(toolbarSelector.style.top) + toolbarSelectorRect.height + 'px'; |
| 84 | + toolbarPanels.style.left = toolbarSelectorRect.x + 'px'; |
| 85 | + break; |
| 86 | +
|
| 87 | + case 'top-right': |
| 88 | + toolbarTools.style.top = toolbarSelector.style.top; |
| 89 | + toolbarTools.style.left = |
| 90 | + parseFloat(toolbarSelector.style.left) - toolbarToolsRect.width + 'px'; |
| 91 | +
|
| 92 | + toolbarPanels.style.top = |
| 93 | + parseFloat(toolbarSelector.style.top) + toolbarSelectorRect.height + 'px'; |
| 94 | + toolbarPanels.style.left = |
| 95 | + parseFloat(toolbarSelector.style.left) - |
| 96 | + toolbarPanelsRect.width + |
| 97 | + toolbarSelectorRect.width + |
| 98 | + 'px'; |
| 99 | + break; |
| 100 | +
|
| 101 | + case 'bottom-left': |
| 102 | + toolbarTools.style.top = toolbarSelector.style.top; |
| 103 | + toolbarTools.style.left = |
| 104 | + parseFloat(toolbarSelector.style.left) + toolbarSelectorRect.width + 'px'; |
| 105 | +
|
| 106 | + toolbarPanels.style.top = |
| 107 | + parseFloat(toolbarSelector.style.top) - toolbarPanelsRect.height + 'px'; |
| 108 | + toolbarPanels.style.left = toolbarSelectorRect.x + 'px'; |
| 109 | + break; |
| 110 | +
|
| 111 | + case 'bottom-right': |
| 112 | + toolbarTools.style.top = toolbarSelector.style.top; |
| 113 | + toolbarTools.style.left = |
| 114 | + parseFloat(toolbarSelector.style.left) - toolbarToolsRect.width + 'px'; |
| 115 | +
|
| 116 | + toolbarPanels.style.top = |
| 117 | + parseFloat(toolbarSelector.style.top) - toolbarPanelsRect.height + 'px'; |
| 118 | + toolbarPanels.style.left = |
| 119 | + parseFloat(toolbarSelector.style.left) - |
| 120 | + toolbarPanelsRect.width + |
| 121 | + toolbarSelectorRect.width + |
| 122 | + 'px'; |
| 123 | + break; |
| 124 | +
|
| 125 | + default: |
| 126 | + break; |
| 127 | + } |
| 128 | + } |
| 129 | +
|
28 | 130 | /** |
29 | 131 | * @param {import('./public').Tool} tool |
30 | 132 | */ |
31 | | - function toggle_tool(tool) { |
| 133 | + async function toggle_tool(tool) { |
32 | 134 | if (tool.component === ActiveComponent) { |
33 | 135 | ActiveComponent = undefined; |
34 | 136 | } else { |
|
37 | 139 |
|
38 | 140 | if (!ActiveComponent) toolbarPanels.style.display = 'none'; |
39 | 141 | else toolbarPanels.style.display = 'block'; |
| 142 | +
|
| 143 | + await tick(); |
| 144 | + layout_toolbar(); |
40 | 145 | } |
41 | 146 |
|
42 | 147 | /** |
43 | 148 | * @param {DragEvent} event |
44 | 149 | */ |
45 | 150 | function drag_start(event) { |
46 | | - const rect = toolbar.getBoundingClientRect(); |
| 151 | + const rect = toolbarSelector.getBoundingClientRect(); |
47 | 152 | dragOffsetX = event.clientX - rect.x; |
48 | 153 | dragOffsetY = event.clientY - rect.y; |
49 | 154 | } |
|
54 | 159 | function drag(event) { |
55 | 160 | if (event.clientX === 0 || event.clientY === 0) return; |
56 | 161 |
|
57 | | - const rect = toolbar.getBoundingClientRect(); |
| 162 | + const x = event.clientX - dragOffsetX; |
| 163 | + const y = event.clientY - dragOffsetY; |
| 164 | + toolbarSelector.style.left = x + 'px'; |
| 165 | + toolbarSelector.style.top = y + 'px'; |
| 166 | +
|
| 167 | + let top = false; |
| 168 | + let left = false; |
| 169 | +
|
| 170 | + if (y < window.innerHeight / 2) top = true; |
| 171 | + if (x < window.innerWidth / 2) left = true; |
58 | 172 |
|
59 | | - const x = window.innerWidth - event.clientX + dragOffsetX - rect.width; |
60 | | - const y = window.innerHeight - event.clientY + dragOffsetY - rect.height; |
61 | | - toolbar.style.right = x + 'px'; |
62 | | - toolbar.style.bottom = y + 'px'; |
| 173 | + if (top && left) computedPosition = 'top-left'; |
| 174 | + if (top && !left) computedPosition = 'top-right'; |
| 175 | + if (!top && left) computedPosition = 'bottom-left'; |
| 176 | + if (!top && !left) computedPosition = 'bottom-right'; |
63 | 177 |
|
64 | | - recalculate_toolbar_panel_position(); |
| 178 | + layout_toolbar(); |
65 | 179 | } |
66 | 180 |
|
67 | 181 | async function toggle_toolbar() { |
68 | 182 | open = !open; |
69 | 183 | await tick(); |
70 | | - recalculate_toolbar_panel_position(); |
71 | | - } |
72 | | -
|
73 | | - function recalculate_toolbar_panel_position() { |
74 | | - const rect = toolbar.getBoundingClientRect(); |
75 | | - toolbarPanels.style.right = toolbar.style.right; |
76 | | - toolbarPanels.style.bottom = parseFloat(toolbar.style.bottom ?? 0) + rect.height + 10 + 'px'; // Add a small gap |
77 | 184 | } |
78 | 185 | </script> |
79 | 186 |
|
80 | | -<svelte:window onresize={recalculate_toolbar_panel_position} /> |
81 | | -
|
82 | | -<div |
83 | | - class="svelte-toolbar" |
84 | | - bind:this={toolbar} |
85 | | - draggable="true" |
86 | | - ondrag={drag} |
87 | | - ondragstart={drag_start} |
88 | | - role="toolbar" |
89 | | - tabindex="-1" |
90 | | -> |
91 | | - {#if open} |
92 | | - <ul class="svelte-toolbar-tools"> |
93 | | - {#each config.tools as tool} |
94 | | - <li class:active={tool.component === ActiveComponent}> |
95 | | - <button onclick={() => toggle_tool(tool)} aria-label={tool.name}>{@html tool.icon}</button |
96 | | - > |
97 | | - </li> |
98 | | - {/each} |
99 | | - </ul> |
100 | | - {/if} |
101 | | - <button type="button" class="svelte-toolbar-selector" onclick={toggle_toolbar}> |
102 | | - <Icon /> |
103 | | - </button> |
104 | | -</div> |
105 | | -<div class="svelte-toolbar-panels" bind:this={toolbarPanels}> |
106 | | - {#if ActiveComponent} |
107 | | - <ActiveComponent /> |
108 | | - {/if} |
| 187 | +<svelte:window onresize={layout_toolbar} /> |
| 188 | + |
| 189 | +<div class="svelte-toolbar"> |
| 190 | + <div |
| 191 | + bind:this={toolbarSelector} |
| 192 | + draggable="true" |
| 193 | + ondrag={drag} |
| 194 | + ondragstart={drag_start} |
| 195 | + role="toolbar" |
| 196 | + tabindex="-1" |
| 197 | + class="svelte-toolbar-selector svelte-toolbar-base" |
| 198 | + > |
| 199 | + <button type="button" onclick={toggle_toolbar}> |
| 200 | + <Icon /> |
| 201 | + </button> |
| 202 | + </div> |
| 203 | + |
| 204 | + <div class="svelte-toolbar-tools svelte-toolbar-base" bind:this={toolbarTools}> |
| 205 | + {#if open} |
| 206 | + <ul> |
| 207 | + {#each config.tools as tool} |
| 208 | + <li class:active={tool.component === ActiveComponent}> |
| 209 | + <button onclick={() => toggle_tool(tool)} aria-label={tool.name}> |
| 210 | + {@html tool.icon} |
| 211 | + </button> |
| 212 | + </li> |
| 213 | + {/each} |
| 214 | + </ul> |
| 215 | + {/if} |
| 216 | + </div> |
| 217 | + |
| 218 | + <div class="svelte-toolbar-panels" bind:this={toolbarPanels}> |
| 219 | + {#if ActiveComponent} |
| 220 | + <ActiveComponent {config} /> |
| 221 | + {/if} |
| 222 | + </div> |
109 | 223 | </div> |
110 | 224 |
|
111 | 225 | <style> |
112 | 226 | .svelte-toolbar { |
113 | 227 | display: inline-flex; |
114 | 228 | background-color: var(--toolbar-background); |
115 | 229 | color: var(--toolbar-color); |
116 | | - position: fixed; |
117 | 230 | z-index: 1000; |
118 | 231 | border-radius: 8px; |
119 | 232 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); |
120 | | - padding: 8px; |
121 | 233 | } |
122 | 234 |
|
123 | 235 | .svelte-toolbar-selector { |
| 236 | + position: fixed; |
| 237 | + } |
| 238 | +
|
| 239 | + .svelte-toolbar-selector button { |
124 | 240 | cursor: pointer; |
125 | | - background: none; |
| 241 | + } |
| 242 | +
|
| 243 | + .svelte-toolbar-base { |
126 | 244 | border: none; |
127 | | - padding: 8px; |
128 | 245 | border-radius: 6px; |
129 | 246 | transition: background-color 0.2s ease-in-out; |
| 247 | + background-color: var(--toolbar-background); |
| 248 | + margin: 0; |
130 | 249 | } |
131 | 250 |
|
132 | 251 | .svelte-toolbar-selector:hover { |
|
140 | 259 | } |
141 | 260 |
|
142 | 261 | .svelte-toolbar-tools { |
| 262 | + position: fixed; |
| 263 | + background-color: var(--toolbar-background); |
| 264 | + } |
| 265 | +
|
| 266 | + .svelte-toolbar-tools ul { |
143 | 267 | list-style: none; |
144 | 268 | margin: 0; |
145 | | - padding: 0; |
| 269 | + padding: 8px; |
146 | 270 | display: flex; |
147 | 271 | align-items: center; |
148 | 272 | } |
|
204 | 328 | color: var(--toolbar-color); |
205 | 329 | } |
206 | 330 |
|
207 | | - :root { |
| 331 | + .svelte-toolbar-selector, |
| 332 | + .svelte-toolbar, |
| 333 | + .svelte-toolbar-panels { |
208 | 334 | --toolbar-background: #f0f0f0; |
209 | 335 | --toolbar-color: #222; |
210 | 336 | --toolbar-selector-hover-background: #e0e0e0; |
|
218 | 344 | } |
219 | 345 |
|
220 | 346 | @media (prefers-color-scheme: dark) { |
221 | | - :root { |
| 347 | + .svelte-toolbar-selector, |
| 348 | + .svelte-toolbar, |
| 349 | + .svelte-toolbar-panels { |
222 | 350 | --toolbar-background: #1e1e27; |
223 | 351 | --toolbar-color: white; |
224 | 352 | --toolbar-selector-hover-background: #333344; |
|
0 commit comments