@signalk/app-dock 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +11 -0
- package/.github/workflows/publish.yml +33 -0
- package/.prettierignore +6 -0
- package/.prettierrc +6 -0
- package/LICENSE +190 -0
- package/README.md +83 -0
- package/eslint.config.js +55 -0
- package/package.json +40 -0
- package/plugin/index.js +180 -0
- package/public/540.js +2 -0
- package/public/540.js.LICENSE.txt +9 -0
- package/public/805.js +1 -0
- package/public/app-icon.svg +29 -0
- package/public/config.html +469 -0
- package/public/dock.css +412 -0
- package/public/dock.js +456 -0
- package/public/icon-settings.svg +21 -0
- package/public/index.html +41 -0
- package/public/main.js +1 -0
- package/public/remoteEntry.js +1 -0
- package/src/configpanel/PluginConfigurationPanel.js +441 -0
- package/src/configpanel/index.js +1 -0
- package/test/plugin.test.js +394 -0
- package/webpack.config.js +39 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72" width="72" height="72">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
|
4
|
+
<stop offset="0%" stop-color="#1e3a5f"/>
|
|
5
|
+
<stop offset="100%" stop-color="#0a1628"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<!-- Background -->
|
|
9
|
+
<rect width="72" height="72" rx="16" fill="url(#bg)"/>
|
|
10
|
+
<!-- Dock pill -->
|
|
11
|
+
<rect x="8" y="48" width="56" height="16" rx="8" fill="#ffffff" fill-opacity="0.1" stroke="#ffffff" stroke-opacity="0.2" stroke-width="0.5"/>
|
|
12
|
+
<!-- Dock icons - 3 squares in the pill -->
|
|
13
|
+
<rect x="14" y="51" width="10" height="10" rx="2.5" fill="#3b82f6" opacity="0.9"/>
|
|
14
|
+
<rect x="31" y="51" width="10" height="10" rx="2.5" fill="#10b981" opacity="0.9"/>
|
|
15
|
+
<rect x="48" y="51" width="10" height="10" rx="2.5" fill="#f59e0b" opacity="0.9"/>
|
|
16
|
+
<!-- Middle icon magnified (popping up) -->
|
|
17
|
+
<rect x="28" y="34" width="16" height="16" rx="3.5" fill="#10b981" opacity="0.95"/>
|
|
18
|
+
<!-- Active dot under middle icon -->
|
|
19
|
+
<circle cx="36" cy="66" r="1.5" fill="#ffffff" fill-opacity="0.8"/>
|
|
20
|
+
<!-- Compass rose hint -->
|
|
21
|
+
<circle cx="36" cy="22" r="10" fill="none" stroke="#ffffff" stroke-opacity="0.15" stroke-width="1"/>
|
|
22
|
+
<line x1="36" y1="14" x2="36" y2="18" stroke="#ffffff" stroke-opacity="0.3" stroke-width="1" stroke-linecap="round"/>
|
|
23
|
+
<line x1="36" y1="26" x2="36" y2="30" stroke="#ffffff" stroke-opacity="0.15" stroke-width="1" stroke-linecap="round"/>
|
|
24
|
+
<line x1="28" y1="22" x2="32" y2="22" stroke="#ffffff" stroke-opacity="0.15" stroke-width="1" stroke-linecap="round"/>
|
|
25
|
+
<line x1="40" y1="22" x2="44" y2="22" stroke="#ffffff" stroke-opacity="0.15" stroke-width="1" stroke-linecap="round"/>
|
|
26
|
+
<!-- Compass needle -->
|
|
27
|
+
<polygon points="36,15 34,22 36,20 38,22" fill="#ef4444" fill-opacity="0.7"/>
|
|
28
|
+
<polygon points="36,29 34,22 36,24 38,22" fill="#ffffff" fill-opacity="0.25"/>
|
|
29
|
+
</svg>
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>App Dock - Configuration</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
}
|
|
13
|
+
body {
|
|
14
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
15
|
+
background: #1a1a2e;
|
|
16
|
+
color: #e0e0e0;
|
|
17
|
+
padding: 24px;
|
|
18
|
+
min-height: 100vh;
|
|
19
|
+
}
|
|
20
|
+
h1 {
|
|
21
|
+
font-size: 22px;
|
|
22
|
+
font-weight: 600;
|
|
23
|
+
margin-bottom: 8px;
|
|
24
|
+
}
|
|
25
|
+
.subtitle {
|
|
26
|
+
color: #888;
|
|
27
|
+
font-size: 13px;
|
|
28
|
+
margin-bottom: 24px;
|
|
29
|
+
}
|
|
30
|
+
.section {
|
|
31
|
+
margin-bottom: 32px;
|
|
32
|
+
}
|
|
33
|
+
.section-title {
|
|
34
|
+
font-size: 14px;
|
|
35
|
+
font-weight: 600;
|
|
36
|
+
color: #aaa;
|
|
37
|
+
text-transform: uppercase;
|
|
38
|
+
letter-spacing: 0.05em;
|
|
39
|
+
margin-bottom: 12px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Discover button */
|
|
43
|
+
.btn {
|
|
44
|
+
display: inline-flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: 8px;
|
|
47
|
+
padding: 10px 20px;
|
|
48
|
+
border: none;
|
|
49
|
+
border-radius: 8px;
|
|
50
|
+
font-size: 14px;
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
transition: all 0.15s ease;
|
|
54
|
+
}
|
|
55
|
+
.btn-primary {
|
|
56
|
+
background: #3b82f6;
|
|
57
|
+
color: #fff;
|
|
58
|
+
}
|
|
59
|
+
.btn-primary:hover {
|
|
60
|
+
background: #2563eb;
|
|
61
|
+
}
|
|
62
|
+
.btn-primary:disabled {
|
|
63
|
+
background: #555;
|
|
64
|
+
cursor: not-allowed;
|
|
65
|
+
}
|
|
66
|
+
.btn-save {
|
|
67
|
+
background: #10b981;
|
|
68
|
+
color: #fff;
|
|
69
|
+
}
|
|
70
|
+
.btn-save:hover {
|
|
71
|
+
background: #059669;
|
|
72
|
+
}
|
|
73
|
+
.btn-danger {
|
|
74
|
+
background: #ef4444;
|
|
75
|
+
color: #fff;
|
|
76
|
+
padding: 6px 12px;
|
|
77
|
+
font-size: 12px;
|
|
78
|
+
}
|
|
79
|
+
.btn-danger:hover {
|
|
80
|
+
background: #dc2626;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* App list */
|
|
84
|
+
.app-list {
|
|
85
|
+
list-style: none;
|
|
86
|
+
}
|
|
87
|
+
.app-item {
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
gap: 12px;
|
|
91
|
+
padding: 10px 14px;
|
|
92
|
+
background: rgba(255, 255, 255, 0.05);
|
|
93
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
94
|
+
border-radius: 10px;
|
|
95
|
+
margin-bottom: 8px;
|
|
96
|
+
cursor: grab;
|
|
97
|
+
transition:
|
|
98
|
+
background 0.15s ease,
|
|
99
|
+
border-color 0.15s ease;
|
|
100
|
+
}
|
|
101
|
+
.app-item:hover {
|
|
102
|
+
background: rgba(255, 255, 255, 0.08);
|
|
103
|
+
border-color: rgba(255, 255, 255, 0.15);
|
|
104
|
+
}
|
|
105
|
+
.app-item.dragging {
|
|
106
|
+
opacity: 0.5;
|
|
107
|
+
background: rgba(59, 130, 246, 0.15);
|
|
108
|
+
}
|
|
109
|
+
.app-item.drag-over {
|
|
110
|
+
border-color: #3b82f6;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.drag-handle {
|
|
114
|
+
color: #555;
|
|
115
|
+
font-size: 18px;
|
|
116
|
+
cursor: grab;
|
|
117
|
+
user-select: none;
|
|
118
|
+
flex-shrink: 0;
|
|
119
|
+
width: 20px;
|
|
120
|
+
text-align: center;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.app-icon-preview {
|
|
124
|
+
width: 40px;
|
|
125
|
+
height: 40px;
|
|
126
|
+
border-radius: 10px;
|
|
127
|
+
display: flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
justify-content: center;
|
|
130
|
+
background: rgba(58, 58, 62, 0.9);
|
|
131
|
+
overflow: hidden;
|
|
132
|
+
flex-shrink: 0;
|
|
133
|
+
font-size: 20px;
|
|
134
|
+
}
|
|
135
|
+
.app-icon-preview img {
|
|
136
|
+
width: 100%;
|
|
137
|
+
height: 100%;
|
|
138
|
+
object-fit: contain;
|
|
139
|
+
padding: 6px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.app-info {
|
|
143
|
+
flex: 1;
|
|
144
|
+
min-width: 0;
|
|
145
|
+
}
|
|
146
|
+
.app-name {
|
|
147
|
+
font-size: 14px;
|
|
148
|
+
font-weight: 600;
|
|
149
|
+
}
|
|
150
|
+
.app-url {
|
|
151
|
+
font-size: 11px;
|
|
152
|
+
color: #777;
|
|
153
|
+
overflow: hidden;
|
|
154
|
+
text-overflow: ellipsis;
|
|
155
|
+
white-space: nowrap;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.app-toggle {
|
|
159
|
+
flex-shrink: 0;
|
|
160
|
+
}
|
|
161
|
+
.app-toggle input[type='checkbox'] {
|
|
162
|
+
width: 18px;
|
|
163
|
+
height: 18px;
|
|
164
|
+
cursor: pointer;
|
|
165
|
+
accent-color: #3b82f6;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.app-actions {
|
|
169
|
+
flex-shrink: 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.empty-state {
|
|
173
|
+
text-align: center;
|
|
174
|
+
padding: 40px 20px;
|
|
175
|
+
color: #666;
|
|
176
|
+
font-size: 14px;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.status {
|
|
180
|
+
margin-top: 12px;
|
|
181
|
+
font-size: 13px;
|
|
182
|
+
color: #10b981;
|
|
183
|
+
min-height: 20px;
|
|
184
|
+
}
|
|
185
|
+
.status.error {
|
|
186
|
+
color: #ef4444;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.actions-bar {
|
|
190
|
+
display: flex;
|
|
191
|
+
gap: 12px;
|
|
192
|
+
align-items: center;
|
|
193
|
+
margin-top: 20px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* Dock preview */
|
|
197
|
+
.dock-preview {
|
|
198
|
+
display: flex;
|
|
199
|
+
align-items: flex-end;
|
|
200
|
+
justify-content: center;
|
|
201
|
+
gap: 8px;
|
|
202
|
+
padding: 14px 20px;
|
|
203
|
+
background: rgba(30, 30, 32, 0.55);
|
|
204
|
+
backdrop-filter: blur(60px);
|
|
205
|
+
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
206
|
+
border-radius: 22px;
|
|
207
|
+
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
|
|
208
|
+
margin: 20px auto;
|
|
209
|
+
width: fit-content;
|
|
210
|
+
}
|
|
211
|
+
.dock-preview-icon {
|
|
212
|
+
width: 48px;
|
|
213
|
+
height: 48px;
|
|
214
|
+
border-radius: 12px;
|
|
215
|
+
display: flex;
|
|
216
|
+
align-items: center;
|
|
217
|
+
justify-content: center;
|
|
218
|
+
background: rgba(58, 58, 62, 0.9);
|
|
219
|
+
overflow: hidden;
|
|
220
|
+
font-size: 22px;
|
|
221
|
+
transition: transform 0.15s ease;
|
|
222
|
+
}
|
|
223
|
+
.dock-preview-icon:hover {
|
|
224
|
+
transform: scale(1.3);
|
|
225
|
+
}
|
|
226
|
+
.dock-preview-icon img {
|
|
227
|
+
width: 100%;
|
|
228
|
+
height: 100%;
|
|
229
|
+
object-fit: contain;
|
|
230
|
+
padding: 6px;
|
|
231
|
+
}
|
|
232
|
+
</style>
|
|
233
|
+
</head>
|
|
234
|
+
<body>
|
|
235
|
+
<h1>App Dock Configuration</h1>
|
|
236
|
+
<p class="subtitle">Discover installed webapps, arrange their order, and customize the dock.</p>
|
|
237
|
+
|
|
238
|
+
<div class="section">
|
|
239
|
+
<div class="section-title">Webapps</div>
|
|
240
|
+
<button class="btn btn-primary" id="discover-btn" onclick="discover()">Discover Installed Webapps</button>
|
|
241
|
+
<div class="status" id="status"></div>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<div class="section">
|
|
245
|
+
<div class="section-title">Dock Apps (drag to reorder)</div>
|
|
246
|
+
<ul class="app-list" id="app-list"></ul>
|
|
247
|
+
<div class="empty-state" id="empty-state" style="display: none">No apps yet. Click Discover above.</div>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<div class="section">
|
|
251
|
+
<div class="section-title">Preview</div>
|
|
252
|
+
<div class="dock-preview" id="dock-preview"></div>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<div class="actions-bar">
|
|
256
|
+
<button class="btn btn-save" onclick="save()">Save Configuration</button>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<script>
|
|
260
|
+
let apps = []
|
|
261
|
+
|
|
262
|
+
async function loadCurrent() {
|
|
263
|
+
try {
|
|
264
|
+
const res = await fetch('/plugins/signalk-app-dock/config')
|
|
265
|
+
if (res.ok) {
|
|
266
|
+
const data = await res.json()
|
|
267
|
+
apps = (data.configuration && data.configuration.apps) || []
|
|
268
|
+
render()
|
|
269
|
+
}
|
|
270
|
+
} catch (e) {
|
|
271
|
+
console.warn('Could not load current config', e)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function discover() {
|
|
276
|
+
const btn = document.getElementById('discover-btn')
|
|
277
|
+
const status = document.getElementById('status')
|
|
278
|
+
btn.disabled = true
|
|
279
|
+
btn.textContent = 'Discovering...'
|
|
280
|
+
status.textContent = ''
|
|
281
|
+
status.className = 'status'
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const res = await fetch('/skServer/webapps')
|
|
285
|
+
const allWebapps = await res.json()
|
|
286
|
+
const webapps = allWebapps
|
|
287
|
+
.filter((w) => w.name !== 'signalk-app-dock' && w.name !== '@signalk/server-admin-ui')
|
|
288
|
+
.map((w) => ({
|
|
289
|
+
name: w.name,
|
|
290
|
+
label: w.signalk?.displayName || w.name,
|
|
291
|
+
url: `/${w.name}/`,
|
|
292
|
+
icon: w.signalk?.appIcon ? `/${w.name}/${w.signalk.appIcon}` : null
|
|
293
|
+
}))
|
|
294
|
+
|
|
295
|
+
if (webapps.length === 0) {
|
|
296
|
+
status.textContent = 'No webapps found. Is the server fully started?'
|
|
297
|
+
status.className = 'status error'
|
|
298
|
+
btn.disabled = false
|
|
299
|
+
btn.textContent = 'Discover Installed Webapps'
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const existingUrls = new Set(apps.map((a) => a.url))
|
|
304
|
+
let added = 0
|
|
305
|
+
for (const w of webapps) {
|
|
306
|
+
if (!existingUrls.has(w.url)) {
|
|
307
|
+
apps.push({
|
|
308
|
+
enabled: true,
|
|
309
|
+
url: w.url,
|
|
310
|
+
label: w.label || '',
|
|
311
|
+
icon: w.icon || '',
|
|
312
|
+
color: ''
|
|
313
|
+
})
|
|
314
|
+
added++
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
status.textContent =
|
|
319
|
+
added > 0
|
|
320
|
+
? `Found ${webapps.length} webapps, added ${added} new.`
|
|
321
|
+
: `All ${webapps.length} webapps already in list.`
|
|
322
|
+
render()
|
|
323
|
+
} catch (e) {
|
|
324
|
+
status.textContent = 'Discovery failed: ' + e.message
|
|
325
|
+
status.className = 'status error'
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
btn.disabled = false
|
|
329
|
+
btn.textContent = 'Discover Installed Webapps'
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function save() {
|
|
333
|
+
const status = document.getElementById('status')
|
|
334
|
+
try {
|
|
335
|
+
const cfgRes = await fetch('/plugins/signalk-app-dock/config')
|
|
336
|
+
const current = await cfgRes.json()
|
|
337
|
+
const newConfig = {
|
|
338
|
+
...current,
|
|
339
|
+
configuration: {
|
|
340
|
+
...(current.configuration || {}),
|
|
341
|
+
apps: apps
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const res = await fetch('/plugins/signalk-app-dock/config', {
|
|
346
|
+
method: 'POST',
|
|
347
|
+
headers: { 'Content-Type': 'application/json' },
|
|
348
|
+
body: JSON.stringify(newConfig)
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
if (res.ok) {
|
|
352
|
+
status.textContent = 'Saved! Plugin will restart with new config.'
|
|
353
|
+
status.className = 'status'
|
|
354
|
+
} else {
|
|
355
|
+
status.textContent = 'Save failed: ' + res.statusText
|
|
356
|
+
status.className = 'status error'
|
|
357
|
+
}
|
|
358
|
+
} catch (e) {
|
|
359
|
+
status.textContent = 'Save failed: ' + e.message
|
|
360
|
+
status.className = 'status error'
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function removeApp(index) {
|
|
365
|
+
apps.splice(index, 1)
|
|
366
|
+
render()
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function toggleApp(index) {
|
|
370
|
+
apps[index].enabled = !apps[index].enabled
|
|
371
|
+
render()
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function renderIcon(app) {
|
|
375
|
+
const iconVal = app.icon || ''
|
|
376
|
+
if (iconVal.startsWith('/') || iconVal.startsWith('http')) {
|
|
377
|
+
return `<img src="${iconVal}" alt="${app.label}" onerror="this.parentNode.textContent='${(app.label || app.url || '??').slice(0, 2).toUpperCase()}'">`
|
|
378
|
+
}
|
|
379
|
+
return iconVal || (app.label || app.url || '??').slice(0, 2).toUpperCase()
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function render() {
|
|
383
|
+
const list = document.getElementById('app-list')
|
|
384
|
+
const empty = document.getElementById('empty-state')
|
|
385
|
+
const preview = document.getElementById('dock-preview')
|
|
386
|
+
|
|
387
|
+
if (apps.length === 0) {
|
|
388
|
+
list.innerHTML = ''
|
|
389
|
+
empty.style.display = ''
|
|
390
|
+
preview.innerHTML = '<span style="color:#555;font-size:13px">No apps</span>'
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
empty.style.display = 'none'
|
|
395
|
+
|
|
396
|
+
list.innerHTML = apps
|
|
397
|
+
.map(
|
|
398
|
+
(app, i) => `
|
|
399
|
+
<li class="app-item" draggable="true" data-index="${i}" style="opacity:${app.enabled === false ? 0.4 : 1}">
|
|
400
|
+
<span class="drag-handle">\u2261</span>
|
|
401
|
+
<div class="app-icon-preview" ${app.color ? `style="background:${app.color}"` : ''}>
|
|
402
|
+
${renderIcon(app)}
|
|
403
|
+
</div>
|
|
404
|
+
<div class="app-info">
|
|
405
|
+
<div class="app-name">${app.label || app.url}</div>
|
|
406
|
+
<div class="app-url">${app.url}</div>
|
|
407
|
+
</div>
|
|
408
|
+
<div class="app-toggle">
|
|
409
|
+
<input type="checkbox" ${app.enabled !== false ? 'checked' : ''} onchange="toggleApp(${i})" title="Enable/disable">
|
|
410
|
+
</div>
|
|
411
|
+
<div class="app-actions">
|
|
412
|
+
<button class="btn btn-danger" onclick="removeApp(${i})">Remove</button>
|
|
413
|
+
</div>
|
|
414
|
+
</li>
|
|
415
|
+
`
|
|
416
|
+
)
|
|
417
|
+
.join('')
|
|
418
|
+
|
|
419
|
+
initDragDrop()
|
|
420
|
+
|
|
421
|
+
preview.innerHTML = apps
|
|
422
|
+
.filter((a) => a.enabled !== false)
|
|
423
|
+
.map(
|
|
424
|
+
(app) => `
|
|
425
|
+
<div class="dock-preview-icon" ${app.color ? `style="background:${app.color}"` : ''}>
|
|
426
|
+
${renderIcon(app)}
|
|
427
|
+
</div>
|
|
428
|
+
`
|
|
429
|
+
)
|
|
430
|
+
.join('')
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
let dragIndex = null
|
|
434
|
+
function initDragDrop() {
|
|
435
|
+
const items = document.querySelectorAll('.app-item')
|
|
436
|
+
items.forEach((item) => {
|
|
437
|
+
item.addEventListener('dragstart', (e) => {
|
|
438
|
+
dragIndex = parseInt(item.dataset.index)
|
|
439
|
+
item.classList.add('dragging')
|
|
440
|
+
e.dataTransfer.effectAllowed = 'move'
|
|
441
|
+
})
|
|
442
|
+
item.addEventListener('dragend', () => {
|
|
443
|
+
item.classList.remove('dragging')
|
|
444
|
+
document.querySelectorAll('.app-item').forEach((el) => el.classList.remove('drag-over'))
|
|
445
|
+
})
|
|
446
|
+
item.addEventListener('dragover', (e) => {
|
|
447
|
+
e.preventDefault()
|
|
448
|
+
e.dataTransfer.dropEffect = 'move'
|
|
449
|
+
item.classList.add('drag-over')
|
|
450
|
+
})
|
|
451
|
+
item.addEventListener('dragleave', () => {
|
|
452
|
+
item.classList.remove('drag-over')
|
|
453
|
+
})
|
|
454
|
+
item.addEventListener('drop', (e) => {
|
|
455
|
+
e.preventDefault()
|
|
456
|
+
const dropIndex = parseInt(item.dataset.index)
|
|
457
|
+
if (dragIndex !== null && dragIndex !== dropIndex) {
|
|
458
|
+
const moved = apps.splice(dragIndex, 1)[0]
|
|
459
|
+
apps.splice(dropIndex, 0, moved)
|
|
460
|
+
render()
|
|
461
|
+
}
|
|
462
|
+
})
|
|
463
|
+
})
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
loadCurrent()
|
|
467
|
+
</script>
|
|
468
|
+
</body>
|
|
469
|
+
</html>
|