@painekillers/tree-builder-viewer 0.0.0-stage → 1.0.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/index.js +2 -0
- package/package.json +8 -3
- package/search.js +120 -0
- package/theme.js +175 -0
- package/themes/dark.css +144 -0
- package/themes/default.css +488 -0
- package/tooltip.js +78 -0
- package/viewer.js +735 -0
- package/README.md +0 -3
package/viewer.js
ADDED
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
import {
|
|
2
|
+
World,
|
|
3
|
+
Camera,
|
|
4
|
+
Bridge
|
|
5
|
+
} from "@painekillers/tree-builder-core"
|
|
6
|
+
|
|
7
|
+
import SearchIndex from "./search.js"
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
Node,
|
|
11
|
+
Graph
|
|
12
|
+
} from "@painekillers/tree-builder-core"
|
|
13
|
+
|
|
14
|
+
import Tooltip from "./tooltip.js"
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
ThemeContainer,
|
|
18
|
+
ThemeApplier
|
|
19
|
+
} from "./theme.js"
|
|
20
|
+
|
|
21
|
+
export default class Viewer {
|
|
22
|
+
#container
|
|
23
|
+
#shadow
|
|
24
|
+
#canvas
|
|
25
|
+
#ui
|
|
26
|
+
|
|
27
|
+
#theme
|
|
28
|
+
|
|
29
|
+
#graph
|
|
30
|
+
#world
|
|
31
|
+
#bridge
|
|
32
|
+
|
|
33
|
+
#observer
|
|
34
|
+
|
|
35
|
+
#search
|
|
36
|
+
#tooltip
|
|
37
|
+
|
|
38
|
+
#enabled = false
|
|
39
|
+
|
|
40
|
+
recurse = true
|
|
41
|
+
/*
|
|
42
|
+
Null: No recursion
|
|
43
|
+
False: Creates fake nodes when encountering recursion
|
|
44
|
+
True: Connects back to existing instance of a node when recursing
|
|
45
|
+
#: Real recursion to depths #
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
constructor(container, themes) {
|
|
49
|
+
this.#container = container
|
|
50
|
+
|
|
51
|
+
this.#shadow = this.#container.attachShadow({
|
|
52
|
+
mode: "open"
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
this.#theme = new ThemeApplier(
|
|
56
|
+
themes ?? new ThemeContainer(),
|
|
57
|
+
this.#shadow
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
this.#canvas = document.createElement("canvas")
|
|
61
|
+
this.#ui = document.createElement("div")
|
|
62
|
+
this.#ui.id = "ui"
|
|
63
|
+
|
|
64
|
+
// Init the world
|
|
65
|
+
this.#world = new World(
|
|
66
|
+
this.#canvas,
|
|
67
|
+
this.#theme.current
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const cam = new Camera(this.#world)
|
|
71
|
+
|
|
72
|
+
this.#world.camera = cam
|
|
73
|
+
|
|
74
|
+
// Resize observer
|
|
75
|
+
this.#observer = new ResizeObserver(() => {
|
|
76
|
+
this.#world.resize(
|
|
77
|
+
this.#container.clientWidth,
|
|
78
|
+
this.#container.clientHeight
|
|
79
|
+
)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// Mouse interactions
|
|
83
|
+
let hovered = null
|
|
84
|
+
|
|
85
|
+
this.#canvas.addEventListener("mousemove", e => {
|
|
86
|
+
if (this.#world.paused) return
|
|
87
|
+
|
|
88
|
+
const pos = this.#world.mouseWorldPosition(e)
|
|
89
|
+
const object = this.#world.objectAt(pos.x, pos.y)
|
|
90
|
+
|
|
91
|
+
if (object === hovered) return
|
|
92
|
+
|
|
93
|
+
hovered?.mouseLeave?.()
|
|
94
|
+
|
|
95
|
+
hovered = object
|
|
96
|
+
|
|
97
|
+
hovered?.mouseEnter?.()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
this.#canvas.addEventListener("mousedown", e => {
|
|
101
|
+
if (this.#world.paused) return
|
|
102
|
+
|
|
103
|
+
const pos = this.#world.mouseWorldPosition(e)
|
|
104
|
+
const object = this.#world.objectAt(pos.x, pos.y)
|
|
105
|
+
|
|
106
|
+
if(object) {
|
|
107
|
+
console.log("clicked node:", object.node.val)
|
|
108
|
+
|
|
109
|
+
if(object.drawNode.layoutNode.original === null) {
|
|
110
|
+
this.nodeMenu(object.node)
|
|
111
|
+
} else {
|
|
112
|
+
this.#bridge.focus(
|
|
113
|
+
object.drawNode.layoutNode.original.node
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// Camera movements
|
|
120
|
+
let dragging = false
|
|
121
|
+
|
|
122
|
+
let lastX = 0
|
|
123
|
+
let lastY = 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
// Pan
|
|
127
|
+
|
|
128
|
+
this.#canvas.addEventListener("mousedown", e => {
|
|
129
|
+
if (e.button !== 0) return
|
|
130
|
+
|
|
131
|
+
dragging = true
|
|
132
|
+
|
|
133
|
+
lastX = e.clientX
|
|
134
|
+
lastY = e.clientY
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
this.#canvas.addEventListener("mouseup", () => {
|
|
139
|
+
dragging = false
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
this.#canvas.addEventListener("mouseleave", () => {
|
|
144
|
+
dragging = false
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
this.#canvas.addEventListener("mousemove", e => {
|
|
149
|
+
if (!dragging) return
|
|
150
|
+
|
|
151
|
+
const dx = e.clientX - lastX
|
|
152
|
+
const dy = e.clientY - lastY
|
|
153
|
+
|
|
154
|
+
lastX = e.clientX
|
|
155
|
+
lastY = e.clientY
|
|
156
|
+
|
|
157
|
+
const pos = cam.pos
|
|
158
|
+
|
|
159
|
+
cam.pos = [
|
|
160
|
+
pos.x - dx / cam.zoom,
|
|
161
|
+
pos.y - dy / cam.zoom
|
|
162
|
+
]
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
// Zoom toward mouse
|
|
167
|
+
|
|
168
|
+
this.#canvas.addEventListener("wheel", e => {
|
|
169
|
+
e.preventDefault()
|
|
170
|
+
|
|
171
|
+
const rect = this.#canvas.getBoundingClientRect()
|
|
172
|
+
|
|
173
|
+
const mouseX =
|
|
174
|
+
e.clientX - rect.left - this.#canvas.width / 2
|
|
175
|
+
|
|
176
|
+
const mouseY =
|
|
177
|
+
e.clientY - rect.top - this.#canvas.height / 2
|
|
178
|
+
|
|
179
|
+
const oldZoom = cam.zoom
|
|
180
|
+
|
|
181
|
+
const pos = cam.pos
|
|
182
|
+
|
|
183
|
+
// World position currently underneath mouse
|
|
184
|
+
const worldX = pos.x + mouseX / oldZoom
|
|
185
|
+
const worldY = pos.y + mouseY / oldZoom
|
|
186
|
+
|
|
187
|
+
const zoomSpeed = 0.001
|
|
188
|
+
|
|
189
|
+
let newZoom =
|
|
190
|
+
oldZoom * (1 - e.deltaY * zoomSpeed)
|
|
191
|
+
|
|
192
|
+
newZoom = Math.max(
|
|
193
|
+
0.1,
|
|
194
|
+
Math.min(newZoom, 5)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
cam.zoom = newZoom
|
|
198
|
+
|
|
199
|
+
// Keep the same world point underneath the mouse
|
|
200
|
+
cam.pos = [
|
|
201
|
+
worldX - mouseX / newZoom,
|
|
202
|
+
worldY - mouseY / newZoom
|
|
203
|
+
]
|
|
204
|
+
}, { passive: false })
|
|
205
|
+
|
|
206
|
+
// Init the bridge
|
|
207
|
+
this.#bridge = new Bridge(
|
|
208
|
+
null,
|
|
209
|
+
this.#world
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
// Init search index
|
|
213
|
+
this.#search = new SearchIndex([])
|
|
214
|
+
|
|
215
|
+
// Create UI
|
|
216
|
+
const searchButton = document.createElement("button")
|
|
217
|
+
searchButton.id = "search-button"
|
|
218
|
+
searchButton.ariaLabel = "Search"
|
|
219
|
+
|
|
220
|
+
const searchMenu = document.createElement("div")
|
|
221
|
+
searchMenu.id = "search-menu"
|
|
222
|
+
|
|
223
|
+
const searchHeader = document.createElement("div")
|
|
224
|
+
searchHeader.className = "menu-header"
|
|
225
|
+
|
|
226
|
+
const searchTitle = document.createElement("span")
|
|
227
|
+
searchTitle.textContent = "Search"
|
|
228
|
+
|
|
229
|
+
const searchMenuClose = document.createElement("button")
|
|
230
|
+
searchMenuClose.id = "search-menu-close"
|
|
231
|
+
searchMenuClose.textContent = "×"
|
|
232
|
+
|
|
233
|
+
searchHeader.append(
|
|
234
|
+
searchTitle,
|
|
235
|
+
searchMenuClose
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
const searchInput = document.createElement("input")
|
|
239
|
+
searchInput.id = "search-input"
|
|
240
|
+
searchInput.type = "text"
|
|
241
|
+
searchInput.placeholder = "Search nodes..."
|
|
242
|
+
|
|
243
|
+
const searchResults = document.createElement("div")
|
|
244
|
+
searchResults.id = "search-results"
|
|
245
|
+
|
|
246
|
+
searchMenu.append(
|
|
247
|
+
searchHeader,
|
|
248
|
+
searchInput,
|
|
249
|
+
searchResults
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
const themeButton = document.createElement("button")
|
|
254
|
+
themeButton.id = "theme-button"
|
|
255
|
+
|
|
256
|
+
const themeMenu = document.createElement("div")
|
|
257
|
+
themeMenu.id = "theme-menu"
|
|
258
|
+
|
|
259
|
+
const themeHeader = document.createElement("div")
|
|
260
|
+
themeHeader.className = "menu-header"
|
|
261
|
+
|
|
262
|
+
const themeTitle = document.createElement("span")
|
|
263
|
+
themeTitle.textContent = "Theme"
|
|
264
|
+
|
|
265
|
+
const themeMenuClose = document.createElement("button")
|
|
266
|
+
themeMenuClose.id = "theme-menu-close"
|
|
267
|
+
themeMenuClose.textContent = "×"
|
|
268
|
+
|
|
269
|
+
themeHeader.append(
|
|
270
|
+
themeTitle,
|
|
271
|
+
themeMenuClose
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
const themeGrid = document.createElement("div")
|
|
275
|
+
themeGrid.id = "theme-grid"
|
|
276
|
+
|
|
277
|
+
themeMenu.append(
|
|
278
|
+
themeHeader,
|
|
279
|
+
themeGrid
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
const nodeMenu = document.createElement("div")
|
|
284
|
+
nodeMenu.id = "node-menu"
|
|
285
|
+
|
|
286
|
+
const nodeHeader = document.createElement("div")
|
|
287
|
+
nodeHeader.className = "menu-header"
|
|
288
|
+
|
|
289
|
+
const nodeMenuTitle = document.createElement("span")
|
|
290
|
+
nodeMenuTitle.id = "node-menu-title"
|
|
291
|
+
nodeMenuTitle.textContent = "Node"
|
|
292
|
+
|
|
293
|
+
const nodeMenuClose = document.createElement("button")
|
|
294
|
+
nodeMenuClose.id = "node-menu-close"
|
|
295
|
+
nodeMenuClose.textContent = "×"
|
|
296
|
+
|
|
297
|
+
nodeHeader.append(
|
|
298
|
+
nodeMenuTitle,
|
|
299
|
+
nodeMenuClose
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
const nodeMenuContent = document.createElement("div")
|
|
303
|
+
nodeMenuContent.id = "node-menu-content"
|
|
304
|
+
|
|
305
|
+
nodeMenu.append(
|
|
306
|
+
nodeHeader,
|
|
307
|
+
nodeMenuContent
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
this.#ui.append(
|
|
312
|
+
searchButton,
|
|
313
|
+
searchMenu,
|
|
314
|
+
themeButton,
|
|
315
|
+
themeMenu,
|
|
316
|
+
nodeMenu
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
this.#shadow.append(
|
|
320
|
+
this.#canvas,
|
|
321
|
+
this.#ui
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
const menus = [
|
|
325
|
+
searchMenu,
|
|
326
|
+
themeMenu,
|
|
327
|
+
nodeMenu
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
function closeMenus() {
|
|
331
|
+
for(const menu of menus) {
|
|
332
|
+
menu.style.display = "none"
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function openMenu(menu) {
|
|
337
|
+
closeMenus()
|
|
338
|
+
menu.style.display = "block"
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
searchButton.addEventListener("click", () => {
|
|
342
|
+
if(searchMenu.style.display === "block") {
|
|
343
|
+
searchMenu.style.display = "none"
|
|
344
|
+
} else {
|
|
345
|
+
openMenu(searchMenu)
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
themeButton.addEventListener("click", () => {
|
|
350
|
+
if(themeMenu.style.display === "block") {
|
|
351
|
+
themeMenu.style.display = "none"
|
|
352
|
+
} else {
|
|
353
|
+
openMenu(themeMenu)
|
|
354
|
+
}
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
searchMenuClose.addEventListener("click", () => {
|
|
358
|
+
searchMenu.style.display = "none"
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
themeMenuClose.addEventListener("click", () => {
|
|
362
|
+
themeMenu.style.display = "none"
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
nodeMenuClose.addEventListener("click", () => {
|
|
366
|
+
nodeMenu.style.display = "none"
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
const blank = new Image()
|
|
370
|
+
blank.src = "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs="
|
|
371
|
+
|
|
372
|
+
const previewGraph = new Graph([
|
|
373
|
+
new Node("A", [], ["B", "C"], {icon: blank}),
|
|
374
|
+
new Node("B", ["A"], [], {icon: blank}),
|
|
375
|
+
new Node("C", ["A"], [], {icon: blank})
|
|
376
|
+
])
|
|
377
|
+
|
|
378
|
+
const updateThemeMenu = () => {
|
|
379
|
+
themeGrid.replaceChildren()
|
|
380
|
+
|
|
381
|
+
const hidden =
|
|
382
|
+
getComputedStyle(themeMenu).display === "none"
|
|
383
|
+
|
|
384
|
+
if(hidden) {
|
|
385
|
+
themeMenu.style.visibility = "hidden"
|
|
386
|
+
themeMenu.style.display = "block"
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
for(const [name, theme] of Object.entries(
|
|
390
|
+
this.#theme.container.themes
|
|
391
|
+
)) {
|
|
392
|
+
const entry = document.createElement("button")
|
|
393
|
+
entry.className = "theme-entry"
|
|
394
|
+
|
|
395
|
+
const preview = document.createElement("canvas")
|
|
396
|
+
preview.className = "theme-preview"
|
|
397
|
+
|
|
398
|
+
const label = document.createElement("div")
|
|
399
|
+
label.className = "theme-name"
|
|
400
|
+
label.textContent = name
|
|
401
|
+
|
|
402
|
+
entry.append(
|
|
403
|
+
preview,
|
|
404
|
+
label
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
themeGrid.append(entry)
|
|
408
|
+
|
|
409
|
+
const rect = preview.getBoundingClientRect()
|
|
410
|
+
|
|
411
|
+
preview.width = rect.width
|
|
412
|
+
preview.height = rect.height
|
|
413
|
+
|
|
414
|
+
const world = new World(
|
|
415
|
+
preview,
|
|
416
|
+
theme
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
world.background = theme.background
|
|
420
|
+
|
|
421
|
+
const camera = new Camera(world)
|
|
422
|
+
|
|
423
|
+
world.camera = camera
|
|
424
|
+
|
|
425
|
+
world.resize(
|
|
426
|
+
preview.width,
|
|
427
|
+
preview.height
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
const bridge = new Bridge(
|
|
431
|
+
previewGraph,
|
|
432
|
+
world
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
bridge.calculateLayout(
|
|
436
|
+
"A",
|
|
437
|
+
null
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
bridge.calculateDraw(
|
|
441
|
+
50,
|
|
442
|
+
50,
|
|
443
|
+
70,
|
|
444
|
+
50
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
bridge.createObjects(null)
|
|
448
|
+
|
|
449
|
+
camera.zoom = 0.4
|
|
450
|
+
camera.pos = [-40, 40]
|
|
451
|
+
|
|
452
|
+
world.update()
|
|
453
|
+
|
|
454
|
+
if(theme === this.#theme.current) {
|
|
455
|
+
entry.classList.add("selected")
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
entry.addEventListener("click", async () => {
|
|
459
|
+
await this.#theme.apply(name)
|
|
460
|
+
|
|
461
|
+
themeMenu.style.display = "none"
|
|
462
|
+
})
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if(hidden) {
|
|
466
|
+
themeMenu.style.display = "none"
|
|
467
|
+
themeMenu.style.visibility = ""
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const updateThemeButtons = () => {
|
|
472
|
+
searchButton.innerHTML =
|
|
473
|
+
this.#theme.current.searchButton
|
|
474
|
+
|
|
475
|
+
themeButton.innerHTML =
|
|
476
|
+
this.#theme.current.themeButton
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const updateSearchResults = () => {
|
|
480
|
+
const query = searchInput.value
|
|
481
|
+
|
|
482
|
+
const results = query
|
|
483
|
+
? this.#search.search(query)
|
|
484
|
+
: this.#search.entries.map(entry => ({
|
|
485
|
+
entry,
|
|
486
|
+
score: 1
|
|
487
|
+
}))
|
|
488
|
+
|
|
489
|
+
searchResults.replaceChildren()
|
|
490
|
+
|
|
491
|
+
for(const result of results) {
|
|
492
|
+
const entry = document.createElement("div")
|
|
493
|
+
entry.className = "search-entry"
|
|
494
|
+
|
|
495
|
+
const info = document.createElement("div")
|
|
496
|
+
|
|
497
|
+
const name = document.createElement("div")
|
|
498
|
+
name.className = "search-entry-name"
|
|
499
|
+
name.textContent = result.entry.name
|
|
500
|
+
|
|
501
|
+
info.append(name)
|
|
502
|
+
|
|
503
|
+
if(
|
|
504
|
+
result.entry.name !==
|
|
505
|
+
result.entry.value.id
|
|
506
|
+
) {
|
|
507
|
+
const alias = document.createElement("div")
|
|
508
|
+
alias.className = "search-entry-alias"
|
|
509
|
+
alias.textContent =
|
|
510
|
+
`(alias of ${result.entry.value.id})`
|
|
511
|
+
|
|
512
|
+
info.append(alias)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const actions = document.createElement("div")
|
|
516
|
+
actions.className = "search-entry-actions"
|
|
517
|
+
|
|
518
|
+
const focus = document.createElement("button")
|
|
519
|
+
focus.dataset.tooltip = "Focus"
|
|
520
|
+
focus.innerHTML =
|
|
521
|
+
this.#theme.current.focusButton
|
|
522
|
+
|
|
523
|
+
if(
|
|
524
|
+
!this.#bridge.toObj?.has(
|
|
525
|
+
result.entry.value
|
|
526
|
+
)
|
|
527
|
+
) {
|
|
528
|
+
focus.disabled = true
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
focus.addEventListener("click", () => {
|
|
532
|
+
this.#bridge.focus(
|
|
533
|
+
result.entry.value
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
searchMenu.style.display = "none"
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
const root = document.createElement("button")
|
|
540
|
+
root.dataset.tooltip = "Set root"
|
|
541
|
+
root.innerHTML =
|
|
542
|
+
this.#theme.current.rootButton
|
|
543
|
+
|
|
544
|
+
root.addEventListener("click", () => {
|
|
545
|
+
this.#bridge.calculateLayout(
|
|
546
|
+
result.entry.value.id,
|
|
547
|
+
this.recurse
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
this.#bridge.calculateDraw(
|
|
551
|
+
50,
|
|
552
|
+
50,
|
|
553
|
+
100,
|
|
554
|
+
50
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
this.#bridge.createObjects(
|
|
558
|
+
this.depth
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
searchMenu.style.display = "none"
|
|
562
|
+
|
|
563
|
+
updateSearchResults()
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
actions.append(
|
|
567
|
+
focus,
|
|
568
|
+
root
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
entry.append(
|
|
572
|
+
info,
|
|
573
|
+
actions
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
searchResults.append(entry)
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
this.nodeMenu = node => {
|
|
581
|
+
nodeMenuTitle.textContent = node.id
|
|
582
|
+
nodeMenuContent.replaceChildren()
|
|
583
|
+
|
|
584
|
+
openMenu(nodeMenu)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
this.#theme.subscribe(() => {
|
|
588
|
+
this.#world.theme =
|
|
589
|
+
this.#theme.current
|
|
590
|
+
|
|
591
|
+
this.#world.background =
|
|
592
|
+
this.#theme.current.background
|
|
593
|
+
|
|
594
|
+
this.#world.update()
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
this.#world.background = this.#theme.current.background
|
|
598
|
+
|
|
599
|
+
this.#theme.subscribe(updateSearchResults)
|
|
600
|
+
this.#theme.subscribe(updateThemeMenu)
|
|
601
|
+
this.#theme.subscribe(updateThemeButtons)
|
|
602
|
+
|
|
603
|
+
searchInput.addEventListener(
|
|
604
|
+
"input",
|
|
605
|
+
updateSearchResults
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
updateThemeButtons()
|
|
609
|
+
updateThemeMenu()
|
|
610
|
+
updateSearchResults()
|
|
611
|
+
|
|
612
|
+
// Init tooltip
|
|
613
|
+
this.tooltip = {
|
|
614
|
+
offset: 12,
|
|
615
|
+
duration: 120
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
get graph() {
|
|
620
|
+
return this.#graph
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
set graph(graph) {
|
|
624
|
+
this.#graph = graph
|
|
625
|
+
this.#bridge.graph = graph
|
|
626
|
+
|
|
627
|
+
this.#search.entries =
|
|
628
|
+
Array.from(graph?.nodes.values()).map(node => ({
|
|
629
|
+
name: node.id,
|
|
630
|
+
value: node
|
|
631
|
+
}))
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
set tooltip({
|
|
635
|
+
offset = 8,
|
|
636
|
+
duration = 120
|
|
637
|
+
} = {}) {
|
|
638
|
+
if(this.#tooltip) {
|
|
639
|
+
this.#tooltip.offset = offset
|
|
640
|
+
this.#tooltip.duration = duration
|
|
641
|
+
return
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
this.#tooltip = new Tooltip(
|
|
645
|
+
this.#ui,
|
|
646
|
+
{
|
|
647
|
+
offset: offset,
|
|
648
|
+
duration: duration
|
|
649
|
+
}
|
|
650
|
+
)
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
setRoot(root) {
|
|
654
|
+
if(!this.#graph) {
|
|
655
|
+
console.warn(
|
|
656
|
+
"Graph must be set before drawing"
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
return
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
this.#bridge.calculateLayout(
|
|
663
|
+
root,
|
|
664
|
+
this.recurse
|
|
665
|
+
)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
setSize(w, h, px, py) {
|
|
669
|
+
if(!this.#bridge.root) {
|
|
670
|
+
console.warn(
|
|
671
|
+
"Root must be set before setting size"
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
return
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
this.#bridge.calculateDraw(
|
|
678
|
+
w ?? 50,
|
|
679
|
+
h ?? 50,
|
|
680
|
+
px ?? 100,
|
|
681
|
+
py ?? 50
|
|
682
|
+
)
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
draw(maxDepth) {
|
|
686
|
+
if(!this.#bridge.draw) {
|
|
687
|
+
console.warn(
|
|
688
|
+
"SetSize must be called first"
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
return
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
this.depth = maxDepth
|
|
695
|
+
|
|
696
|
+
this.#bridge.createObjects(
|
|
697
|
+
maxDepth ?? null
|
|
698
|
+
)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
enable() {
|
|
702
|
+
if(this.#enabled) return
|
|
703
|
+
|
|
704
|
+
if(!this.#graph) {
|
|
705
|
+
console.warn(
|
|
706
|
+
"Graph must be set before enabling the viewer"
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
return
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
this.#enabled = true
|
|
713
|
+
|
|
714
|
+
this.#world.resize(
|
|
715
|
+
this.#container.clientWidth,
|
|
716
|
+
this.#container.clientHeight
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
this.#observer.observe(
|
|
720
|
+
this.#container
|
|
721
|
+
)
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
disable() {
|
|
725
|
+
if(!this.#enabled) return
|
|
726
|
+
|
|
727
|
+
this.#enabled = false
|
|
728
|
+
|
|
729
|
+
this.#observer.unobserve(
|
|
730
|
+
this.#container
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
this.#tooltip?.hide()
|
|
734
|
+
}
|
|
735
|
+
}
|