@unbrained/pm-web 2026.9.13 → 2026.9.23
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/CHANGELOG.md +18 -1
- package/README.md +6 -4
- package/dist/app.d.ts +10 -0
- package/dist/app.js +19 -9
- package/dist/app.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/routes/admin.js +6 -6
- package/dist/routes/admin.js.map +1 -1
- package/dist/routes/auth.js +2 -2
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/extensions.js +24 -37
- package/dist/routes/extensions.js.map +1 -1
- package/dist/routes/github.js +169 -107
- package/dist/routes/github.js.map +1 -1
- package/dist/routes/groups.js +9 -14
- package/dist/routes/groups.js.map +1 -1
- package/dist/routes/pm.js +532 -1015
- package/dist/routes/pm.js.map +1 -1
- package/dist/routes/projects.js +10 -6
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/route-helpers.d.ts +74 -0
- package/dist/routes/route-helpers.js +85 -0
- package/dist/routes/route-helpers.js.map +1 -0
- package/dist/routes/sharing.js +30 -18
- package/dist/routes/sharing.js.map +1 -1
- package/dist/server.js +18 -23
- package/dist/server.js.map +1 -1
- package/dist/services/mutation-event-watcher.js +3 -28
- package/dist/services/mutation-event-watcher.js.map +1 -1
- package/dist/services/pm-runner.js +3 -1
- package/dist/services/pm-runner.js.map +1 -1
- package/dist/services/project-watcher.js +14 -38
- package/dist/services/project-watcher.js.map +1 -1
- package/dist/services/sse.d.ts +7 -0
- package/dist/services/sse.js +22 -0
- package/dist/services/sse.js.map +1 -1
- package/dist/services/watcher-utils.d.ts +42 -0
- package/dist/services/watcher-utils.js +60 -0
- package/dist/services/watcher-utils.js.map +1 -0
- package/manifest.json +2 -2
- package/package.json +20 -9
- package/public/src/app.js +22 -52
- package/public/src/app.js.map +1 -1
- package/public/src/app.ts +18 -75
- package/public/src/browser-window.js +29 -0
- package/public/src/browser-window.js.map +1 -0
- package/public/src/browser-window.ts +56 -0
- package/public/src/components/modals.js +1 -1
- package/public/src/components/modals.js.map +1 -1
- package/public/src/components/modals.ts +1 -1
- package/public/src/i18n.js +14 -6
- package/public/src/i18n.js.map +1 -1
- package/public/src/i18n.ts +16 -6
- package/public/src/sw.ts +1 -0
- package/public/src/utils.js +58 -0
- package/public/src/utils.js.map +1 -1
- package/public/src/utils.ts +53 -0
- package/public/src/views/admin.js +2 -5
- package/public/src/views/admin.js.map +1 -1
- package/public/src/views/admin.ts +2 -2
- package/public/src/views/auth.js +5 -1
- package/public/src/views/auth.js.map +1 -1
- package/public/src/views/auth.ts +6 -1
- package/public/src/views/calendar.js +1 -2
- package/public/src/views/calendar.js.map +1 -1
- package/public/src/views/calendar.ts +1 -2
- package/public/src/views/config.js +14 -21
- package/public/src/views/config.js.map +1 -1
- package/public/src/views/config.ts +14 -19
- package/public/src/views/context.js +11 -12
- package/public/src/views/context.js.map +1 -1
- package/public/src/views/context.ts +13 -12
- package/public/src/views/create.js +3 -11
- package/public/src/views/create.js.map +1 -1
- package/public/src/views/create.ts +3 -3
- package/public/src/views/export.js +2 -3
- package/public/src/views/export.js.map +1 -1
- package/public/src/views/export.ts +2 -3
- package/public/src/views/github.js +33 -47
- package/public/src/views/github.js.map +1 -1
- package/public/src/views/github.ts +35 -28
- package/public/src/views/graph-canvas.js +92 -99
- package/public/src/views/graph-canvas.js.map +1 -1
- package/public/src/views/graph-canvas.ts +94 -93
- package/public/src/views/graph.js +88 -73
- package/public/src/views/graph.js.map +1 -1
- package/public/src/views/graph.ts +97 -63
- package/public/src/views/groups.js +2 -5
- package/public/src/views/groups.js.map +1 -1
- package/public/src/views/groups.ts +2 -2
- package/public/src/views/guide.js +11 -10
- package/public/src/views/guide.js.map +1 -1
- package/public/src/views/guide.ts +12 -10
- package/public/src/views/items.js +100 -130
- package/public/src/views/items.js.map +1 -1
- package/public/src/views/items.ts +99 -117
- package/public/src/views/normalize.js +2 -9
- package/public/src/views/normalize.js.map +1 -1
- package/public/src/views/normalize.ts +2 -9
- package/public/src/views/packages.js +7 -4
- package/public/src/views/packages.js.map +1 -1
- package/public/src/views/packages.ts +8 -4
- package/public/src/views/plan-execution.js.map +1 -1
- package/public/src/views/plan-execution.ts +5 -15
- package/public/src/views/plan.js +2 -1
- package/public/src/views/plan.js.map +1 -1
- package/public/src/views/plan.ts +6 -17
- package/public/src/views/projects.js +5 -6
- package/public/src/views/projects.js.map +1 -1
- package/public/src/views/projects.ts +5 -6
- package/public/src/views/search.js +5 -1
- package/public/src/views/search.js.map +1 -1
- package/public/src/views/search.ts +7 -1
- package/public/src/views/settings.js +30 -55
- package/public/src/views/settings.js.map +1 -1
- package/public/src/views/settings.ts +23 -17
- package/public/src/views/shared.js +2 -5
- package/public/src/views/shared.js.map +1 -1
- package/public/src/views/shared.ts +2 -5
- package/public/src/views/sharing.js +2 -5
- package/public/src/views/sharing.js.map +1 -1
- package/public/src/views/sharing.ts +2 -2
- package/public/src/views/templates.js +9 -16
- package/public/src/views/templates.js.map +1 -1
- package/public/src/views/templates.ts +9 -14
- package/public/src/views/validate.js +2 -9
- package/public/src/views/validate.js.map +1 -1
- package/public/src/views/validate.ts +2 -9
- package/public/sw.js +1 -0
|
@@ -3,11 +3,32 @@
|
|
|
3
3
|
// ═══════════════════════════════════════════════════════════════
|
|
4
4
|
import { state } from '../state.js';
|
|
5
5
|
import { api } from '../api.js';
|
|
6
|
-
import { escHtml } from '../utils.js';
|
|
6
|
+
import { escHtml, resetButton, showError } from '../utils.js';
|
|
7
7
|
import { confirmDialog } from '../components/modals.js';
|
|
8
8
|
import { toast } from '../components/toast.js';
|
|
9
|
+
import { loadItemsBadge } from './projects.js';
|
|
9
10
|
import type { GitHubImportResponse, GitHubIssuesResponse, GitHubIssueRow, GitHubPushResponse, GitHubRepoResponse, ListResponse } from '../api-types.js';
|
|
10
11
|
|
|
12
|
+
/** Renders a styled result box with a title header and inner HTML body, used
|
|
13
|
+
* by the push and import result displays. */
|
|
14
|
+
function githubResultBox(title: string, bodyHtml: string): string {
|
|
15
|
+
return `<div style="background:var(--bg-card2);border:1px solid var(--border);border-radius:var(--radius);padding:12px">
|
|
16
|
+
<div style="font-size:12px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px">${title}</div>
|
|
17
|
+
${bodyHtml}
|
|
18
|
+
</div>`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Toast the result of a GitHub push or import operation with a count and any
|
|
22
|
+
* errors, matching the pluralisation and error-count format both share. */
|
|
23
|
+
function githubOpToast(verb: string, noun: string, count: number, errors: string[]): void {
|
|
24
|
+
toast(`${verb} ${count} ${noun}${count!==1?'s':''}${errors.length?' ('+errors.length+' error'+(errors.length!==1?'s':'')+')':''}`, errors.length ? 'info' : 'success');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Show result HTML in a result element, making it visible. No-op when null. */
|
|
28
|
+
function showResultEl(el: HTMLElement | null, html: string): void {
|
|
29
|
+
if (el) { el.style.display = ''; el.innerHTML = html; }
|
|
30
|
+
}
|
|
31
|
+
|
|
11
32
|
/**
|
|
12
33
|
* Renders the GitHub integration page for the current project: a header, a
|
|
13
34
|
* token-required prompt when the user has no GitHub token, and otherwise the
|
|
@@ -150,8 +171,8 @@ export async function linkGitHubRepo(): Promise<void> {
|
|
|
150
171
|
toast('Repository linked','success');
|
|
151
172
|
renderGitHubView();
|
|
152
173
|
} catch(err: unknown) {
|
|
153
|
-
|
|
154
|
-
|
|
174
|
+
showError(errEl, err);
|
|
175
|
+
resetButton(btn, 'Link Repository');
|
|
155
176
|
}
|
|
156
177
|
}
|
|
157
178
|
|
|
@@ -296,21 +317,15 @@ export async function pushItemsToGitHub(): Promise<void> {
|
|
|
296
317
|
const data = await api<GitHubPushResponse>('POST', `/projects/${state.currentProject!.id}/github/push`, { itemIds });
|
|
297
318
|
const pushed = data.pushed || [];
|
|
298
319
|
const errors = data.errors || [];
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
<div style="background:var(--bg-card2);border:1px solid var(--border);border-radius:var(--radius);padding:12px">
|
|
304
|
-
<div style="font-size:12px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px">Push Results</div>
|
|
305
|
-
${pushed.length > 0 ? `<div style="color:var(--status-closed);font-size:13px;margin-bottom:6px">✓ Pushed ${pushed.length}: ${pushed.map((p)=>`<a href="${escHtml(p.issueUrl)}" target="_blank" style="color:var(--accent)">#${p.issueNumber}</a>`).join(', ')}</div>` : ''}
|
|
306
|
-
${errors.length > 0 ? `<div style="color:var(--status-blocked);font-size:13px">✗ ${errors.length} failed: ${errors.map((e)=>escHtml(String(e))).join('; ')}</div>` : ''}
|
|
307
|
-
</div>`;
|
|
308
|
-
}
|
|
320
|
+
githubOpToast('Pushed', 'item', pushed.length, errors);
|
|
321
|
+
showResultEl(resultEl, githubResultBox('Push Results',
|
|
322
|
+
`${pushed.length > 0 ? `<div style="color:var(--status-closed);font-size:13px;margin-bottom:6px">✓ Pushed ${pushed.length}: ${pushed.map((p)=>`<a href="${escHtml(p.issueUrl)}" target="_blank" style="color:var(--accent)">#${p.issueNumber}</a>`).join(', ')}</div>` : ''}
|
|
323
|
+
${errors.length > 0 ? `<div style="color:var(--status-blocked);font-size:13px">✗ ${errors.length} failed: ${errors.map((e)=>escHtml(String(e))).join('; ')}</div>` : ''}`));
|
|
309
324
|
loadItemsForPush();
|
|
310
325
|
} catch(err: unknown) {
|
|
311
326
|
toast(`Push failed: ${err instanceof Error ? err.message : String(err)}`, 'error');
|
|
312
327
|
} finally {
|
|
313
|
-
|
|
328
|
+
resetButton(btn, 'Push to GitHub');
|
|
314
329
|
}
|
|
315
330
|
}
|
|
316
331
|
|
|
@@ -347,22 +362,14 @@ export async function importGitHubIssues(): Promise<void> {
|
|
|
347
362
|
const data = await api<GitHubImportResponse>('POST',`/projects/${state.currentProject!.id}/github/import`,{issueNumbers});
|
|
348
363
|
const created = data.created || [];
|
|
349
364
|
const errors = data.errors || [];
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
<div style="font-size:12px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px">Import Results</div>
|
|
356
|
-
${created.length > 0 ? `<div style="color:var(--status-closed);font-size:13px;margin-bottom:6px">✓ Created ${created.length} item${created.length!==1?'s':''}: ${created.map((id)=>`<span class="mono" style="font-size:11px">${escHtml(String(id))}</span>`).join(', ')}</div>` : ''}
|
|
357
|
-
${errors.length > 0 ? `<div style="color:var(--status-blocked);font-size:13px">✗ ${errors.length} error${errors.length!==1?'s':''}: ${errors.map((e)=>escHtml(String(e))).join('; ')}</div>` : ''}
|
|
358
|
-
</div>`;
|
|
359
|
-
}
|
|
360
|
-
// loadItemsBadge is in projects.ts, import via app bridge
|
|
361
|
-
const loadBadge = window.__app?.loadItemsBadge;
|
|
362
|
-
if (loadBadge) loadBadge();
|
|
365
|
+
githubOpToast('Imported', 'issue', created.length, errors);
|
|
366
|
+
showResultEl(resultEl, githubResultBox('Import Results',
|
|
367
|
+
`${created.length > 0 ? `<div style="color:var(--status-closed);font-size:13px;margin-bottom:6px">✓ Created ${created.length} item${created.length!==1?'s':''}: ${created.map((id)=>`<span class="mono" style="font-size:11px">${escHtml(String(id))}</span>`).join(', ')}</div>` : ''}
|
|
368
|
+
${errors.length > 0 ? `<div style="color:var(--status-blocked);font-size:13px">✗ ${errors.length} error${errors.length!==1?'s':''}: ${errors.map((e)=>escHtml(String(e))).join('; ')}</div>` : ''}`));
|
|
369
|
+
void loadItemsBadge();
|
|
363
370
|
} catch(err: unknown) {
|
|
364
371
|
toast(`Import failed: ${err instanceof Error ? err.message : String(err)}`,'error');
|
|
365
372
|
} finally {
|
|
366
|
-
|
|
373
|
+
resetButton(btn, 'Import Selected');
|
|
367
374
|
}
|
|
368
375
|
}
|
|
@@ -137,6 +137,11 @@ function escHtml(s) {
|
|
|
137
137
|
.replace(/>/g, '>')
|
|
138
138
|
.replace(/"/g, '"');
|
|
139
139
|
}
|
|
140
|
+
/** Builds a tag-to-color map from a tag frequency map, assigning palette colors to the most frequent tags in descending order. */
|
|
141
|
+
export function tagColorMapFromFreq(freq) {
|
|
142
|
+
const top = [...freq.entries()].sort((a, b) => b[1] - a[1]).slice(0, TAG_PALETTE.length).map(([t]) => t);
|
|
143
|
+
return new Map(top.map((t, i) => [t, TAG_PALETTE[i]]));
|
|
144
|
+
}
|
|
140
145
|
// ═══════════════════════════════════════════════════════════════
|
|
141
146
|
// GraphCanvas class
|
|
142
147
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -548,6 +553,21 @@ export class GraphCanvas {
|
|
|
548
553
|
return false;
|
|
549
554
|
return a.source.id === b.source.id && a.target.id === b.target.id && a.type === b.type;
|
|
550
555
|
}
|
|
556
|
+
/** Returns the bidirectional edge key, flag, and curve direction for the given source/target node pair. */
|
|
557
|
+
edgeBidirInfo(s, t) {
|
|
558
|
+
const key = [s.id, t.id].sort().join('|');
|
|
559
|
+
const isBiDir = this.biDirPairs.has(key);
|
|
560
|
+
const curveDir = isBiDir ? (s.id < t.id ? 1 : -1) : 0;
|
|
561
|
+
return { key, isBiDir, curveDir };
|
|
562
|
+
}
|
|
563
|
+
/** Computes the start and end points of an edge segment, offset from the node radii along the unit direction vector. */
|
|
564
|
+
edgeEndpoints(s, t, nx, ny) {
|
|
565
|
+
const x1 = s.x + nx * s.r;
|
|
566
|
+
const y1 = s.y + ny * s.r;
|
|
567
|
+
const x2 = t.x - nx * (t.r + 7);
|
|
568
|
+
const y2 = t.y - ny * (t.r + 7);
|
|
569
|
+
return { x1, y1, x2, y2 };
|
|
570
|
+
}
|
|
551
571
|
buildEdgeGeometry(edge) {
|
|
552
572
|
const { source: s, target: t } = edge;
|
|
553
573
|
const dx = t.x - s.x || 0.01;
|
|
@@ -557,16 +577,11 @@ export class GraphCanvas {
|
|
|
557
577
|
const ny = dy / len;
|
|
558
578
|
const px = -dy / len;
|
|
559
579
|
const py = dx / len;
|
|
560
|
-
const key =
|
|
561
|
-
const isBiDir = this.biDirPairs.has(key);
|
|
562
|
-
const curveDir = isBiDir ? (s.id < t.id ? 1 : -1) : 0;
|
|
580
|
+
const { key, isBiDir, curveDir } = this.edgeBidirInfo(s, t);
|
|
563
581
|
const cpOffset = isBiDir ? len * 0.22 * curveDir : 0;
|
|
564
582
|
const cpX = (s.x + t.x) / 2 + px * cpOffset;
|
|
565
583
|
const cpY = (s.y + t.y) / 2 + py * cpOffset;
|
|
566
|
-
const x1 = s
|
|
567
|
-
const y1 = s.y + ny * s.r;
|
|
568
|
-
const x2 = t.x - nx * (t.r + 7);
|
|
569
|
-
const y2 = t.y - ny * (t.r + 7);
|
|
584
|
+
const { x1, y1, x2, y2 } = this.edgeEndpoints(s, t, nx, ny);
|
|
570
585
|
const qX = (u) => (1 - u) * (1 - u) * x1 + 2 * (1 - u) * u * cpX + u * u * x2;
|
|
571
586
|
const qY = (u) => (1 - u) * (1 - u) * y1 + 2 * (1 - u) * u * cpY + u * u * y2;
|
|
572
587
|
const midU = 0.5;
|
|
@@ -683,8 +698,7 @@ export class GraphCanvas {
|
|
|
683
698
|
for (const t of n.tags ?? [])
|
|
684
699
|
freq.set(t, (freq.get(t) ?? 0) + 1);
|
|
685
700
|
}
|
|
686
|
-
|
|
687
|
-
this.tagColorMap = new Map(top.map((t, i) => [t, TAG_PALETTE[i]]));
|
|
701
|
+
this.tagColorMap = tagColorMapFromFreq(freq);
|
|
688
702
|
}
|
|
689
703
|
else {
|
|
690
704
|
this.tagColorMap = new Map();
|
|
@@ -1272,9 +1286,7 @@ export class GraphCanvas {
|
|
|
1272
1286
|
const px = -dy / len;
|
|
1273
1287
|
const py = dx / len;
|
|
1274
1288
|
// Determine curvature direction for bidirectional edges
|
|
1275
|
-
const
|
|
1276
|
-
const isBiDir = this.biDirPairs.has(key);
|
|
1277
|
-
const curveDir = isBiDir ? (s.id < t.id ? 1 : -1) : 0;
|
|
1289
|
+
const { isBiDir, curveDir } = this.edgeBidirInfo(s, t);
|
|
1278
1290
|
const curvature = isBiDir ? 0.22 : 0.0;
|
|
1279
1291
|
const cpFactor = len * curvature * curveDir;
|
|
1280
1292
|
// Control point (on the perpendicular bisector)
|
|
@@ -1283,10 +1295,7 @@ export class GraphCanvas {
|
|
|
1283
1295
|
// Start/end points offset from node radii
|
|
1284
1296
|
const nx = dx / len;
|
|
1285
1297
|
const ny = dy / len;
|
|
1286
|
-
const x1 = s
|
|
1287
|
-
const y1 = s.y + ny * s.r;
|
|
1288
|
-
const x2 = t.x - nx * (t.r + 7);
|
|
1289
|
-
const y2 = t.y - ny * (t.r + 7);
|
|
1298
|
+
const { x1, y1, x2, y2 } = this.edgeEndpoints(s, t, nx, ny);
|
|
1290
1299
|
const color = onCritPath ? '#fbbf24' : getEdgeColor(edge.type);
|
|
1291
1300
|
const isActive = highlighted || onCritPath;
|
|
1292
1301
|
ctx.save();
|
|
@@ -1777,11 +1786,8 @@ export class GraphCanvas {
|
|
|
1777
1786
|
break;
|
|
1778
1787
|
}
|
|
1779
1788
|
}
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
return;
|
|
1783
|
-
this.canvas.focus();
|
|
1784
|
-
const { x, y } = this.getPos(e);
|
|
1789
|
+
/** Initiates a pointer drag at the given canvas coordinates: records the start position, performs a hit test, and begins either a node drag or canvas pan. */
|
|
1790
|
+
beginPointerDrag(x, y, setCursor) {
|
|
1785
1791
|
this.lastX = x;
|
|
1786
1792
|
this.lastY = y;
|
|
1787
1793
|
this.downX = x;
|
|
@@ -1795,33 +1801,75 @@ export class GraphCanvas {
|
|
|
1795
1801
|
hit.fx = hit.x;
|
|
1796
1802
|
hit.fy = hit.y;
|
|
1797
1803
|
this.alpha = Math.max(this.alpha, 0.25);
|
|
1798
|
-
|
|
1804
|
+
if (setCursor)
|
|
1805
|
+
this.canvas.style.cursor = 'grabbing';
|
|
1799
1806
|
}
|
|
1800
1807
|
else {
|
|
1801
1808
|
this.isDraggingCanvas = true;
|
|
1802
|
-
|
|
1809
|
+
if (setCursor)
|
|
1810
|
+
this.canvas.style.cursor = 'grabbing';
|
|
1803
1811
|
}
|
|
1804
1812
|
}
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
const
|
|
1810
|
-
|
|
1811
|
-
|
|
1813
|
+
/** Returns the canvas-relative coordinates of the first touch, or null when there is not exactly one active touch. */
|
|
1814
|
+
firstTouchPos(e) {
|
|
1815
|
+
if (e.touches.length !== 1)
|
|
1816
|
+
return null;
|
|
1817
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
1818
|
+
return { x: e.touches[0].clientX - rect.left, y: e.touches[0].clientY - rect.top };
|
|
1819
|
+
}
|
|
1820
|
+
/** Handles a pointer move during an active drag: updates hasMoved, drags the grabbed node or pans the canvas, and returns true when a drag consumed the event. */
|
|
1821
|
+
handleDragMove(x, y, dx, dy, moveThreshold, clearFlyOnNode) {
|
|
1822
|
+
if (Math.abs(x - this.downX) > moveThreshold || Math.abs(y - this.downY) > moveThreshold)
|
|
1812
1823
|
this.hasMoved = true;
|
|
1813
1824
|
if (this.isDraggingNode && this.dragNode) {
|
|
1814
1825
|
const [wx, wy] = this.toWorld(x, y);
|
|
1815
1826
|
this.dragNode.fx = wx;
|
|
1816
1827
|
this.dragNode.fy = wy;
|
|
1817
|
-
|
|
1828
|
+
if (clearFlyOnNode)
|
|
1829
|
+
this.flyTarget = null;
|
|
1830
|
+
return true;
|
|
1818
1831
|
}
|
|
1819
|
-
|
|
1832
|
+
if (this.isDraggingCanvas) {
|
|
1820
1833
|
this.tx += dx;
|
|
1821
1834
|
this.ty += dy;
|
|
1822
1835
|
this.flyTarget = null;
|
|
1836
|
+
return true;
|
|
1823
1837
|
}
|
|
1824
|
-
|
|
1838
|
+
return false;
|
|
1839
|
+
}
|
|
1840
|
+
/** Ends a node drag gesture: if the pointer did not move, toggles selection and flies to the node, then clears the drag state. Returns true when a node drag was in progress. */
|
|
1841
|
+
endNodeDrag() {
|
|
1842
|
+
if (!this.isDraggingNode || !this.dragNode)
|
|
1843
|
+
return false;
|
|
1844
|
+
if (!this.hasMoved) {
|
|
1845
|
+
const id = this.dragNode.id;
|
|
1846
|
+
const newSel = this.filter.selectedId === id ? null : id;
|
|
1847
|
+
this.filter = { ...this.filter, selectedId: newSel };
|
|
1848
|
+
this.particles = [];
|
|
1849
|
+
this.lastParticleSpawn = 0;
|
|
1850
|
+
this.onSelectNode(newSel);
|
|
1851
|
+
if (newSel)
|
|
1852
|
+
this.flyTo(this.dragNode);
|
|
1853
|
+
}
|
|
1854
|
+
this.dragNode.fx = null;
|
|
1855
|
+
this.dragNode.fy = null;
|
|
1856
|
+
this.dragNode = null;
|
|
1857
|
+
return true;
|
|
1858
|
+
}
|
|
1859
|
+
onMouseDown(e) {
|
|
1860
|
+
if (e.button !== 0)
|
|
1861
|
+
return;
|
|
1862
|
+
this.canvas.focus();
|
|
1863
|
+
const { x, y } = this.getPos(e);
|
|
1864
|
+
this.beginPointerDrag(x, y, true);
|
|
1865
|
+
}
|
|
1866
|
+
onMouseMove(e) {
|
|
1867
|
+
const r = this.canvas.getBoundingClientRect();
|
|
1868
|
+
const x = e.clientX - r.left;
|
|
1869
|
+
const y = e.clientY - r.top;
|
|
1870
|
+
const dx = x - this.lastX;
|
|
1871
|
+
const dy = y - this.lastY;
|
|
1872
|
+
if (!this.handleDragMove(x, y, dx, dy, 3, true)) {
|
|
1825
1873
|
const [wx, wy] = this.toWorld(x, y);
|
|
1826
1874
|
const hit = this.hitTest(wx, wy);
|
|
1827
1875
|
const newHov = hit?.id ?? null;
|
|
@@ -1838,20 +1886,7 @@ export class GraphCanvas {
|
|
|
1838
1886
|
onMouseUp(e) {
|
|
1839
1887
|
if (e.button !== 0)
|
|
1840
1888
|
return;
|
|
1841
|
-
if (this.
|
|
1842
|
-
if (!this.hasMoved) {
|
|
1843
|
-
const id = this.dragNode.id;
|
|
1844
|
-
const newSel = this.filter.selectedId === id ? null : id;
|
|
1845
|
-
this.filter = { ...this.filter, selectedId: newSel };
|
|
1846
|
-
this.particles = [];
|
|
1847
|
-
this.lastParticleSpawn = 0;
|
|
1848
|
-
this.onSelectNode(newSel);
|
|
1849
|
-
if (newSel)
|
|
1850
|
-
this.flyTo(this.dragNode);
|
|
1851
|
-
}
|
|
1852
|
-
this.dragNode.fx = null;
|
|
1853
|
-
this.dragNode.fy = null;
|
|
1854
|
-
this.dragNode = null;
|
|
1889
|
+
if (this.endNodeDrag()) {
|
|
1855
1890
|
this.isDraggingNode = false;
|
|
1856
1891
|
}
|
|
1857
1892
|
else if (this.isDraggingCanvas) {
|
|
@@ -1879,29 +1914,12 @@ export class GraphCanvas {
|
|
|
1879
1914
|
}
|
|
1880
1915
|
onTouchStart(e) {
|
|
1881
1916
|
e.preventDefault();
|
|
1882
|
-
const
|
|
1883
|
-
if (
|
|
1884
|
-
|
|
1885
|
-
const y = e.touches[0].clientY - rect.top;
|
|
1886
|
-
this.lastX = x;
|
|
1887
|
-
this.lastY = y;
|
|
1888
|
-
this.downX = x;
|
|
1889
|
-
this.downY = y;
|
|
1890
|
-
this.hasMoved = false;
|
|
1891
|
-
const [wx, wy] = this.toWorld(x, y);
|
|
1892
|
-
const hit = this.hitTest(wx, wy);
|
|
1893
|
-
if (hit) {
|
|
1894
|
-
this.isDraggingNode = true;
|
|
1895
|
-
this.dragNode = hit;
|
|
1896
|
-
hit.fx = hit.x;
|
|
1897
|
-
hit.fy = hit.y;
|
|
1898
|
-
this.alpha = Math.max(this.alpha, 0.25);
|
|
1899
|
-
}
|
|
1900
|
-
else {
|
|
1901
|
-
this.isDraggingCanvas = true;
|
|
1902
|
-
}
|
|
1917
|
+
const pos = this.firstTouchPos(e);
|
|
1918
|
+
if (pos) {
|
|
1919
|
+
this.beginPointerDrag(pos.x, pos.y, false);
|
|
1903
1920
|
}
|
|
1904
1921
|
else if (e.touches.length === 2) {
|
|
1922
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
1905
1923
|
this.isDraggingNode = false;
|
|
1906
1924
|
this.isDraggingCanvas = false;
|
|
1907
1925
|
if (this.dragNode) {
|
|
@@ -1918,28 +1936,17 @@ export class GraphCanvas {
|
|
|
1918
1936
|
}
|
|
1919
1937
|
onTouchMove(e) {
|
|
1920
1938
|
e.preventDefault();
|
|
1921
|
-
const
|
|
1922
|
-
if (
|
|
1923
|
-
const x
|
|
1924
|
-
const y = e.touches[0].clientY - rect.top;
|
|
1939
|
+
const pos = this.firstTouchPos(e);
|
|
1940
|
+
if (pos) {
|
|
1941
|
+
const { x, y } = pos;
|
|
1925
1942
|
const dx = x - this.lastX;
|
|
1926
1943
|
const dy = y - this.lastY;
|
|
1927
|
-
|
|
1928
|
-
this.hasMoved = true;
|
|
1929
|
-
if (this.isDraggingNode && this.dragNode) {
|
|
1930
|
-
const [wx, wy] = this.toWorld(x, y);
|
|
1931
|
-
this.dragNode.fx = wx;
|
|
1932
|
-
this.dragNode.fy = wy;
|
|
1933
|
-
}
|
|
1934
|
-
else if (this.isDraggingCanvas) {
|
|
1935
|
-
this.tx += dx;
|
|
1936
|
-
this.ty += dy;
|
|
1937
|
-
this.flyTarget = null;
|
|
1938
|
-
}
|
|
1944
|
+
this.handleDragMove(x, y, dx, dy, 5, false);
|
|
1939
1945
|
this.lastX = x;
|
|
1940
1946
|
this.lastY = y;
|
|
1941
1947
|
}
|
|
1942
1948
|
else if (e.touches.length === 2) {
|
|
1949
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
1943
1950
|
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
|
1944
1951
|
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
|
1945
1952
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
@@ -1957,21 +1964,7 @@ export class GraphCanvas {
|
|
|
1957
1964
|
onTouchEnd(e) {
|
|
1958
1965
|
e.preventDefault();
|
|
1959
1966
|
if (e.touches.length === 0) {
|
|
1960
|
-
|
|
1961
|
-
if (!this.hasMoved) {
|
|
1962
|
-
const id = this.dragNode.id;
|
|
1963
|
-
const newSel = this.filter.selectedId === id ? null : id;
|
|
1964
|
-
this.filter = { ...this.filter, selectedId: newSel };
|
|
1965
|
-
this.particles = [];
|
|
1966
|
-
this.lastParticleSpawn = 0;
|
|
1967
|
-
this.onSelectNode(newSel);
|
|
1968
|
-
if (newSel)
|
|
1969
|
-
this.flyTo(this.dragNode);
|
|
1970
|
-
}
|
|
1971
|
-
this.dragNode.fx = null;
|
|
1972
|
-
this.dragNode.fy = null;
|
|
1973
|
-
this.dragNode = null;
|
|
1974
|
-
}
|
|
1967
|
+
this.endNodeDrag();
|
|
1975
1968
|
this.isDraggingNode = false;
|
|
1976
1969
|
this.isDraggingCanvas = false;
|
|
1977
1970
|
}
|