jekyll-theme-zer0 1.27.0 → 1.28.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +386 -4
- data/_data/consumers.yml +147 -0
- data/_data/features.yml +77 -5
- data/_data/i18n/fr.yml +98 -0
- data/_data/i18n/manifest.yml +1502 -0
- data/_data/navigation/docs.yml +2 -0
- data/_data/navigation/main.yml +6 -0
- data/_data/series.yml +19 -0
- data/_data/theme-manifest.yml +348 -266
- data/_includes/README.md +21 -2
- data/_includes/analytics/google-tag-manager-body.html +11 -2
- data/_includes/analytics/google-tag-manager-head.html +16 -6
- data/_includes/analytics/posthog.html +33 -4
- data/_includes/components/abc-letter.html +43 -0
- data/_includes/components/book-card.html +42 -0
- data/_includes/components/book-nav.html +80 -0
- data/_includes/components/book-plate.html +31 -0
- data/_includes/components/book-toc.html +47 -0
- data/_includes/components/bookshelf.html +68 -0
- data/_includes/components/card-grid.html +60 -0
- data/_includes/components/data-card.html +95 -0
- data/_includes/components/halfmoon.html +5 -1
- data/_includes/components/page-feedback.html +65 -2
- data/_includes/components/theme-controls-bar.html +10 -2
- data/_includes/components/theme-customizer.html +8 -2
- data/_includes/content/seo.html +9 -3
- data/_includes/core/color-mode-init.html +13 -4
- data/_includes/core/favicon.html +46 -0
- data/_includes/core/head.html +9 -0
- data/_includes/custom/body-end.html +18 -0
- data/_includes/custom/body-start.html +17 -0
- data/_includes/custom/footer.html +18 -0
- data/_includes/custom/head.html +18 -0
- data/_includes/navigation/local-graph.html +28 -2
- data/_includes/navigation/nav-tree.html +3 -3
- data/_includes/navigation/sidebar-config.html +21 -0
- data/_includes/navigation/sidebar-nav.html +4 -0
- data/_includes/navigation/sidebar-pagetree.html +150 -0
- data/_includes/navigation/unified-drawer.html +8 -2
- data/_includes/obsidian/full-graph.html +165 -136
- data/_layouts/404.html +260 -0
- data/_layouts/README.md +2 -0
- data/_layouts/book-abc.html +106 -0
- data/_layouts/book-story.html +91 -0
- data/_layouts/book.html +113 -0
- data/_layouts/collection.html +17 -5
- data/_layouts/home.html +5 -2
- data/_layouts/landing.html +9 -0
- data/_layouts/news.html +155 -31
- data/_layouts/root.html +16 -2
- data/_layouts/section.html +57 -15
- data/_sass/components/_book.scss +423 -0
- data/_sass/core/_navbar.scss +13 -0
- data/_sass/core/_obsidian.scss +286 -6
- data/_sass/theme/_backgrounds.scss +21 -8
- data/assets/css/main.scss +1 -0
- data/assets/js/auto-hide-nav.js +5 -1
- data/assets/js/halfmoon.js +26 -0
- data/assets/js/obsidian-graph.js +702 -264
- data/assets/js/obsidian-local-graph.js +161 -54
- data/assets/js/search-modal.js +4 -1
- data/scripts/bin/manifest +31 -4
- data/scripts/bin/validate +5 -1
- data/scripts/install/README.md +47 -6
- data/scripts/install/ai/client.sh +302 -93
- data/scripts/install/ai/prompts/spec.schema.json +1 -1
- data/scripts/install/ai/wizard.sh +10 -5
- data/scripts/install/apply.sh +7 -3
- data/scripts/install/cli.sh +54 -4
- data/scripts/install/config.sh +167 -0
- data/scripts/install/doctor.sh +38 -0
- data/scripts/install/plan.sh +10 -0
- data/scripts/install/spec.sh +15 -7
- data/scripts/install/template.sh +4 -0
- data/scripts/propagate.rb +277 -0
- data/scripts/translate.rb +90 -0
- metadata +27 -2
data/assets/js/obsidian-graph.js
CHANGED
|
@@ -19,15 +19,24 @@
|
|
|
19
19
|
* targets become floating "broken" nodes drawn in red so the graph also
|
|
20
20
|
* surfaces dangling links.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
22
|
+
* Mirrors the Obsidian desktop graph controls: Filters (search with tag:/
|
|
23
|
+
* path: operators, per-collection toggles, orphans, unresolved), Display
|
|
24
|
+
* (arrows, node size, link thickness, label fade threshold) and Forces
|
|
25
|
+
* (repulsion, link distance, animated re-layout). Settings persist to
|
|
26
|
+
* localStorage. The stylesheet re-derives itself when the Bootstrap
|
|
27
|
+
* color mode (data-bs-theme) changes — no reload needed.
|
|
28
|
+
*
|
|
29
|
+
* Public hooks (read-only, used by test/visual/features/obsidian-graph.spec.js):
|
|
23
30
|
* window.ObsidianGraph.cy — cytoscape instance once initialized
|
|
24
31
|
* window.ObsidianGraph.byKey — normalized lookup table
|
|
25
32
|
* window.ObsidianGraph.entries — raw entries array
|
|
33
|
+
* window.ObsidianGraph.settings — live settings object
|
|
26
34
|
*/
|
|
27
35
|
(function () {
|
|
28
36
|
'use strict';
|
|
29
37
|
|
|
30
38
|
var CONTAINER_ID = 'obsidian-graph';
|
|
39
|
+
var STORAGE_KEY = 'zer0.obsidianGraph.v1';
|
|
31
40
|
// Index URL resolution mirrors obsidian-wiki-links.js: the baseurl-aware
|
|
32
41
|
// value js-cdn.html emits (OBSIDIAN_CONFIG.wikiIndexUrl), then the legacy
|
|
33
42
|
// per-page override global, then a <base>-relative fallback.
|
|
@@ -35,6 +44,61 @@
|
|
|
35
44
|
window.OBSIDIAN_WIKI_INDEX_URL ||
|
|
36
45
|
((document.querySelector('base') || {}).href || '/') + 'assets/data/wiki-index.json');
|
|
37
46
|
|
|
47
|
+
var HUB_DEGREE_THRESHOLD = 12;
|
|
48
|
+
|
|
49
|
+
// User-tunable view state. Multipliers are relative to the base style so
|
|
50
|
+
// "1" always means the stock look. Persisted per-browser via localStorage.
|
|
51
|
+
var DEFAULTS = {
|
|
52
|
+
collections: {}, // name -> false when hidden; missing = visible
|
|
53
|
+
showOrphans: false, // matches Obsidian's "Show orphans" off state
|
|
54
|
+
showBroken: true, // unresolved [[targets]] as dashed red nodes
|
|
55
|
+
showArrows: true,
|
|
56
|
+
nodeScale: 1, // 0.5 – 2
|
|
57
|
+
linkScale: 1, // 0.5 – 3
|
|
58
|
+
labelZoom: 1.25, // zoom level past which labels appear (0.4 – 2.5)
|
|
59
|
+
repulsion: 1, // 0.3 – 3 × base node repulsion
|
|
60
|
+
linkDistance: 1, // 0.4 – 2.5 × base ideal edge length
|
|
61
|
+
animateLayout: true // ignored when prefers-reduced-motion
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function clamp(value, min, max, fallback) {
|
|
65
|
+
var n = parseFloat(value);
|
|
66
|
+
if (!isFinite(n)) return fallback;
|
|
67
|
+
return Math.min(max, Math.max(min, n));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function loadSettings() {
|
|
71
|
+
var merged = JSON.parse(JSON.stringify(DEFAULTS));
|
|
72
|
+
try {
|
|
73
|
+
var raw = window.localStorage.getItem(STORAGE_KEY);
|
|
74
|
+
if (!raw) return merged;
|
|
75
|
+
var saved = JSON.parse(raw);
|
|
76
|
+
if (!saved || typeof saved !== 'object') return merged;
|
|
77
|
+
if (saved.collections && typeof saved.collections === 'object') {
|
|
78
|
+
merged.collections = saved.collections;
|
|
79
|
+
}
|
|
80
|
+
['showOrphans', 'showBroken', 'showArrows', 'animateLayout'].forEach(function (k) {
|
|
81
|
+
if (typeof saved[k] === 'boolean') merged[k] = saved[k];
|
|
82
|
+
});
|
|
83
|
+
merged.nodeScale = clamp(saved.nodeScale, 0.5, 2, merged.nodeScale);
|
|
84
|
+
merged.linkScale = clamp(saved.linkScale, 0.5, 3, merged.linkScale);
|
|
85
|
+
merged.labelZoom = clamp(saved.labelZoom, 0.4, 2.5, merged.labelZoom);
|
|
86
|
+
merged.repulsion = clamp(saved.repulsion, 0.3, 3, merged.repulsion);
|
|
87
|
+
merged.linkDistance = clamp(saved.linkDistance, 0.4, 2.5, merged.linkDistance);
|
|
88
|
+
} catch (e) { /* private mode / corrupt payload — fall back to defaults */ }
|
|
89
|
+
return merged;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
var saveTimer = null;
|
|
93
|
+
function saveSettings(settings) {
|
|
94
|
+
clearTimeout(saveTimer);
|
|
95
|
+
saveTimer = setTimeout(function () {
|
|
96
|
+
try {
|
|
97
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
|
98
|
+
} catch (e) { /* storage unavailable — view state is session-only */ }
|
|
99
|
+
}, 150);
|
|
100
|
+
}
|
|
101
|
+
|
|
38
102
|
function normalize(value) {
|
|
39
103
|
return String(value || '').toLowerCase().trim().replace(/\s+/g, ' ');
|
|
40
104
|
}
|
|
@@ -43,6 +107,22 @@
|
|
|
43
107
|
return !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
|
|
44
108
|
}
|
|
45
109
|
|
|
110
|
+
function debounce(fn, wait) {
|
|
111
|
+
var t;
|
|
112
|
+
return function () {
|
|
113
|
+
var ctx = this;
|
|
114
|
+
var args = arguments;
|
|
115
|
+
clearTimeout(t);
|
|
116
|
+
t = setTimeout(function () { fn.apply(ctx, args); }, wait);
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function escapeHtml(value) {
|
|
121
|
+
return String(value == null ? '' : value)
|
|
122
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
123
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
124
|
+
}
|
|
125
|
+
|
|
46
126
|
// Cytoscape is vendored under assets/vendor/ (no runtime CDN). Path comes from
|
|
47
127
|
// window.OBSIDIAN_CONFIG.cytoscapeUrl (set by Liquid), with a base fallback.
|
|
48
128
|
function cytoscapeSrc() {
|
|
@@ -99,27 +179,7 @@
|
|
|
99
179
|
}
|
|
100
180
|
|
|
101
181
|
// Cytoscape needs concrete color values (not CSS vars). readTheme()
|
|
102
|
-
// resolves light/dark
|
|
103
|
-
// Shared cose layout — looser packing, more edge length, wider padding.
|
|
104
|
-
var COSE_LAYOUT = {
|
|
105
|
-
name: 'cose',
|
|
106
|
-
animate: false,
|
|
107
|
-
randomize: true,
|
|
108
|
-
nodeRepulsion: function () { return 32000; },
|
|
109
|
-
idealEdgeLength: function () { return 190; },
|
|
110
|
-
edgeElasticity: function () { return 60; },
|
|
111
|
-
nodeOverlap: 40,
|
|
112
|
-
gravity: 0.1,
|
|
113
|
-
nestingFactor: 1.5,
|
|
114
|
-
numIter: 3000,
|
|
115
|
-
padding: 80,
|
|
116
|
-
componentSpacing: 140
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
// Labels appear when zoomed past this level or on hover/highlight.
|
|
120
|
-
var LABEL_ZOOM_THRESHOLD = 1.25;
|
|
121
|
-
var HUB_DEGREE_THRESHOLD = 12;
|
|
122
|
-
|
|
182
|
+
// resolves light/dark from the live Bootstrap color mode.
|
|
123
183
|
function readTheme() {
|
|
124
184
|
var attr = (document.documentElement.getAttribute('data-bs-theme') ||
|
|
125
185
|
document.body.getAttribute('data-bs-theme') || '').toLowerCase();
|
|
@@ -130,56 +190,63 @@
|
|
|
130
190
|
labelOutline: 'rgba(13,17,23,0.88)',
|
|
131
191
|
edge: 'rgba(173,181,189,0.28)',
|
|
132
192
|
edgeArrow: 'rgba(173,181,189,0.42)',
|
|
133
|
-
canvasBg: 'transparent',
|
|
134
193
|
nodeBorder: 'rgba(255,255,255,0.18)'
|
|
135
194
|
} : {
|
|
136
195
|
label: 'rgba(27,31,35,0.92)',
|
|
137
196
|
labelOutline: 'rgba(248,249,250,0.90)',
|
|
138
197
|
edge: 'rgba(73,80,87,0.22)',
|
|
139
198
|
edgeArrow: 'rgba(73,80,87,0.38)',
|
|
140
|
-
canvasBg: 'transparent',
|
|
141
199
|
nodeBorder: 'rgba(0,0,0,0.14)'
|
|
142
200
|
};
|
|
143
201
|
}
|
|
144
202
|
|
|
203
|
+
var COLLECTION_PALETTE = {
|
|
204
|
+
posts: [13, 110, 253],
|
|
205
|
+
docs: [25, 135, 84],
|
|
206
|
+
notes: [111, 66, 193],
|
|
207
|
+
notebooks: [214, 51, 132],
|
|
208
|
+
quickstart: [253, 126, 20],
|
|
209
|
+
about: [32, 201, 151],
|
|
210
|
+
hobbies: [255, 193, 7],
|
|
211
|
+
news: [102, 16, 242],
|
|
212
|
+
services: [13, 202, 240],
|
|
213
|
+
books: [220, 53, 133]
|
|
214
|
+
};
|
|
215
|
+
|
|
145
216
|
function collectionColor(name, alpha) {
|
|
146
217
|
alpha = alpha == null ? 0.72 : alpha;
|
|
147
|
-
|
|
148
|
-
var palette = {
|
|
149
|
-
posts: [13, 110, 253],
|
|
150
|
-
docs: [25, 135, 84],
|
|
151
|
-
notes: [111, 66, 193],
|
|
152
|
-
notebooks: [214, 51, 132],
|
|
153
|
-
quickstart: [253, 126, 20],
|
|
154
|
-
about: [32, 201, 151],
|
|
155
|
-
hobbies: [255, 193, 7],
|
|
156
|
-
news: [102, 16, 242],
|
|
157
|
-
services: [13, 202, 240]
|
|
158
|
-
};
|
|
159
|
-
var rgb = palette[name] || [108, 117, 125];
|
|
218
|
+
var rgb = COLLECTION_PALETTE[name] || [108, 117, 125];
|
|
160
219
|
return 'rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',' + alpha + ')';
|
|
161
220
|
}
|
|
162
221
|
|
|
222
|
+
function baseNodeSize(degree) {
|
|
223
|
+
// Same mapping the old mapData(degree, 0, 25, 8, 34) produced, computed in
|
|
224
|
+
// data so the Display "node size" multiplier can rescale it live.
|
|
225
|
+
var d = Math.min(Math.max(degree || 0, 0), 25);
|
|
226
|
+
return 8 + (d / 25) * 26;
|
|
227
|
+
}
|
|
228
|
+
|
|
163
229
|
function buildElements(entries, byKey) {
|
|
164
230
|
var elements = [];
|
|
165
|
-
var seenTargets = Object.create(null);
|
|
166
231
|
var brokenIds = Object.create(null);
|
|
167
232
|
|
|
168
233
|
entries.forEach(function (entry) {
|
|
169
|
-
var nid = entry.url;
|
|
170
234
|
elements.push({
|
|
171
235
|
group: 'nodes',
|
|
172
236
|
data: {
|
|
173
|
-
id:
|
|
174
|
-
label: entry.title || entry.basename ||
|
|
237
|
+
id: entry.url,
|
|
238
|
+
label: entry.title || entry.basename || entry.url,
|
|
175
239
|
url: entry.url,
|
|
176
240
|
collection: entry.collection || 'page',
|
|
177
241
|
color: collectionColor(entry.collection),
|
|
178
242
|
excerpt: entry.excerpt || '',
|
|
243
|
+
// Search-operator fodder (tag:/path: + alias matches). Client-side
|
|
244
|
+
// only — the wiki-index.json contract is unchanged.
|
|
245
|
+
tags: (entry.tags || []).map(normalize),
|
|
246
|
+
aliasText: (entry.aliases || []).map(normalize).join(' | '),
|
|
179
247
|
broken: false
|
|
180
248
|
}
|
|
181
249
|
});
|
|
182
|
-
seenTargets[nid] = true;
|
|
183
250
|
});
|
|
184
251
|
|
|
185
252
|
entries.forEach(function (entry) {
|
|
@@ -203,6 +270,8 @@
|
|
|
203
270
|
collection: 'broken',
|
|
204
271
|
color: 'rgba(220,53,69,0.78)',
|
|
205
272
|
excerpt: 'Unresolved wiki-link',
|
|
273
|
+
tags: [],
|
|
274
|
+
aliasText: '',
|
|
206
275
|
broken: true
|
|
207
276
|
}
|
|
208
277
|
});
|
|
@@ -233,261 +302,620 @@
|
|
|
233
302
|
elements.forEach(function (el) {
|
|
234
303
|
if (el.group !== 'nodes') return;
|
|
235
304
|
el.data.degree = degree[el.data.id] || 0;
|
|
305
|
+
el.data.size = baseNodeSize(el.data.degree);
|
|
236
306
|
});
|
|
237
307
|
}
|
|
238
308
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// Graph application — wraps one cytoscape instance plus the settings-driven
|
|
311
|
+
// style/filter/layout plumbing shared by every control.
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
function GraphApp(container, entries, byKey) {
|
|
314
|
+
this.container = container;
|
|
315
|
+
this.entries = entries;
|
|
316
|
+
this.byKey = byKey;
|
|
317
|
+
this.settings = loadSettings();
|
|
318
|
+
this.cy = null;
|
|
319
|
+
this.searchQuery = '';
|
|
247
320
|
}
|
|
248
321
|
|
|
249
|
-
function
|
|
250
|
-
if (typeof window.cytoscape !== 'function') {
|
|
251
|
-
container.innerHTML =
|
|
252
|
-
'<div class="alert alert-danger" role="alert">' +
|
|
253
|
-
'Graph view failed to load: <code>cytoscape</code> library is not ' +
|
|
254
|
-
'available. Check your network connection or content security policy.' +
|
|
255
|
-
'</div>';
|
|
256
|
-
return null;
|
|
257
|
-
}
|
|
258
|
-
|
|
322
|
+
GraphApp.prototype.buildStyle = function () {
|
|
259
323
|
var theme = readTheme();
|
|
260
|
-
|
|
261
|
-
var
|
|
262
|
-
|
|
324
|
+
var s = this.settings;
|
|
325
|
+
var motion = prefersReducedMotion() ? '0ms' : '160ms';
|
|
326
|
+
return [
|
|
327
|
+
{
|
|
328
|
+
selector: 'node',
|
|
329
|
+
style: {
|
|
330
|
+
'background-color': 'data(color)',
|
|
331
|
+
'background-opacity': 0.82,
|
|
332
|
+
'label': 'data(label)',
|
|
333
|
+
'font-size': '10px',
|
|
334
|
+
'font-weight': 500,
|
|
335
|
+
'color': theme.label,
|
|
336
|
+
'text-outline-color': theme.labelOutline,
|
|
337
|
+
'text-outline-width': 3,
|
|
338
|
+
'text-outline-opacity': 1,
|
|
339
|
+
'text-background-opacity': 0,
|
|
340
|
+
'text-border-opacity': 0,
|
|
341
|
+
'text-valign': 'bottom',
|
|
342
|
+
'text-margin-y': 6,
|
|
343
|
+
'text-wrap': 'ellipsis',
|
|
344
|
+
'text-max-width': '120px',
|
|
345
|
+
// Hidden until zoomed-in, hovered, or a high-degree hub.
|
|
346
|
+
'min-zoomed-font-size': 11,
|
|
347
|
+
'text-opacity': 0,
|
|
348
|
+
'width': 'data(size)',
|
|
349
|
+
'height': 'data(size)',
|
|
350
|
+
'border-width': 1,
|
|
351
|
+
'border-color': theme.nodeBorder,
|
|
352
|
+
'opacity': 0.88,
|
|
353
|
+
'transition-property': 'background-color, border-color, width, height, text-opacity, opacity',
|
|
354
|
+
'transition-duration': motion
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
// Hub landmarks stay labeled when zoomed out.
|
|
359
|
+
selector: 'node[degree >= ' + HUB_DEGREE_THRESHOLD + ']',
|
|
360
|
+
style: {
|
|
361
|
+
'text-opacity': 0.88,
|
|
362
|
+
'font-size': '11px',
|
|
363
|
+
'font-weight': 600,
|
|
364
|
+
'min-zoomed-font-size': 0
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
selector: 'node[?broken]',
|
|
369
|
+
style: {
|
|
370
|
+
'border-style': 'dashed',
|
|
371
|
+
'border-color': 'rgba(220,53,69,0.75)',
|
|
372
|
+
'border-width': 1.5,
|
|
373
|
+
'background-opacity': 0.55,
|
|
374
|
+
'opacity': 0.78
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
selector: 'edge',
|
|
379
|
+
style: {
|
|
380
|
+
'curve-style': 'bezier',
|
|
381
|
+
'width': s.linkScale,
|
|
382
|
+
'line-color': theme.edge,
|
|
383
|
+
'target-arrow-color': theme.edgeArrow,
|
|
384
|
+
'target-arrow-shape': s.showArrows ? 'triangle' : 'none',
|
|
385
|
+
'arrow-scale': 0.75,
|
|
386
|
+
'opacity': 0.55,
|
|
387
|
+
'transition-property': 'line-color, width, opacity',
|
|
388
|
+
'transition-duration': motion
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
selector: 'edge[?broken]',
|
|
393
|
+
style: {
|
|
394
|
+
'line-color': 'rgba(220,53,69,0.32)',
|
|
395
|
+
'target-arrow-color': 'rgba(220,53,69,0.42)',
|
|
396
|
+
'line-style': 'dashed',
|
|
397
|
+
'opacity': 0.45
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
selector: '.faded',
|
|
402
|
+
style: {
|
|
403
|
+
'opacity': 0.08,
|
|
404
|
+
'text-opacity': 0
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
selector: 'node.highlighted',
|
|
409
|
+
style: {
|
|
410
|
+
'border-color': 'rgba(253,126,20,0.95)',
|
|
411
|
+
'border-width': 2.5,
|
|
412
|
+
'opacity': 1,
|
|
413
|
+
'background-opacity': 0.95,
|
|
414
|
+
'text-opacity': 1,
|
|
415
|
+
'font-size': '11px',
|
|
416
|
+
'font-weight': 700,
|
|
417
|
+
'min-zoomed-font-size': 0,
|
|
418
|
+
'z-index': 9999
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
selector: 'edge.highlighted',
|
|
423
|
+
style: {
|
|
424
|
+
'line-color': 'rgba(253,126,20,0.85)',
|
|
425
|
+
'target-arrow-color': 'rgba(253,126,20,0.85)',
|
|
426
|
+
'width': Math.max(2, 2 * this.settings.linkScale),
|
|
427
|
+
'opacity': 0.95,
|
|
428
|
+
'z-index': 9999
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
// Filtered out by the Filters panel. display:none also hides the
|
|
433
|
+
// connected edges, so the visible subgraph stays consistent.
|
|
434
|
+
selector: '.obsidian-hidden',
|
|
435
|
+
style: { 'display': 'none' }
|
|
436
|
+
}
|
|
437
|
+
];
|
|
438
|
+
};
|
|
263
439
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
440
|
+
GraphApp.prototype.render = function () {
|
|
441
|
+
var app = this;
|
|
442
|
+
this.cy = window.cytoscape({
|
|
443
|
+
container: this.container,
|
|
444
|
+
elements: this.elements,
|
|
267
445
|
minZoom: 0.1,
|
|
268
446
|
maxZoom: 4,
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
style: {
|
|
273
|
-
'background-color': 'data(color)',
|
|
274
|
-
'background-opacity': 0.82,
|
|
275
|
-
'label': 'data(label)',
|
|
276
|
-
'font-size': '10px',
|
|
277
|
-
'font-weight': 500,
|
|
278
|
-
'color': theme.label,
|
|
279
|
-
'text-outline-color': theme.labelOutline,
|
|
280
|
-
'text-outline-width': 3,
|
|
281
|
-
'text-outline-opacity': 1,
|
|
282
|
-
'text-background-opacity': 0,
|
|
283
|
-
'text-border-opacity': 0,
|
|
284
|
-
'text-valign': 'bottom',
|
|
285
|
-
'text-margin-y': 6,
|
|
286
|
-
'text-wrap': 'ellipsis',
|
|
287
|
-
'text-max-width': '120px',
|
|
288
|
-
// Hidden until zoomed-in, hovered, or a high-degree hub.
|
|
289
|
-
'min-zoomed-font-size': 11,
|
|
290
|
-
'text-opacity': 0,
|
|
291
|
-
'width': 'mapData(degree, 0, 25, 8, 34)',
|
|
292
|
-
'height': 'mapData(degree, 0, 25, 8, 34)',
|
|
293
|
-
'border-width': 1,
|
|
294
|
-
'border-color': theme.nodeBorder,
|
|
295
|
-
'opacity': 0.88,
|
|
296
|
-
'transition-property': 'background-color, border-color, width, height, text-opacity, opacity',
|
|
297
|
-
'transition-duration': nodeDur
|
|
298
|
-
}
|
|
299
|
-
},
|
|
300
|
-
{
|
|
301
|
-
// Hub landmarks stay labeled when zoomed out.
|
|
302
|
-
selector: 'node[degree >= ' + HUB_DEGREE_THRESHOLD + ']',
|
|
303
|
-
style: {
|
|
304
|
-
'text-opacity': 0.88,
|
|
305
|
-
'font-size': '11px',
|
|
306
|
-
'font-weight': 600,
|
|
307
|
-
'min-zoomed-font-size': 0
|
|
308
|
-
}
|
|
309
|
-
},
|
|
310
|
-
{
|
|
311
|
-
selector: 'node[?broken]',
|
|
312
|
-
style: {
|
|
313
|
-
'border-style': 'dashed',
|
|
314
|
-
'border-color': 'rgba(220,53,69,0.75)',
|
|
315
|
-
'border-width': 1.5,
|
|
316
|
-
'background-opacity': 0.55,
|
|
317
|
-
'opacity': 0.78
|
|
318
|
-
}
|
|
319
|
-
},
|
|
320
|
-
{
|
|
321
|
-
selector: 'edge',
|
|
322
|
-
style: {
|
|
323
|
-
'curve-style': 'bezier',
|
|
324
|
-
'width': 1,
|
|
325
|
-
'line-color': theme.edge,
|
|
326
|
-
'target-arrow-color': theme.edgeArrow,
|
|
327
|
-
'target-arrow-shape': 'triangle',
|
|
328
|
-
'arrow-scale': 0.75,
|
|
329
|
-
'opacity': 0.55,
|
|
330
|
-
'transition-property': 'line-color, width, opacity',
|
|
331
|
-
'transition-duration': edgeDur
|
|
332
|
-
}
|
|
333
|
-
},
|
|
334
|
-
{
|
|
335
|
-
selector: 'edge[?broken]',
|
|
336
|
-
style: {
|
|
337
|
-
'line-color': 'rgba(220,53,69,0.32)',
|
|
338
|
-
'target-arrow-color': 'rgba(220,53,69,0.42)',
|
|
339
|
-
'line-style': 'dashed',
|
|
340
|
-
'opacity': 0.45
|
|
341
|
-
}
|
|
342
|
-
},
|
|
343
|
-
{
|
|
344
|
-
selector: '.faded',
|
|
345
|
-
style: {
|
|
346
|
-
'opacity': 0.08,
|
|
347
|
-
'text-opacity': 0
|
|
348
|
-
}
|
|
349
|
-
},
|
|
350
|
-
{
|
|
351
|
-
selector: 'node.highlighted',
|
|
352
|
-
style: {
|
|
353
|
-
'border-color': 'rgba(253,126,20,0.95)',
|
|
354
|
-
'border-width': 2.5,
|
|
355
|
-
'opacity': 1,
|
|
356
|
-
'background-opacity': 0.95,
|
|
357
|
-
'text-opacity': 1,
|
|
358
|
-
'font-size': '11px',
|
|
359
|
-
'font-weight': 700,
|
|
360
|
-
'min-zoomed-font-size': 0,
|
|
361
|
-
'z-index': 9999
|
|
362
|
-
}
|
|
363
|
-
},
|
|
364
|
-
{
|
|
365
|
-
selector: 'edge.highlighted',
|
|
366
|
-
style: {
|
|
367
|
-
'line-color': 'rgba(253,126,20,0.85)',
|
|
368
|
-
'target-arrow-color': 'rgba(253,126,20,0.85)',
|
|
369
|
-
'width': 2,
|
|
370
|
-
'opacity': 0.95,
|
|
371
|
-
'z-index': 9999
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
],
|
|
375
|
-
layout: COSE_LAYOUT
|
|
447
|
+
wheelSensitivity: 0.3,
|
|
448
|
+
style: this.buildStyle(),
|
|
449
|
+
layout: { name: 'preset' } // real layout runs below, honouring settings
|
|
376
450
|
});
|
|
377
451
|
|
|
378
|
-
cy.on('tap', 'node', function (evt) {
|
|
379
|
-
var
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
window.open(url, '_blank', 'noopener');
|
|
387
|
-
} else {
|
|
388
|
-
window.location.href = url;
|
|
389
|
-
}
|
|
452
|
+
this.cy.on('tap', 'node', function (evt) {
|
|
453
|
+
var url = evt.target.data('url');
|
|
454
|
+
if (!url) return;
|
|
455
|
+
var native = evt.originalEvent;
|
|
456
|
+
if (native && (native.metaKey || native.ctrlKey)) {
|
|
457
|
+
window.open(url, '_blank', 'noopener');
|
|
458
|
+
} else {
|
|
459
|
+
window.location.href = url;
|
|
390
460
|
}
|
|
391
461
|
});
|
|
392
462
|
|
|
393
|
-
// Hover: highlight neighborhood, fade everything else.
|
|
394
|
-
cy.on('mouseover', 'node', function (evt) {
|
|
463
|
+
// Hover: highlight neighborhood, fade everything else, show the preview.
|
|
464
|
+
this.cy.on('mouseover', 'node', function (evt) {
|
|
395
465
|
var node = evt.target;
|
|
396
466
|
var nhood = node.closedNeighborhood();
|
|
397
|
-
cy.elements().not(nhood).addClass('faded');
|
|
467
|
+
app.cy.elements().not(nhood).addClass('faded');
|
|
398
468
|
nhood.addClass('highlighted');
|
|
469
|
+
app.showPreview(node);
|
|
399
470
|
});
|
|
400
|
-
cy.on('mouseout', 'node', function () {
|
|
401
|
-
cy.elements().removeClass('faded highlighted');
|
|
402
|
-
|
|
471
|
+
this.cy.on('mouseout', 'node', function () {
|
|
472
|
+
app.cy.elements().removeClass('faded highlighted');
|
|
473
|
+
app.hidePreview();
|
|
474
|
+
app.updateLabelVisibility();
|
|
475
|
+
app.applySearch(); // restore an active search highlight after hover
|
|
403
476
|
});
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
updateLabelVisibility(
|
|
477
|
+
this.cy.on('zoom pan', function () {
|
|
478
|
+
app.hidePreview();
|
|
479
|
+
app.updateLabelVisibility();
|
|
407
480
|
});
|
|
481
|
+
this.cy.on('grab', 'node', function () { app.hidePreview(); });
|
|
482
|
+
|
|
483
|
+
this.applyFilters(false);
|
|
484
|
+
this.runLayout(true);
|
|
485
|
+
this.updateLabelVisibility();
|
|
486
|
+
};
|
|
408
487
|
|
|
409
|
-
|
|
488
|
+
GraphApp.prototype.visibleElements = function () {
|
|
489
|
+
return this.cy.elements().not('.obsidian-hidden');
|
|
490
|
+
};
|
|
410
491
|
|
|
411
|
-
|
|
412
|
-
|
|
492
|
+
GraphApp.prototype.runLayout = function (randomize) {
|
|
493
|
+
if (!this.cy) return;
|
|
494
|
+
var s = this.settings;
|
|
495
|
+
if (this.activeLayout) { try { this.activeLayout.stop(); } catch (e) { /* ignore */ } }
|
|
496
|
+
this.activeLayout = this.cy.layout({
|
|
497
|
+
name: 'cose',
|
|
498
|
+
animate: s.animateLayout && !prefersReducedMotion(),
|
|
499
|
+
randomize: !!randomize,
|
|
500
|
+
nodeRepulsion: function () { return 32000 * s.repulsion; },
|
|
501
|
+
idealEdgeLength: function () { return 190 * s.linkDistance; },
|
|
502
|
+
edgeElasticity: function () { return 60; },
|
|
503
|
+
nodeOverlap: 40,
|
|
504
|
+
gravity: 0.1,
|
|
505
|
+
nestingFactor: 1.5,
|
|
506
|
+
numIter: 3000,
|
|
507
|
+
padding: 80,
|
|
508
|
+
componentSpacing: 140,
|
|
509
|
+
fit: true
|
|
510
|
+
});
|
|
511
|
+
this.activeLayout.run();
|
|
512
|
+
};
|
|
413
513
|
|
|
414
|
-
function
|
|
415
|
-
|
|
416
|
-
var
|
|
417
|
-
|
|
514
|
+
GraphApp.prototype.applyDisplay = function () {
|
|
515
|
+
if (!this.cy) return;
|
|
516
|
+
var s = this.settings;
|
|
517
|
+
this.cy.style().fromJson(this.buildStyle()).update();
|
|
518
|
+
var app = this;
|
|
519
|
+
this.cy.batch(function () {
|
|
520
|
+
app.cy.nodes().forEach(function (node) {
|
|
521
|
+
node.data('size', baseNodeSize(node.data('degree')) * s.nodeScale);
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
this.updateLabelVisibility();
|
|
525
|
+
saveSettings(s);
|
|
526
|
+
};
|
|
418
527
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
528
|
+
GraphApp.prototype.applyFilters = function (refit) {
|
|
529
|
+
if (!this.cy) return;
|
|
530
|
+
var s = this.settings;
|
|
531
|
+
var app = this;
|
|
532
|
+
this.cy.batch(function () {
|
|
533
|
+
app.cy.nodes().forEach(function (node) {
|
|
534
|
+
var d = node.data();
|
|
535
|
+
var hide = false;
|
|
536
|
+
if (d.broken) {
|
|
537
|
+
hide = !s.showBroken;
|
|
538
|
+
} else if (s.collections[d.collection || 'page'] === false) {
|
|
539
|
+
hide = true;
|
|
540
|
+
}
|
|
541
|
+
if (!hide && !s.showOrphans && (d.degree || 0) === 0) hide = true;
|
|
542
|
+
node.toggleClass('obsidian-hidden', hide);
|
|
429
543
|
});
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
544
|
+
});
|
|
545
|
+
this.updateStats();
|
|
546
|
+
if (refit !== false) this.cy.fit(this.visibleElements(), 80);
|
|
547
|
+
saveSettings(s);
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
GraphApp.prototype.updateLabelVisibility = function () {
|
|
551
|
+
if (!this.cy) return;
|
|
552
|
+
var show = this.cy.zoom() >= this.settings.labelZoom;
|
|
553
|
+
this.cy.nodes().forEach(function (node) {
|
|
554
|
+
if (node.hasClass('highlighted')) return;
|
|
555
|
+
if ((node.data('degree') || 0) >= HUB_DEGREE_THRESHOLD) return;
|
|
556
|
+
node.style('text-opacity', show ? 0.9 : 0);
|
|
557
|
+
});
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
// Search supports Obsidian-flavoured operators alongside plain text:
|
|
561
|
+
// tag:foo entries tagged foo path:docs URL contains "docs"
|
|
562
|
+
// Plain terms match title/basename/aliases; all terms must match (AND).
|
|
563
|
+
GraphApp.prototype.matchesQuery = function (node, terms) {
|
|
564
|
+
var d = node.data();
|
|
565
|
+
var label = normalize(d.label);
|
|
566
|
+
var aliases = d.aliasText || '';
|
|
567
|
+
for (var i = 0; i < terms.length; i++) {
|
|
568
|
+
var term = terms[i];
|
|
569
|
+
if (term.slice(0, 4) === 'tag:') {
|
|
570
|
+
var tag = term.slice(4).replace(/^#/, '');
|
|
571
|
+
if (!tag) continue;
|
|
572
|
+
if ((d.tags || []).indexOf(tag) === -1) return false;
|
|
573
|
+
} else if (term.slice(0, 5) === 'path:') {
|
|
574
|
+
var path = term.slice(5);
|
|
575
|
+
if (!path) continue;
|
|
576
|
+
if (String(d.url || '').toLowerCase().indexOf(path) === -1) return false;
|
|
577
|
+
} else if (label.indexOf(term) === -1 && aliases.indexOf(term) === -1) {
|
|
578
|
+
return false;
|
|
433
579
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
580
|
+
}
|
|
581
|
+
return true;
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
GraphApp.prototype.applySearch = function () {
|
|
585
|
+
if (!this.cy) return;
|
|
586
|
+
var status = document.getElementById('obsidian-graph-status');
|
|
587
|
+
var q = normalize(this.searchQuery);
|
|
588
|
+
this.cy.elements().removeClass('faded highlighted');
|
|
589
|
+
if (!q) {
|
|
590
|
+
if (status) status.textContent = '';
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
var terms = q.split(/\s+/).filter(Boolean);
|
|
594
|
+
var app = this;
|
|
595
|
+
var matches = this.visibleElements().nodes().filter(function (n) {
|
|
596
|
+
return app.matchesQuery(n, terms);
|
|
597
|
+
});
|
|
598
|
+
if (matches.length === 0) {
|
|
599
|
+
if (status) status.textContent = 'No nodes match “' + this.searchQuery + '”.';
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
var nhood = matches.closedNeighborhood().not('.obsidian-hidden');
|
|
603
|
+
this.cy.elements().not(nhood).addClass('faded');
|
|
604
|
+
matches.addClass('highlighted');
|
|
605
|
+
if (status) {
|
|
606
|
+
status.textContent = matches.length + ' node' +
|
|
438
607
|
(matches.length === 1 ? '' : 's') + ' matched.';
|
|
439
|
-
cy.fit(matches, 110);
|
|
440
608
|
}
|
|
609
|
+
this.cy.fit(matches, 110);
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
GraphApp.prototype.updateStats = function () {
|
|
613
|
+
var el = document.getElementById('obsidian-graph-stats');
|
|
614
|
+
if (!el || !this.cy) return;
|
|
615
|
+
var total = this.cy.nodes('[!broken]').length;
|
|
616
|
+
var visible = this.cy.nodes('[!broken]').not('.obsidian-hidden').length;
|
|
617
|
+
var edges = this.visibleElements().edges().length;
|
|
618
|
+
var broken = this.cy.nodes('[?broken]').length;
|
|
619
|
+
var pages = (visible === total) ?
|
|
620
|
+
total + ' pages' : visible + ' of ' + total + ' pages';
|
|
621
|
+
el.innerHTML =
|
|
622
|
+
'<span class="badge text-bg-secondary me-2">' + pages + '</span>' +
|
|
623
|
+
'<span class="badge text-bg-info me-2">' + edges + ' links</span>' +
|
|
624
|
+
(broken > 0 ?
|
|
625
|
+
'<span class="badge text-bg-danger">' + broken + ' broken</span>' :
|
|
626
|
+
'<span class="badge text-bg-success">0 broken</span>');
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
// --- Hover preview card ----------------------------------------------------
|
|
630
|
+
GraphApp.prototype.showPreview = function (node) {
|
|
631
|
+
var card = document.getElementById('obsidian-graph-preview');
|
|
632
|
+
if (!card || node.data('broken')) { this.hidePreview(); return; }
|
|
633
|
+
var d = node.data();
|
|
634
|
+
var links = d.degree === 1 ? '1 link' : (d.degree || 0) + ' links';
|
|
635
|
+
card.innerHTML =
|
|
636
|
+
'<div class="card-body p-2">' +
|
|
637
|
+
'<div class="d-flex align-items-center gap-2 mb-1">' +
|
|
638
|
+
'<span class="obsidian-graph-preview-swatch" aria-hidden="true"></span>' +
|
|
639
|
+
'<strong class="small text-truncate">' + escapeHtml(d.label) + '</strong>' +
|
|
640
|
+
'</div>' +
|
|
641
|
+
'<div class="d-flex gap-2 align-items-center small text-secondary mb-1">' +
|
|
642
|
+
'<span class="badge text-bg-light border">' + escapeHtml(d.collection) + '</span>' +
|
|
643
|
+
'<span>' + links + '</span>' +
|
|
644
|
+
'</div>' +
|
|
645
|
+
(d.excerpt ?
|
|
646
|
+
'<p class="small mb-0 obsidian-graph-preview-excerpt">' + escapeHtml(d.excerpt) + '</p>' : '') +
|
|
647
|
+
'</div>';
|
|
648
|
+
var swatch = card.querySelector('.obsidian-graph-preview-swatch');
|
|
649
|
+
if (swatch) swatch.style.backgroundColor = d.color;
|
|
650
|
+
card.hidden = false;
|
|
651
|
+
// Position near the node, clamped inside the shell.
|
|
652
|
+
var pos = node.renderedPosition();
|
|
653
|
+
var shell = card.parentNode;
|
|
654
|
+
var maxX = shell.clientWidth - card.offsetWidth - 8;
|
|
655
|
+
var maxY = shell.clientHeight - card.offsetHeight - 8;
|
|
656
|
+
card.style.left = Math.max(8, Math.min(pos.x + 16, maxX)) + 'px';
|
|
657
|
+
card.style.top = Math.max(8, Math.min(pos.y + 16, maxY)) + 'px';
|
|
658
|
+
};
|
|
441
659
|
|
|
442
|
-
|
|
660
|
+
GraphApp.prototype.hidePreview = function () {
|
|
661
|
+
var card = document.getElementById('obsidian-graph-preview');
|
|
662
|
+
if (card) card.hidden = true;
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
// --- Theme reactivity ------------------------------------------------------
|
|
666
|
+
GraphApp.prototype.watchTheme = function () {
|
|
667
|
+
var app = this;
|
|
668
|
+
var restyle = debounce(function () { app.applyDisplay(); }, 50);
|
|
669
|
+
var observer = new MutationObserver(restyle);
|
|
670
|
+
[document.documentElement, document.body].forEach(function (el) {
|
|
671
|
+
observer.observe(el, { attributes: true, attributeFilter: ['data-bs-theme'] });
|
|
672
|
+
});
|
|
673
|
+
if (window.matchMedia) {
|
|
674
|
+
var mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
675
|
+
if (mq.addEventListener) mq.addEventListener('change', restyle);
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
// ---------------------------------------------------------------------------
|
|
680
|
+
// Control panel wiring
|
|
681
|
+
// ---------------------------------------------------------------------------
|
|
682
|
+
function bindCheckbox(id, value, onChange) {
|
|
683
|
+
var input = document.getElementById(id);
|
|
684
|
+
if (!input) return;
|
|
685
|
+
input.checked = !!value;
|
|
686
|
+
input.addEventListener('change', function () { onChange(input.checked); });
|
|
443
687
|
}
|
|
444
688
|
|
|
445
|
-
function
|
|
446
|
-
var
|
|
447
|
-
if (!
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
var
|
|
451
|
-
if (
|
|
452
|
-
var status = document.getElementById('obsidian-graph-status');
|
|
453
|
-
if (status) status.textContent = '';
|
|
454
|
-
cy.fit(undefined, 80);
|
|
689
|
+
function bindRange(id, value, onInput) {
|
|
690
|
+
var input = document.getElementById(id);
|
|
691
|
+
if (!input) return;
|
|
692
|
+
input.value = value;
|
|
693
|
+
input.addEventListener('input', function () {
|
|
694
|
+
var n = parseFloat(input.value);
|
|
695
|
+
if (isFinite(n)) onInput(n);
|
|
455
696
|
});
|
|
456
697
|
}
|
|
457
698
|
|
|
458
|
-
function
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
699
|
+
function wireCollectionFilters(app) {
|
|
700
|
+
var host = document.getElementById('obsidian-graph-collections');
|
|
701
|
+
if (!host) return;
|
|
702
|
+
var counts = Object.create(null);
|
|
703
|
+
app.entries.forEach(function (entry) {
|
|
704
|
+
var name = entry.collection || 'page';
|
|
705
|
+
counts[name] = (counts[name] || 0) + 1;
|
|
706
|
+
});
|
|
707
|
+
var names = Object.keys(counts).sort(function (a, b) {
|
|
708
|
+
return counts[b] - counts[a] || a.localeCompare(b);
|
|
709
|
+
});
|
|
710
|
+
host.innerHTML = '';
|
|
711
|
+
names.forEach(function (name) {
|
|
712
|
+
var id = 'obsidian-graph-col-' + name.replace(/[^a-z0-9-]/gi, '-');
|
|
713
|
+
var item = document.createElement('div');
|
|
714
|
+
item.className = 'form-check form-switch obsidian-graph-collection-filter';
|
|
715
|
+
item.innerHTML =
|
|
716
|
+
'<input class="form-check-input" type="checkbox" role="switch" id="' + id + '">' +
|
|
717
|
+
'<label class="form-check-label d-flex align-items-center gap-2" for="' + id + '">' +
|
|
718
|
+
'<span class="swatch" aria-hidden="true"></span>' +
|
|
719
|
+
'<span class="flex-grow-1 text-capitalize">' + escapeHtml(name) + '</span>' +
|
|
720
|
+
'<span class="badge text-bg-secondary">' + counts[name] + '</span>' +
|
|
721
|
+
'</label>';
|
|
722
|
+
var input = item.querySelector('input');
|
|
723
|
+
input.checked = app.settings.collections[name] !== false;
|
|
724
|
+
input.addEventListener('change', function () {
|
|
725
|
+
app.settings.collections[name] = input.checked;
|
|
726
|
+
app.applyFilters();
|
|
727
|
+
app.applySearch();
|
|
728
|
+
});
|
|
729
|
+
item.querySelector('.swatch').style.backgroundColor = collectionColor(name, 0.92);
|
|
730
|
+
host.appendChild(item);
|
|
731
|
+
});
|
|
465
732
|
}
|
|
466
733
|
|
|
467
|
-
function
|
|
468
|
-
var
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
734
|
+
function wireControls(app) {
|
|
735
|
+
var s = app.settings;
|
|
736
|
+
|
|
737
|
+
var search = document.getElementById('obsidian-graph-search');
|
|
738
|
+
if (search) {
|
|
739
|
+
search.addEventListener('input', debounce(function () {
|
|
740
|
+
app.searchQuery = search.value;
|
|
741
|
+
app.applySearch();
|
|
742
|
+
}, 150));
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
wireCollectionFilters(app);
|
|
746
|
+
|
|
747
|
+
bindCheckbox('obsidian-graph-orphans', s.showOrphans, function (checked) {
|
|
748
|
+
s.showOrphans = checked;
|
|
749
|
+
app.applyFilters();
|
|
750
|
+
});
|
|
751
|
+
bindCheckbox('obsidian-graph-broken', s.showBroken, function (checked) {
|
|
752
|
+
s.showBroken = checked;
|
|
753
|
+
app.applyFilters();
|
|
754
|
+
});
|
|
755
|
+
bindCheckbox('obsidian-graph-arrows', s.showArrows, function (checked) {
|
|
756
|
+
s.showArrows = checked;
|
|
757
|
+
app.applyDisplay();
|
|
758
|
+
});
|
|
759
|
+
bindCheckbox('obsidian-graph-animate', s.animateLayout, function (checked) {
|
|
760
|
+
s.animateLayout = checked;
|
|
761
|
+
saveSettings(s);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
bindRange('obsidian-graph-node-size', s.nodeScale, debounce(function (n) {
|
|
765
|
+
s.nodeScale = clamp(n, 0.5, 2, 1);
|
|
766
|
+
app.applyDisplay();
|
|
767
|
+
}, 80));
|
|
768
|
+
bindRange('obsidian-graph-link-width', s.linkScale, debounce(function (n) {
|
|
769
|
+
s.linkScale = clamp(n, 0.5, 3, 1);
|
|
770
|
+
app.applyDisplay();
|
|
771
|
+
}, 80));
|
|
772
|
+
bindRange('obsidian-graph-label-zoom', s.labelZoom, debounce(function (n) {
|
|
773
|
+
s.labelZoom = clamp(n, 0.4, 2.5, 1.25);
|
|
774
|
+
app.applyDisplay();
|
|
775
|
+
}, 80));
|
|
776
|
+
|
|
777
|
+
var relayout = debounce(function () { app.runLayout(false); }, 350);
|
|
778
|
+
bindRange('obsidian-graph-repulsion', s.repulsion, function (n) {
|
|
779
|
+
s.repulsion = clamp(n, 0.3, 3, 1);
|
|
780
|
+
saveSettings(s);
|
|
781
|
+
relayout();
|
|
474
782
|
});
|
|
783
|
+
bindRange('obsidian-graph-link-distance', s.linkDistance, function (n) {
|
|
784
|
+
s.linkDistance = clamp(n, 0.4, 2.5, 1);
|
|
785
|
+
saveSettings(s);
|
|
786
|
+
relayout();
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
var rerun = document.getElementById('obsidian-graph-relayout');
|
|
790
|
+
if (rerun) {
|
|
791
|
+
rerun.addEventListener('click', function () { app.runLayout(true); });
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
var reset = document.getElementById('obsidian-graph-reset');
|
|
795
|
+
if (reset) {
|
|
796
|
+
reset.addEventListener('click', function () {
|
|
797
|
+
app.settings = JSON.parse(JSON.stringify(DEFAULTS));
|
|
798
|
+
s = app.settings;
|
|
799
|
+
try { window.localStorage.removeItem(STORAGE_KEY); } catch (e) { /* ignore */ }
|
|
800
|
+
// Reflect defaults back into every control, then re-apply the view.
|
|
801
|
+
syncControls(app);
|
|
802
|
+
wireCollectionFilters(app);
|
|
803
|
+
app.applyDisplay();
|
|
804
|
+
app.applyFilters();
|
|
805
|
+
window.ObsidianGraph.settings = app.settings;
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
var fit = document.getElementById('obsidian-graph-fit');
|
|
810
|
+
if (fit) {
|
|
811
|
+
fit.addEventListener('click', function () {
|
|
812
|
+
app.cy.elements().removeClass('faded highlighted');
|
|
813
|
+
if (search) search.value = '';
|
|
814
|
+
app.searchQuery = '';
|
|
815
|
+
var status = document.getElementById('obsidian-graph-status');
|
|
816
|
+
if (status) status.textContent = '';
|
|
817
|
+
app.cy.fit(app.visibleElements(), 80);
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function zoomBy(factor) {
|
|
822
|
+
if (!app.cy) return;
|
|
823
|
+
app.cy.animate({
|
|
824
|
+
zoom: {
|
|
825
|
+
level: app.cy.zoom() * factor,
|
|
826
|
+
renderedPosition: { x: app.container.clientWidth / 2, y: app.container.clientHeight / 2 }
|
|
827
|
+
},
|
|
828
|
+
duration: prefersReducedMotion() ? 0 : 120
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
var zoomIn = document.getElementById('obsidian-graph-zoom-in');
|
|
832
|
+
if (zoomIn) zoomIn.addEventListener('click', function () { zoomBy(1.35); });
|
|
833
|
+
var zoomOut = document.getElementById('obsidian-graph-zoom-out');
|
|
834
|
+
if (zoomOut) zoomOut.addEventListener('click', function () { zoomBy(1 / 1.35); });
|
|
835
|
+
|
|
836
|
+
wireFullscreen(app);
|
|
475
837
|
}
|
|
476
838
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
var
|
|
480
|
-
var
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
'
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
839
|
+
// Reflect app.settings into the checkbox/range inputs (used by Reset).
|
|
840
|
+
function syncControls(app) {
|
|
841
|
+
var s = app.settings;
|
|
842
|
+
var map = {
|
|
843
|
+
'obsidian-graph-orphans': s.showOrphans,
|
|
844
|
+
'obsidian-graph-broken': s.showBroken,
|
|
845
|
+
'obsidian-graph-arrows': s.showArrows,
|
|
846
|
+
'obsidian-graph-animate': s.animateLayout
|
|
847
|
+
};
|
|
848
|
+
Object.keys(map).forEach(function (id) {
|
|
849
|
+
var el = document.getElementById(id);
|
|
850
|
+
if (el) el.checked = !!map[id];
|
|
851
|
+
});
|
|
852
|
+
var ranges = {
|
|
853
|
+
'obsidian-graph-node-size': s.nodeScale,
|
|
854
|
+
'obsidian-graph-link-width': s.linkScale,
|
|
855
|
+
'obsidian-graph-label-zoom': s.labelZoom,
|
|
856
|
+
'obsidian-graph-repulsion': s.repulsion,
|
|
857
|
+
'obsidian-graph-link-distance': s.linkDistance
|
|
858
|
+
};
|
|
859
|
+
Object.keys(ranges).forEach(function (id) {
|
|
860
|
+
var el = document.getElementById(id);
|
|
861
|
+
if (el) el.value = ranges[id];
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function wireFullscreen(app) {
|
|
866
|
+
var btn = document.getElementById('obsidian-graph-fullscreen');
|
|
867
|
+
var shell = document.getElementById('obsidian-graph-shell');
|
|
868
|
+
if (!btn || !shell) return;
|
|
869
|
+
|
|
870
|
+
function isFullscreen() {
|
|
871
|
+
return document.fullscreenElement === shell ||
|
|
872
|
+
shell.classList.contains('obsidian-graph-shell-expanded');
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function refresh() {
|
|
876
|
+
var active = isFullscreen();
|
|
877
|
+
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
|
|
878
|
+
var icon = btn.querySelector('.bi');
|
|
879
|
+
if (icon) {
|
|
880
|
+
icon.className = active ?
|
|
881
|
+
'bi bi-fullscreen-exit' : 'bi bi-arrows-fullscreen';
|
|
882
|
+
}
|
|
883
|
+
if (app.cy) {
|
|
884
|
+
app.cy.resize();
|
|
885
|
+
app.cy.fit(app.visibleElements(), 80);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
btn.addEventListener('click', function () {
|
|
890
|
+
if (isFullscreen()) {
|
|
891
|
+
if (document.fullscreenElement === shell && document.exitFullscreen) {
|
|
892
|
+
document.exitFullscreen();
|
|
893
|
+
}
|
|
894
|
+
shell.classList.remove('obsidian-graph-shell-expanded');
|
|
895
|
+
refresh();
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if (shell.requestFullscreen) {
|
|
899
|
+
shell.requestFullscreen().catch(function () {
|
|
900
|
+
// Fullscreen API blocked (iframe / permission) — CSS overlay fallback.
|
|
901
|
+
shell.classList.add('obsidian-graph-shell-expanded');
|
|
902
|
+
refresh();
|
|
903
|
+
});
|
|
904
|
+
} else {
|
|
905
|
+
shell.classList.add('obsidian-graph-shell-expanded');
|
|
906
|
+
refresh();
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
document.addEventListener('fullscreenchange', refresh);
|
|
910
|
+
document.addEventListener('keydown', function (evt) {
|
|
911
|
+
if (evt.key === 'Escape' && shell.classList.contains('obsidian-graph-shell-expanded')) {
|
|
912
|
+
shell.classList.remove('obsidian-graph-shell-expanded');
|
|
913
|
+
refresh();
|
|
914
|
+
}
|
|
915
|
+
});
|
|
489
916
|
}
|
|
490
917
|
|
|
918
|
+
// ---------------------------------------------------------------------------
|
|
491
919
|
function init() {
|
|
492
920
|
var container = document.getElementById(CONTAINER_ID);
|
|
493
921
|
if (!container) return;
|
|
@@ -505,8 +933,6 @@
|
|
|
505
933
|
.then(function (payload) {
|
|
506
934
|
var entries = (payload && payload.entries) || [];
|
|
507
935
|
var byKey = buildLookup(entries);
|
|
508
|
-
var elements = buildElements(entries, byKey);
|
|
509
|
-
computeNodeDegree(elements);
|
|
510
936
|
loadCytoscape(function (ok) {
|
|
511
937
|
if (!ok) {
|
|
512
938
|
container.innerHTML =
|
|
@@ -517,12 +943,24 @@
|
|
|
517
943
|
return;
|
|
518
944
|
}
|
|
519
945
|
container.innerHTML = '';
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
946
|
+
var app = new GraphApp(container, entries, byKey);
|
|
947
|
+
app.elements = buildElements(entries, byKey);
|
|
948
|
+
computeNodeDegree(app.elements);
|
|
949
|
+
// Apply the persisted node-size multiplier before first paint.
|
|
950
|
+
app.elements.forEach(function (el) {
|
|
951
|
+
if (el.group === 'nodes') el.data.size *= app.settings.nodeScale;
|
|
952
|
+
});
|
|
953
|
+
app.render();
|
|
954
|
+
app.updateStats();
|
|
955
|
+
wireControls(app);
|
|
956
|
+
app.watchTheme();
|
|
957
|
+
window.ObsidianGraph = {
|
|
958
|
+
cy: app.cy,
|
|
959
|
+
byKey: byKey,
|
|
960
|
+
entries: entries,
|
|
961
|
+
settings: app.settings,
|
|
962
|
+
app: app
|
|
963
|
+
};
|
|
526
964
|
});
|
|
527
965
|
})
|
|
528
966
|
.catch(function (err) {
|