pict-section-inlinedocumentation 0.0.1

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.
Files changed (51) hide show
  1. package/README.md +107 -0
  2. package/docs/.nojekyll +0 -0
  3. package/docs/README.md +83 -0
  4. package/docs/_cover.md +15 -0
  5. package/docs/_sidebar.md +24 -0
  6. package/docs/_topbar.md +8 -0
  7. package/docs/_version.json +7 -0
  8. package/docs/api-reference.md +185 -0
  9. package/docs/architecture.md +103 -0
  10. package/docs/css/docuserve.css +327 -0
  11. package/docs/embedding-level1-sidebar.md +92 -0
  12. package/docs/embedding-level2-routes.md +86 -0
  13. package/docs/embedding-level3-tooltips.md +97 -0
  14. package/docs/embedding-level4-autogen.md +126 -0
  15. package/docs/index.html +39 -0
  16. package/docs/overview.md +42 -0
  17. package/docs/quickstart.md +95 -0
  18. package/docs/reference.md +73 -0
  19. package/docs/retold-catalog.json +181 -0
  20. package/docs/retold-keyword-index.json +4374 -0
  21. package/example_applications/basic/docs/README.md +40 -0
  22. package/example_applications/basic/docs/_sidebar.md +4 -0
  23. package/example_applications/basic/docs/_topics.json +10 -0
  24. package/example_applications/basic/docs/advanced-topics.md +47 -0
  25. package/example_applications/basic/docs/getting-started.md +70 -0
  26. package/example_applications/basic/index.html +100 -0
  27. package/example_applications/bookshop/.quackage.json +10 -0
  28. package/example_applications/bookshop/Pict-Application-Bookshop-Configuration.json +15 -0
  29. package/example_applications/bookshop/Pict-Application-Bookshop.js +218 -0
  30. package/example_applications/bookshop/data/BookshopData.json +65 -0
  31. package/example_applications/bookshop/data/pict_documentation_topics.json +46 -0
  32. package/example_applications/bookshop/docs/_sidebar.md +6 -0
  33. package/example_applications/bookshop/docs/book-detail.md +21 -0
  34. package/example_applications/bookshop/docs/book-list.md +21 -0
  35. package/example_applications/bookshop/docs/search-filter.md +18 -0
  36. package/example_applications/bookshop/docs/store.md +29 -0
  37. package/example_applications/bookshop/docs/welcome.md +23 -0
  38. package/example_applications/bookshop/html/index.html +236 -0
  39. package/example_applications/bookshop/package.json +34 -0
  40. package/example_applications/bookshop/views/PictView-Bookshop-BookList.js +324 -0
  41. package/example_applications/bookshop/views/PictView-Bookshop-HelpToggle.js +44 -0
  42. package/example_applications/bookshop/views/PictView-Bookshop-Store.js +271 -0
  43. package/package.json +55 -0
  44. package/source/Pict-Section-InlineDocumentation.js +10 -0
  45. package/source/providers/Pict-Provider-InlineDocumentation.js +1995 -0
  46. package/source/views/Pict-View-InlineDocumentation-Content.js +542 -0
  47. package/source/views/Pict-View-InlineDocumentation-Layout.js +206 -0
  48. package/source/views/Pict-View-InlineDocumentation-Nav.js +475 -0
  49. package/source/views/Pict-View-InlineDocumentation-TopicManager.js +1623 -0
  50. package/test/Browser_Integration_tests.js +1449 -0
  51. package/test/Pict-Section-InlineDocumentation_test.js +1285 -0
@@ -0,0 +1,1623 @@
1
+ const libPictView = require('pict-view');
2
+
3
+ const _ViewConfiguration =
4
+ {
5
+ ViewIdentifier: "InlineDoc-TopicManager",
6
+
7
+ AutoRender: false,
8
+
9
+ CSS: /*css*/`
10
+ .pict-inline-doc-tm-topic-list {
11
+ max-height: 400px;
12
+ overflow-y: auto;
13
+ margin: 0 -0.5em;
14
+ }
15
+ .pict-inline-doc-tm-topic-item {
16
+ display: flex;
17
+ align-items: center;
18
+ padding: 0.6em 0.8em;
19
+ border-bottom: 1px solid #EAE3D8;
20
+ cursor: pointer;
21
+ transition: background 0.1s;
22
+ }
23
+ .pict-inline-doc-tm-topic-item:hover {
24
+ background: #F5F0E8;
25
+ }
26
+ .pict-inline-doc-tm-topic-item:last-child {
27
+ border-bottom: none;
28
+ }
29
+ .pict-inline-doc-tm-topic-info {
30
+ flex: 1;
31
+ min-width: 0;
32
+ }
33
+ .pict-inline-doc-tm-topic-title {
34
+ font-weight: 600;
35
+ color: #3D3229;
36
+ font-size: 0.95em;
37
+ }
38
+ .pict-inline-doc-tm-topic-meta {
39
+ font-size: 0.8em;
40
+ color: #8A7F72;
41
+ margin-top: 0.15em;
42
+ }
43
+ .pict-inline-doc-tm-topic-actions {
44
+ display: flex;
45
+ gap: 0.3em;
46
+ flex-shrink: 0;
47
+ }
48
+ .pict-inline-doc-tm-action-btn {
49
+ display: inline-flex;
50
+ align-items: center;
51
+ justify-content: center;
52
+ width: 28px;
53
+ height: 28px;
54
+ border: 1px solid #DDD6CA;
55
+ border-radius: 3px;
56
+ background: #fff;
57
+ color: #5E5549;
58
+ font-size: 0.85em;
59
+ cursor: pointer;
60
+ transition: background 0.1s, border-color 0.1s;
61
+ }
62
+ .pict-inline-doc-tm-action-btn:hover {
63
+ background: #F0ECE4;
64
+ border-color: #C4BDB3;
65
+ }
66
+ .pict-inline-doc-tm-action-btn.danger:hover {
67
+ background: #FDE8E8;
68
+ border-color: #E57373;
69
+ color: #C62828;
70
+ }
71
+ .pict-inline-doc-tm-new-topic {
72
+ display: flex;
73
+ align-items: center;
74
+ justify-content: center;
75
+ padding: 0.7em;
76
+ margin-top: 0.5em;
77
+ border: 1px dashed #DDD6CA;
78
+ border-radius: 4px;
79
+ color: #2E7D74;
80
+ font-size: 0.9em;
81
+ font-weight: 500;
82
+ cursor: pointer;
83
+ transition: background 0.1s, border-color 0.1s;
84
+ }
85
+ .pict-inline-doc-tm-new-topic:hover {
86
+ background: #F0F9F7;
87
+ border-color: #2E7D74;
88
+ }
89
+ .pict-inline-doc-tm-empty {
90
+ text-align: center;
91
+ padding: 2em 1em;
92
+ color: #8A7F72;
93
+ font-size: 0.9em;
94
+ }
95
+ .pict-inline-doc-tm-form-group {
96
+ margin-bottom: 0.8em;
97
+ }
98
+ .pict-inline-doc-tm-form-label {
99
+ display: block;
100
+ font-size: 0.8em;
101
+ font-weight: 600;
102
+ color: #5E5549;
103
+ text-transform: uppercase;
104
+ letter-spacing: 0.03em;
105
+ margin-bottom: 0.3em;
106
+ }
107
+ .pict-inline-doc-tm-form-input {
108
+ width: 100%;
109
+ padding: 0.45em 0.6em;
110
+ font-size: 0.9em;
111
+ color: #3D3229;
112
+ background: #FDFCFA;
113
+ border: 1px solid #DDD6CA;
114
+ border-radius: 4px;
115
+ box-sizing: border-box;
116
+ }
117
+ .pict-inline-doc-tm-form-input:focus {
118
+ outline: none;
119
+ border-color: #2E7D74;
120
+ box-shadow: 0 0 0 2px rgba(46, 125, 116, 0.15);
121
+ }
122
+ .pict-inline-doc-tm-form-input[readonly] {
123
+ background: #F5F0E8;
124
+ color: #8A7F72;
125
+ }
126
+ .pict-inline-doc-tm-form-hint {
127
+ font-size: 0.75em;
128
+ color: #8A7F72;
129
+ margin-top: 0.2em;
130
+ }
131
+ .pict-inline-doc-tm-routes-section {
132
+ margin-top: 0.5em;
133
+ }
134
+ .pict-inline-doc-tm-route-chips {
135
+ display: flex;
136
+ flex-wrap: wrap;
137
+ gap: 0.3em;
138
+ margin-bottom: 0.5em;
139
+ }
140
+ .pict-inline-doc-tm-route-chip {
141
+ display: inline-flex;
142
+ align-items: center;
143
+ padding: 0.2em 0.5em;
144
+ background: #E8E3D8;
145
+ border-radius: 12px;
146
+ font-size: 0.82em;
147
+ color: #3D3229;
148
+ font-family: 'SFMono-Regular', 'SF Mono', 'Menlo', 'Consolas', monospace;
149
+ }
150
+ .pict-inline-doc-tm-route-chip-remove {
151
+ margin-left: 0.4em;
152
+ cursor: pointer;
153
+ color: #8A7F72;
154
+ font-size: 0.9em;
155
+ line-height: 1;
156
+ }
157
+ .pict-inline-doc-tm-route-chip-remove:hover {
158
+ color: #C62828;
159
+ }
160
+ .pict-inline-doc-tm-route-actions {
161
+ display: flex;
162
+ flex-wrap: wrap;
163
+ gap: 0.3em;
164
+ }
165
+ .pict-inline-doc-tm-route-action-btn {
166
+ display: inline-flex;
167
+ align-items: center;
168
+ padding: 0.25em 0.5em;
169
+ border: 1px solid #DDD6CA;
170
+ border-radius: 3px;
171
+ background: #fff;
172
+ color: #5E5549;
173
+ font-size: 0.8em;
174
+ cursor: pointer;
175
+ transition: background 0.1s;
176
+ }
177
+ .pict-inline-doc-tm-route-action-btn:hover {
178
+ background: #F0ECE4;
179
+ }
180
+ .pict-inline-doc-tm-route-action-btn.accent {
181
+ border-color: #2E7D74;
182
+ color: #2E7D74;
183
+ }
184
+ .pict-inline-doc-tm-route-action-btn.accent:hover {
185
+ background: #F0F9F7;
186
+ }
187
+ .pict-inline-doc-tm-route-input-row {
188
+ display: none;
189
+ align-items: center;
190
+ gap: 0.3em;
191
+ margin-top: 0.4em;
192
+ }
193
+ .pict-inline-doc-tm-route-input-row.visible {
194
+ display: flex;
195
+ }
196
+ .pict-inline-doc-tm-route-input {
197
+ flex: 1;
198
+ padding: 0.35em 0.5em;
199
+ font-size: 0.85em;
200
+ font-family: 'SFMono-Regular', 'SF Mono', 'Menlo', 'Consolas', monospace;
201
+ color: #3D3229;
202
+ background: #FDFCFA;
203
+ border: 1px solid #DDD6CA;
204
+ border-radius: 3px;
205
+ }
206
+ .pict-inline-doc-tm-route-input:focus {
207
+ outline: none;
208
+ border-color: #2E7D74;
209
+ }
210
+ .pict-inline-doc-tm-wc-container {
211
+ padding: 0.5em 0;
212
+ }
213
+ .pict-inline-doc-tm-wc-label {
214
+ font-size: 0.85em;
215
+ color: #5E5549;
216
+ margin-bottom: 0.6em;
217
+ }
218
+ .pict-inline-doc-tm-wc-segments {
219
+ display: flex;
220
+ flex-wrap: wrap;
221
+ align-items: center;
222
+ gap: 0.15em;
223
+ margin-bottom: 0.8em;
224
+ }
225
+ .pict-inline-doc-tm-wc-slash {
226
+ color: #8A7F72;
227
+ font-size: 1.1em;
228
+ font-weight: 300;
229
+ font-family: 'SFMono-Regular', 'SF Mono', 'Menlo', 'Consolas', monospace;
230
+ }
231
+ .pict-inline-doc-tm-wc-segment {
232
+ display: inline-flex;
233
+ align-items: center;
234
+ padding: 0.4em 0.7em;
235
+ border: 1px solid #DDD6CA;
236
+ border-radius: 4px;
237
+ background: #fff;
238
+ color: #3D3229;
239
+ font-family: 'SFMono-Regular', 'SF Mono', 'Menlo', 'Consolas', monospace;
240
+ font-size: 0.9em;
241
+ cursor: pointer;
242
+ transition: background 0.15s, border-color 0.15s, opacity 0.15s;
243
+ user-select: none;
244
+ }
245
+ .pict-inline-doc-tm-wc-segment:hover {
246
+ border-color: #2E7D74;
247
+ background: #F0F9F7;
248
+ }
249
+ .pict-inline-doc-tm-wc-segment.selected {
250
+ background: #2E7D74;
251
+ color: #fff;
252
+ border-color: #2E7D74;
253
+ }
254
+ .pict-inline-doc-tm-wc-segment.after-wildcard {
255
+ opacity: 0.35;
256
+ border-style: dashed;
257
+ cursor: default;
258
+ }
259
+ .pict-inline-doc-tm-wc-segment.after-wildcard:hover {
260
+ border-color: #DDD6CA;
261
+ background: #fff;
262
+ }
263
+ .pict-inline-doc-tm-wc-wildcard-star {
264
+ display: inline-flex;
265
+ align-items: center;
266
+ padding: 0.4em 0.6em;
267
+ color: #2E7D74;
268
+ font-family: 'SFMono-Regular', 'SF Mono', 'Menlo', 'Consolas', monospace;
269
+ font-size: 1.1em;
270
+ font-weight: 700;
271
+ }
272
+ .pict-inline-doc-tm-wc-preview {
273
+ padding: 0.5em 0.7em;
274
+ background: #F5F0E8;
275
+ border: 1px solid #E5DED4;
276
+ border-radius: 4px;
277
+ font-family: 'SFMono-Regular', 'SF Mono', 'Menlo', 'Consolas', monospace;
278
+ font-size: 0.9em;
279
+ color: #3D3229;
280
+ }
281
+ .pict-inline-doc-tm-wc-preview-label {
282
+ font-size: 0.75em;
283
+ color: #8A7F72;
284
+ text-transform: uppercase;
285
+ letter-spacing: 0.03em;
286
+ margin-bottom: 0.3em;
287
+ }
288
+ .pict-inline-doc-tm-bind-route-display {
289
+ padding: 0.5em 0.7em;
290
+ background: #F5F0E8;
291
+ border: 1px solid #E5DED4;
292
+ border-radius: 4px;
293
+ font-family: 'SFMono-Regular', 'SF Mono', 'Menlo', 'Consolas', monospace;
294
+ font-size: 0.9em;
295
+ color: #3D3229;
296
+ margin-bottom: 0.8em;
297
+ }
298
+ .pict-inline-doc-tm-bind-topic-list {
299
+ max-height: 250px;
300
+ overflow-y: auto;
301
+ margin-bottom: 0.6em;
302
+ }
303
+ .pict-inline-doc-tm-bind-topic-option {
304
+ display: flex;
305
+ align-items: center;
306
+ padding: 0.5em 0.6em;
307
+ border-bottom: 1px solid #EAE3D8;
308
+ cursor: pointer;
309
+ transition: background 0.1s;
310
+ }
311
+ .pict-inline-doc-tm-bind-topic-option:hover {
312
+ background: #F5F0E8;
313
+ }
314
+ .pict-inline-doc-tm-bind-topic-option.selected {
315
+ background: #F0F9F7;
316
+ }
317
+ .pict-inline-doc-tm-bind-radio {
318
+ width: 16px;
319
+ height: 16px;
320
+ border: 2px solid #DDD6CA;
321
+ border-radius: 50%;
322
+ margin-right: 0.6em;
323
+ flex-shrink: 0;
324
+ position: relative;
325
+ }
326
+ .pict-inline-doc-tm-bind-topic-option.selected .pict-inline-doc-tm-bind-radio {
327
+ border-color: #2E7D74;
328
+ }
329
+ .pict-inline-doc-tm-bind-topic-option.selected .pict-inline-doc-tm-bind-radio::after {
330
+ content: '';
331
+ position: absolute;
332
+ top: 3px;
333
+ left: 3px;
334
+ width: 6px;
335
+ height: 6px;
336
+ background: #2E7D74;
337
+ border-radius: 50%;
338
+ }
339
+ .pict-inline-doc-tm-bind-route-type {
340
+ display: flex;
341
+ gap: 0.5em;
342
+ margin-bottom: 0.5em;
343
+ }
344
+ .pict-inline-doc-tm-bind-route-type-btn {
345
+ flex: 1;
346
+ padding: 0.5em;
347
+ border: 1px solid #DDD6CA;
348
+ border-radius: 4px;
349
+ background: #fff;
350
+ color: #5E5549;
351
+ font-size: 0.85em;
352
+ text-align: center;
353
+ cursor: pointer;
354
+ transition: background 0.1s, border-color 0.1s;
355
+ }
356
+ .pict-inline-doc-tm-bind-route-type-btn:hover {
357
+ background: #F0ECE4;
358
+ }
359
+ .pict-inline-doc-tm-bind-route-type-btn.selected {
360
+ background: #2E7D74;
361
+ color: #fff;
362
+ border-color: #2E7D74;
363
+ }
364
+ .pict-inline-doc-tm-sidebar-list {
365
+ max-height: 300px;
366
+ overflow-y: auto;
367
+ }
368
+ .pict-inline-doc-tm-sidebar-item {
369
+ padding: 0.4em 0.6em;
370
+ cursor: pointer;
371
+ font-size: 0.9em;
372
+ color: #3D3229;
373
+ border-bottom: 1px solid #EAE3D8;
374
+ transition: background 0.1s;
375
+ }
376
+ .pict-inline-doc-tm-sidebar-item:hover {
377
+ background: #F5F0E8;
378
+ }
379
+ .pict-inline-doc-tm-sidebar-item .path {
380
+ font-family: 'SFMono-Regular', 'SF Mono', 'Menlo', 'Consolas', monospace;
381
+ font-size: 0.85em;
382
+ color: #8A7F72;
383
+ margin-left: 0.5em;
384
+ }
385
+ .pict-inline-doc-tm-validation-error {
386
+ color: #C62828;
387
+ font-size: 0.8em;
388
+ margin-top: 0.3em;
389
+ }
390
+ `,
391
+
392
+ Templates: [],
393
+ Renderables: []
394
+ };
395
+
396
+ class InlineDocumentationTopicManagerView extends libPictView
397
+ {
398
+ constructor(pFable, pOptions, pServiceHash)
399
+ {
400
+ super(pFable, pOptions, pServiceHash);
401
+ }
402
+
403
+ /**
404
+ * Get the modal view instance if available.
405
+ *
406
+ * @returns {Object|null} The PictSectionModal view, or null
407
+ */
408
+ _getModal()
409
+ {
410
+ return this.pict.views['PictSectionModal'] || this.pict.views['Pict-Section-Modal'] || null;
411
+ }
412
+
413
+ /**
414
+ * Get the inline documentation provider.
415
+ *
416
+ * @returns {Object|null} The provider instance
417
+ */
418
+ _getProvider()
419
+ {
420
+ return this.pict.providers['Pict-InlineDocumentation'] || null;
421
+ }
422
+
423
+ // -- Topic List --
424
+
425
+ /**
426
+ * Show the topic manager modal with the full topic list.
427
+ */
428
+ showTopicManager()
429
+ {
430
+ let tmpModal = this._getModal();
431
+ let tmpProvider = this._getProvider();
432
+
433
+ if (!tmpModal)
434
+ {
435
+ this.log.warn('InlineDocumentation TopicManager: Pict-Section-Modal view is not registered.');
436
+ return;
437
+ }
438
+
439
+ if (!tmpProvider)
440
+ {
441
+ return;
442
+ }
443
+
444
+ let tmpTopics = tmpProvider.getTopicList();
445
+ let tmpContent = this._buildTopicListHTML(tmpTopics);
446
+
447
+ tmpModal.show(
448
+ {
449
+ title: 'Manage Topics',
450
+ content: tmpContent,
451
+ closeable: true,
452
+ width: '520px',
453
+ buttons:
454
+ [
455
+ { Hash: 'close', Label: 'Close', Style: 'primary' }
456
+ ],
457
+ onOpen: (pDialog) =>
458
+ {
459
+ this._wireTopicListHandlers(pDialog);
460
+ }
461
+ });
462
+ }
463
+
464
+ /**
465
+ * Build the HTML for the topic list modal.
466
+ *
467
+ * @param {Array} pTopics - Array from getTopicList()
468
+ * @returns {string} HTML content
469
+ */
470
+ _buildTopicListHTML(pTopics)
471
+ {
472
+ if (!pTopics || pTopics.length < 1)
473
+ {
474
+ return '<div class="pict-inline-doc-tm-empty">No topics defined yet.</div>'
475
+ + '<div class="pict-inline-doc-tm-new-topic" data-action="new-topic">+ New Topic</div>';
476
+ }
477
+
478
+ let tmpHTML = '<div class="pict-inline-doc-tm-topic-list">';
479
+
480
+ for (let i = 0; i < pTopics.length; i++)
481
+ {
482
+ let tmpTopic = pTopics[i];
483
+ tmpHTML += '<div class="pict-inline-doc-tm-topic-item" data-topic-code="' + this._escapeHTML(tmpTopic.TopicCode) + '">';
484
+ tmpHTML += '<div class="pict-inline-doc-tm-topic-info">';
485
+ tmpHTML += '<div class="pict-inline-doc-tm-topic-title">' + this._escapeHTML(tmpTopic.TopicTitle) + '</div>';
486
+ tmpHTML += '<div class="pict-inline-doc-tm-topic-meta">';
487
+ tmpHTML += this._escapeHTML(tmpTopic.TopicCode);
488
+ if (tmpTopic.TopicHelpFilePath)
489
+ {
490
+ tmpHTML += ' &middot; ' + this._escapeHTML(tmpTopic.TopicHelpFilePath);
491
+ }
492
+ tmpHTML += ' &middot; ' + tmpTopic.RouteCount + ' route' + (tmpTopic.RouteCount !== 1 ? 's' : '');
493
+ tmpHTML += '</div>';
494
+ tmpHTML += '</div>';
495
+ tmpHTML += '<div class="pict-inline-doc-tm-topic-actions">';
496
+ tmpHTML += '<button class="pict-inline-doc-tm-action-btn" data-action="edit" data-topic-code="' + this._escapeHTML(tmpTopic.TopicCode) + '" title="Edit">&#x270E;</button>';
497
+ tmpHTML += '<button class="pict-inline-doc-tm-action-btn danger" data-action="delete" data-topic-code="' + this._escapeHTML(tmpTopic.TopicCode) + '" title="Delete">&#x2715;</button>';
498
+ tmpHTML += '</div>';
499
+ tmpHTML += '</div>';
500
+ }
501
+
502
+ tmpHTML += '</div>';
503
+ tmpHTML += '<div class="pict-inline-doc-tm-new-topic" data-action="new-topic">+ New Topic</div>';
504
+
505
+ return tmpHTML;
506
+ }
507
+
508
+ /**
509
+ * Wire click handlers for the topic list modal.
510
+ *
511
+ * @param {HTMLElement} pDialog - The modal dialog element
512
+ */
513
+ _wireTopicListHandlers(pDialog)
514
+ {
515
+ let tmpSelf = this;
516
+ let tmpModal = this._getModal();
517
+ let tmpProvider = this._getProvider();
518
+
519
+ // Edit buttons
520
+ let tmpEditBtns = pDialog.querySelectorAll('[data-action="edit"]');
521
+ for (let i = 0; i < tmpEditBtns.length; i++)
522
+ {
523
+ tmpEditBtns[i].addEventListener('click', (pEvent) =>
524
+ {
525
+ pEvent.stopPropagation();
526
+ let tmpCode = tmpEditBtns[i].getAttribute('data-topic-code');
527
+ // Dismiss the list modal, then open editor
528
+ if (tmpModal && tmpModal.dismissModals)
529
+ {
530
+ tmpModal.dismissModals();
531
+ }
532
+ setTimeout(() => { tmpSelf.showTopicEditor(tmpCode); }, 250);
533
+ });
534
+ }
535
+
536
+ // Delete buttons
537
+ let tmpDeleteBtns = pDialog.querySelectorAll('[data-action="delete"]');
538
+ for (let i = 0; i < tmpDeleteBtns.length; i++)
539
+ {
540
+ tmpDeleteBtns[i].addEventListener('click', (pEvent) =>
541
+ {
542
+ pEvent.stopPropagation();
543
+ let tmpCode = tmpDeleteBtns[i].getAttribute('data-topic-code');
544
+
545
+ if (tmpModal && tmpModal.confirm)
546
+ {
547
+ tmpModal.confirm(
548
+ 'Are you sure you want to delete the topic "' + tmpCode + '"?',
549
+ { title: 'Delete Topic', dangerous: true }
550
+ ).then((pConfirmed) =>
551
+ {
552
+ if (pConfirmed && tmpProvider)
553
+ {
554
+ tmpProvider.removeTopic(tmpCode);
555
+ tmpProvider.saveTopics();
556
+
557
+ // Re-render nav
558
+ let tmpNavView = tmpSelf.pict.views['InlineDoc-Nav'];
559
+ if (tmpNavView)
560
+ {
561
+ tmpNavView.render();
562
+ }
563
+
564
+ if (tmpModal.toast)
565
+ {
566
+ tmpModal.toast('Topic deleted.', { type: 'success' });
567
+ }
568
+
569
+ // Re-open the list
570
+ setTimeout(() => { tmpSelf.showTopicManager(); }, 250);
571
+ }
572
+ });
573
+ }
574
+ });
575
+ }
576
+
577
+ // New topic button
578
+ let tmpNewBtn = pDialog.querySelector('[data-action="new-topic"]');
579
+ if (tmpNewBtn)
580
+ {
581
+ tmpNewBtn.addEventListener('click', () =>
582
+ {
583
+ if (tmpModal && tmpModal.dismissModals)
584
+ {
585
+ tmpModal.dismissModals();
586
+ }
587
+ setTimeout(() => { tmpSelf.showTopicEditor(null); }, 250);
588
+ });
589
+ }
590
+ }
591
+
592
+ // -- Topic Editor --
593
+
594
+ /**
595
+ * Show the topic editor modal for creating or editing a topic.
596
+ *
597
+ * @param {string|null} pTopicCode - Topic code to edit, or null for new
598
+ */
599
+ showTopicEditor(pTopicCode)
600
+ {
601
+ let tmpModal = this._getModal();
602
+ let tmpProvider = this._getProvider();
603
+
604
+ if (!tmpModal || !tmpProvider)
605
+ {
606
+ return;
607
+ }
608
+
609
+ let tmpState = this.pict.AppData.InlineDocumentation;
610
+ let tmpIsNew = !pTopicCode;
611
+ let tmpTopic = null;
612
+
613
+ if (pTopicCode && tmpState.Topics && tmpState.Topics[pTopicCode])
614
+ {
615
+ // Clone for editing
616
+ tmpTopic = JSON.parse(JSON.stringify(tmpState.Topics[pTopicCode]));
617
+ }
618
+ else
619
+ {
620
+ tmpTopic =
621
+ {
622
+ TopicCode: '',
623
+ TopicTitle: '',
624
+ TopicHelpFilePath: '',
625
+ Routes: []
626
+ };
627
+ }
628
+
629
+ let tmpContent = this._buildTopicEditorHTML(tmpTopic, tmpIsNew);
630
+
631
+ // Track editor routes state in closure
632
+ let tmpEditorRoutes = tmpTopic.Routes ? tmpTopic.Routes.slice() : [];
633
+
634
+ tmpModal.show(
635
+ {
636
+ title: tmpIsNew ? 'New Topic' : 'Edit Topic',
637
+ content: tmpContent,
638
+ closeable: true,
639
+ width: '500px',
640
+ buttons:
641
+ [
642
+ { Hash: 'cancel', Label: 'Cancel' },
643
+ { Hash: 'save', Label: 'Save', Style: 'primary' }
644
+ ],
645
+ onOpen: (pDialog) =>
646
+ {
647
+ this._wireTopicEditorHandlers(pDialog, tmpTopic, tmpIsNew, tmpEditorRoutes);
648
+ }
649
+ }).then((pResult) =>
650
+ {
651
+ if (pResult === 'save')
652
+ {
653
+ this._handleTopicEditorSave(tmpTopic, tmpIsNew, tmpEditorRoutes);
654
+ }
655
+ else
656
+ {
657
+ // Return to list on cancel
658
+ setTimeout(() => { this.showTopicManager(); }, 250);
659
+ }
660
+ });
661
+ }
662
+
663
+ /**
664
+ * Build the HTML for the topic editor form.
665
+ *
666
+ * @param {Object} pTopic - The topic data
667
+ * @param {boolean} pIsNew - Whether this is a new topic
668
+ * @returns {string} HTML content
669
+ */
670
+ _buildTopicEditorHTML(pTopic, pIsNew)
671
+ {
672
+ let tmpHTML = '';
673
+
674
+ // Topic Code
675
+ tmpHTML += '<div class="pict-inline-doc-tm-form-group">';
676
+ tmpHTML += '<label class="pict-inline-doc-tm-form-label">Topic Code</label>';
677
+ if (pIsNew)
678
+ {
679
+ tmpHTML += '<input type="text" class="pict-inline-doc-tm-form-input" id="tm-editor-code" value="" placeholder="MY-TOPIC-CODE" />';
680
+ tmpHTML += '<div class="pict-inline-doc-tm-form-hint">Uppercase letters, numbers, and hyphens only.</div>';
681
+ }
682
+ else
683
+ {
684
+ tmpHTML += '<input type="text" class="pict-inline-doc-tm-form-input" id="tm-editor-code" value="' + this._escapeHTML(pTopic.TopicCode) + '" readonly />';
685
+ }
686
+ tmpHTML += '<div class="pict-inline-doc-tm-validation-error" id="tm-editor-code-error"></div>';
687
+ tmpHTML += '</div>';
688
+
689
+ // Topic Title
690
+ tmpHTML += '<div class="pict-inline-doc-tm-form-group">';
691
+ tmpHTML += '<label class="pict-inline-doc-tm-form-label">Title</label>';
692
+ tmpHTML += '<input type="text" class="pict-inline-doc-tm-form-input" id="tm-editor-title" value="' + this._escapeHTML(pTopic.TopicTitle || pTopic.Name || '') + '" placeholder="My Topic Title" />';
693
+ tmpHTML += '<div class="pict-inline-doc-tm-validation-error" id="tm-editor-title-error"></div>';
694
+ tmpHTML += '</div>';
695
+
696
+ // Help File Path
697
+ tmpHTML += '<div class="pict-inline-doc-tm-form-group">';
698
+ tmpHTML += '<label class="pict-inline-doc-tm-form-label">Help Document</label>';
699
+ tmpHTML += '<div style="display:flex;gap:0.3em;align-items:center;">';
700
+ tmpHTML += '<input type="text" class="pict-inline-doc-tm-form-input" id="tm-editor-helpfile" value="' + this._escapeHTML(pTopic.TopicHelpFilePath || '') + '" placeholder="help-topic.md" style="flex:1;" />';
701
+ tmpHTML += '<button class="pict-inline-doc-tm-route-action-btn" id="tm-editor-browse-sidebar" title="Browse sidebar documents">Browse</button>';
702
+ tmpHTML += '</div>';
703
+ tmpHTML += '</div>';
704
+
705
+ // Routes
706
+ tmpHTML += '<div class="pict-inline-doc-tm-form-group">';
707
+ tmpHTML += '<label class="pict-inline-doc-tm-form-label">Routes</label>';
708
+ tmpHTML += '<div class="pict-inline-doc-tm-routes-section">';
709
+ tmpHTML += '<div class="pict-inline-doc-tm-route-chips" id="tm-editor-route-chips">';
710
+ tmpHTML += this._buildRouteChipsHTML(pTopic.Routes || []);
711
+ tmpHTML += '</div>';
712
+ tmpHTML += '<div class="pict-inline-doc-tm-route-actions">';
713
+ tmpHTML += '<button class="pict-inline-doc-tm-route-action-btn" id="tm-editor-add-route">+ Add Route</button>';
714
+
715
+ let tmpState = this.pict.AppData.InlineDocumentation;
716
+ if (tmpState && tmpState.CurrentRoute)
717
+ {
718
+ tmpHTML += '<button class="pict-inline-doc-tm-route-action-btn accent" id="tm-editor-add-current-route">+ Current Route</button>';
719
+ tmpHTML += '<button class="pict-inline-doc-tm-route-action-btn accent" id="tm-editor-build-wildcard">Build Wildcard</button>';
720
+ }
721
+
722
+ tmpHTML += '</div>';
723
+ tmpHTML += '<div class="pict-inline-doc-tm-route-input-row" id="tm-editor-route-input-row">';
724
+ tmpHTML += '<input type="text" class="pict-inline-doc-tm-route-input" id="tm-editor-route-input" placeholder="/my/route" />';
725
+ tmpHTML += '<button class="pict-inline-doc-tm-route-action-btn accent" id="tm-editor-route-input-add">Add</button>';
726
+ tmpHTML += '</div>';
727
+ tmpHTML += '</div>';
728
+ tmpHTML += '</div>';
729
+
730
+ return tmpHTML;
731
+ }
732
+
733
+ /**
734
+ * Build HTML for route chips.
735
+ *
736
+ * @param {Array} pRoutes - Array of route pattern strings
737
+ * @returns {string} HTML for the chips
738
+ */
739
+ _buildRouteChipsHTML(pRoutes)
740
+ {
741
+ if (!pRoutes || pRoutes.length < 1)
742
+ {
743
+ return '<span style="font-size:0.8em;color:#8A7F72;">No routes bound.</span>';
744
+ }
745
+
746
+ let tmpHTML = '';
747
+ for (let i = 0; i < pRoutes.length; i++)
748
+ {
749
+ tmpHTML += '<span class="pict-inline-doc-tm-route-chip">';
750
+ tmpHTML += this._escapeHTML(pRoutes[i]);
751
+ tmpHTML += '<span class="pict-inline-doc-tm-route-chip-remove" data-route="' + this._escapeHTML(pRoutes[i]) + '">&times;</span>';
752
+ tmpHTML += '</span>';
753
+ }
754
+ return tmpHTML;
755
+ }
756
+
757
+ /**
758
+ * Refresh the route chips in an open editor dialog.
759
+ *
760
+ * @param {Array} pRoutes - Current routes array
761
+ */
762
+ _refreshRouteChips(pRoutes)
763
+ {
764
+ if (typeof document === 'undefined')
765
+ {
766
+ return;
767
+ }
768
+
769
+ let tmpContainer = document.getElementById('tm-editor-route-chips');
770
+ if (tmpContainer)
771
+ {
772
+ tmpContainer.innerHTML = this._buildRouteChipsHTML(pRoutes);
773
+ this._wireRouteChipRemoveHandlers(pRoutes);
774
+ }
775
+ }
776
+
777
+ /**
778
+ * Wire remove handlers on route chips.
779
+ *
780
+ * @param {Array} pRoutes - The mutable routes array
781
+ */
782
+ _wireRouteChipRemoveHandlers(pRoutes)
783
+ {
784
+ if (typeof document === 'undefined')
785
+ {
786
+ return;
787
+ }
788
+
789
+ let tmpSelf = this;
790
+ let tmpRemoveBtns = document.querySelectorAll('.pict-inline-doc-tm-route-chip-remove');
791
+ for (let i = 0; i < tmpRemoveBtns.length; i++)
792
+ {
793
+ tmpRemoveBtns[i].addEventListener('click', (pEvent) =>
794
+ {
795
+ pEvent.stopPropagation();
796
+ let tmpRoute = tmpRemoveBtns[i].getAttribute('data-route');
797
+ let tmpIdx = pRoutes.indexOf(tmpRoute);
798
+ if (tmpIdx >= 0)
799
+ {
800
+ pRoutes.splice(tmpIdx, 1);
801
+ }
802
+ tmpSelf._refreshRouteChips(pRoutes);
803
+ });
804
+ }
805
+ }
806
+
807
+ /**
808
+ * Wire all handlers for the topic editor form.
809
+ *
810
+ * @param {HTMLElement} pDialog - The modal dialog element
811
+ * @param {Object} pTopic - The topic data
812
+ * @param {boolean} pIsNew - Whether this is a new topic
813
+ * @param {Array} pEditorRoutes - Mutable routes array for this editor session
814
+ */
815
+ _wireTopicEditorHandlers(pDialog, pTopic, pIsNew, pEditorRoutes)
816
+ {
817
+ let tmpSelf = this;
818
+ let tmpProvider = this._getProvider();
819
+ let tmpState = this.pict.AppData.InlineDocumentation;
820
+
821
+ // Route chip remove handlers
822
+ this._wireRouteChipRemoveHandlers(pEditorRoutes);
823
+
824
+ // Add Route button — show input row
825
+ let tmpAddRouteBtn = document.getElementById('tm-editor-add-route');
826
+ if (tmpAddRouteBtn)
827
+ {
828
+ tmpAddRouteBtn.addEventListener('click', () =>
829
+ {
830
+ let tmpRow = document.getElementById('tm-editor-route-input-row');
831
+ if (tmpRow)
832
+ {
833
+ tmpRow.classList.toggle('visible');
834
+ let tmpInput = document.getElementById('tm-editor-route-input');
835
+ if (tmpInput)
836
+ {
837
+ tmpInput.focus();
838
+ }
839
+ }
840
+ });
841
+ }
842
+
843
+ // Add route from text input
844
+ let tmpRouteInputAddBtn = document.getElementById('tm-editor-route-input-add');
845
+ if (tmpRouteInputAddBtn)
846
+ {
847
+ tmpRouteInputAddBtn.addEventListener('click', () =>
848
+ {
849
+ let tmpInput = document.getElementById('tm-editor-route-input');
850
+ if (tmpInput && tmpInput.value.trim())
851
+ {
852
+ let tmpRoute = tmpInput.value.trim();
853
+ if (tmpRoute.charAt(0) !== '/')
854
+ {
855
+ tmpRoute = '/' + tmpRoute;
856
+ }
857
+ if (pEditorRoutes.indexOf(tmpRoute) < 0)
858
+ {
859
+ pEditorRoutes.push(tmpRoute);
860
+ tmpSelf._refreshRouteChips(pEditorRoutes);
861
+ }
862
+ tmpInput.value = '';
863
+ }
864
+ });
865
+ }
866
+
867
+ // Enter key on route input
868
+ let tmpRouteInput = document.getElementById('tm-editor-route-input');
869
+ if (tmpRouteInput)
870
+ {
871
+ tmpRouteInput.addEventListener('keydown', (pEvent) =>
872
+ {
873
+ if (pEvent.key === 'Enter')
874
+ {
875
+ pEvent.preventDefault();
876
+ if (tmpRouteInputAddBtn)
877
+ {
878
+ tmpRouteInputAddBtn.click();
879
+ }
880
+ }
881
+ });
882
+ }
883
+
884
+ // Add Current Route button
885
+ let tmpAddCurrentBtn = document.getElementById('tm-editor-add-current-route');
886
+ if (tmpAddCurrentBtn && tmpState && tmpState.CurrentRoute)
887
+ {
888
+ tmpAddCurrentBtn.addEventListener('click', () =>
889
+ {
890
+ let tmpRoute = tmpState.CurrentRoute;
891
+ if (pEditorRoutes.indexOf(tmpRoute) < 0)
892
+ {
893
+ pEditorRoutes.push(tmpRoute);
894
+ tmpSelf._refreshRouteChips(pEditorRoutes);
895
+ }
896
+ });
897
+ }
898
+
899
+ // Build Wildcard button
900
+ let tmpWildcardBtn = document.getElementById('tm-editor-build-wildcard');
901
+ if (tmpWildcardBtn && tmpProvider && tmpState && tmpState.CurrentRoute)
902
+ {
903
+ tmpWildcardBtn.addEventListener('click', () =>
904
+ {
905
+ let tmpModal = tmpSelf._getModal();
906
+ if (tmpModal && tmpModal.dismissModals)
907
+ {
908
+ tmpModal.dismissModals();
909
+ }
910
+ setTimeout(() =>
911
+ {
912
+ tmpSelf.showWildcardBuilder(tmpState.CurrentRoute, (pPattern) =>
913
+ {
914
+ if (pPattern && pEditorRoutes.indexOf(pPattern) < 0)
915
+ {
916
+ pEditorRoutes.push(pPattern);
917
+ }
918
+ // Re-open the editor
919
+ tmpSelf._reopenEditorAfterSubflow(pTopic, pIsNew, pEditorRoutes);
920
+ });
921
+ }, 250);
922
+ });
923
+ }
924
+
925
+ // Browse Sidebar button
926
+ let tmpBrowseBtn = document.getElementById('tm-editor-browse-sidebar');
927
+ if (tmpBrowseBtn)
928
+ {
929
+ tmpBrowseBtn.addEventListener('click', () =>
930
+ {
931
+ let tmpModal = tmpSelf._getModal();
932
+ if (tmpModal && tmpModal.dismissModals)
933
+ {
934
+ tmpModal.dismissModals();
935
+ }
936
+ setTimeout(() =>
937
+ {
938
+ tmpSelf._showSidebarPicker((pPath) =>
939
+ {
940
+ if (pPath)
941
+ {
942
+ pTopic.TopicHelpFilePath = pPath;
943
+ }
944
+ tmpSelf._reopenEditorAfterSubflow(pTopic, pIsNew, pEditorRoutes);
945
+ });
946
+ }, 250);
947
+ });
948
+ }
949
+ }
950
+
951
+ /**
952
+ * Re-open the topic editor after returning from a sub-flow (wildcard builder, sidebar picker).
953
+ *
954
+ * Captures current form values from the DOM before the modal was dismissed,
955
+ * then reconstructs the editor with updated state.
956
+ *
957
+ * @param {Object} pTopic - The topic data (may have been updated by sub-flow)
958
+ * @param {boolean} pIsNew - Whether this is a new topic
959
+ * @param {Array} pEditorRoutes - Current routes for this editor session
960
+ */
961
+ _reopenEditorAfterSubflow(pTopic, pIsNew, pEditorRoutes)
962
+ {
963
+ let tmpModal = this._getModal();
964
+ let tmpProvider = this._getProvider();
965
+
966
+ if (!tmpModal || !tmpProvider)
967
+ {
968
+ return;
969
+ }
970
+
971
+ // Rebuild the topic from whatever was captured
972
+ let tmpContent = this._buildTopicEditorHTML(
973
+ {
974
+ TopicCode: pTopic.TopicCode,
975
+ TopicTitle: pTopic.TopicTitle,
976
+ TopicHelpFilePath: pTopic.TopicHelpFilePath,
977
+ Routes: pEditorRoutes
978
+ }, pIsNew);
979
+
980
+ tmpModal.show(
981
+ {
982
+ title: pIsNew ? 'New Topic' : 'Edit Topic',
983
+ content: tmpContent,
984
+ closeable: true,
985
+ width: '500px',
986
+ buttons:
987
+ [
988
+ { Hash: 'cancel', Label: 'Cancel' },
989
+ { Hash: 'save', Label: 'Save', Style: 'primary' }
990
+ ],
991
+ onOpen: (pDialog) =>
992
+ {
993
+ this._wireTopicEditorHandlers(pDialog, pTopic, pIsNew, pEditorRoutes);
994
+ }
995
+ }).then((pResult) =>
996
+ {
997
+ if (pResult === 'save')
998
+ {
999
+ this._handleTopicEditorSave(pTopic, pIsNew, pEditorRoutes);
1000
+ }
1001
+ else
1002
+ {
1003
+ setTimeout(() => { this.showTopicManager(); }, 250);
1004
+ }
1005
+ });
1006
+ }
1007
+
1008
+ /**
1009
+ * Handle saving from the topic editor.
1010
+ *
1011
+ * Reads form values, validates, and persists.
1012
+ *
1013
+ * @param {Object} pTopic - The original topic data
1014
+ * @param {boolean} pIsNew - Whether this is a new topic
1015
+ * @param {Array} pEditorRoutes - Current routes from the editor
1016
+ */
1017
+ _handleTopicEditorSave(pTopic, pIsNew, pEditorRoutes)
1018
+ {
1019
+ let tmpProvider = this._getProvider();
1020
+ let tmpModal = this._getModal();
1021
+ let tmpState = this.pict.AppData.InlineDocumentation;
1022
+
1023
+ if (!tmpProvider)
1024
+ {
1025
+ return;
1026
+ }
1027
+
1028
+ // Read form values from DOM (they're still present briefly during dismiss)
1029
+ let tmpCode = '';
1030
+ let tmpTitle = '';
1031
+ let tmpHelpFile = '';
1032
+
1033
+ if (typeof document !== 'undefined')
1034
+ {
1035
+ let tmpCodeInput = document.getElementById('tm-editor-code');
1036
+ let tmpTitleInput = document.getElementById('tm-editor-title');
1037
+ let tmpHelpInput = document.getElementById('tm-editor-helpfile');
1038
+
1039
+ if (tmpCodeInput) { tmpCode = tmpCodeInput.value.trim(); }
1040
+ if (tmpTitleInput) { tmpTitle = tmpTitleInput.value.trim(); }
1041
+ if (tmpHelpInput) { tmpHelpFile = tmpHelpInput.value.trim(); }
1042
+ }
1043
+
1044
+ // Validation
1045
+ let tmpErrors = [];
1046
+
1047
+ if (pIsNew)
1048
+ {
1049
+ if (!tmpCode)
1050
+ {
1051
+ tmpErrors.push('Topic Code is required.');
1052
+ }
1053
+ else if (!/^[A-Z0-9][A-Z0-9-]*$/.test(tmpCode))
1054
+ {
1055
+ tmpErrors.push('Topic Code must use uppercase letters, numbers, and hyphens only.');
1056
+ }
1057
+ else if (tmpState.Topics && tmpState.Topics[tmpCode])
1058
+ {
1059
+ tmpErrors.push('A topic with code "' + tmpCode + '" already exists.');
1060
+ }
1061
+ }
1062
+ else
1063
+ {
1064
+ tmpCode = pTopic.TopicCode;
1065
+ }
1066
+
1067
+ if (!tmpTitle)
1068
+ {
1069
+ tmpErrors.push('Title is required.');
1070
+ }
1071
+
1072
+ if (tmpErrors.length > 0)
1073
+ {
1074
+ if (tmpModal && tmpModal.toast)
1075
+ {
1076
+ tmpModal.toast(tmpErrors.join(' '), { type: 'error' });
1077
+ }
1078
+ // Re-open editor with current values
1079
+ pTopic.TopicTitle = tmpTitle;
1080
+ pTopic.TopicHelpFilePath = tmpHelpFile;
1081
+ if (pIsNew)
1082
+ {
1083
+ pTopic.TopicCode = tmpCode;
1084
+ }
1085
+ setTimeout(() => { this._reopenEditorAfterSubflow(pTopic, pIsNew, pEditorRoutes); }, 300);
1086
+ return;
1087
+ }
1088
+
1089
+ // Apply changes
1090
+ if (pIsNew)
1091
+ {
1092
+ tmpProvider.addTopic(tmpCode,
1093
+ {
1094
+ TopicCode: tmpCode,
1095
+ TopicTitle: tmpTitle,
1096
+ TopicHelpFilePath: tmpHelpFile,
1097
+ Routes: pEditorRoutes
1098
+ });
1099
+ }
1100
+ else
1101
+ {
1102
+ tmpProvider.updateTopic(tmpCode,
1103
+ {
1104
+ TopicTitle: tmpTitle,
1105
+ TopicHelpFilePath: tmpHelpFile,
1106
+ Routes: pEditorRoutes
1107
+ });
1108
+ }
1109
+
1110
+ tmpProvider.saveTopics();
1111
+
1112
+ // Re-render nav
1113
+ let tmpNavView = this.pict.views['InlineDoc-Nav'];
1114
+ if (tmpNavView)
1115
+ {
1116
+ tmpNavView.render();
1117
+ }
1118
+
1119
+ if (tmpModal && tmpModal.toast)
1120
+ {
1121
+ tmpModal.toast('Topic saved.', { type: 'success' });
1122
+ }
1123
+
1124
+ // Return to topic list
1125
+ setTimeout(() => { this.showTopicManager(); }, 300);
1126
+ }
1127
+
1128
+ // -- Wildcard Builder --
1129
+
1130
+ /**
1131
+ * Show the wildcard builder modal.
1132
+ *
1133
+ * Displays route segments as clickable blocks and lets the user
1134
+ * visually choose where the wildcard starts.
1135
+ *
1136
+ * @param {string} pCurrentRoute - The route to build a pattern for
1137
+ * @param {Function} fOnSelect - Callback receiving the selected pattern (or null on cancel)
1138
+ */
1139
+ showWildcardBuilder(pCurrentRoute, fOnSelect)
1140
+ {
1141
+ let tmpModal = this._getModal();
1142
+ let tmpProvider = this._getProvider();
1143
+
1144
+ if (!tmpModal || !tmpProvider)
1145
+ {
1146
+ if (typeof fOnSelect === 'function') { fOnSelect(null); }
1147
+ return;
1148
+ }
1149
+
1150
+ let tmpSegments = tmpProvider.getRouteSegments(pCurrentRoute);
1151
+
1152
+ if (tmpSegments.length < 1)
1153
+ {
1154
+ if (tmpModal.toast)
1155
+ {
1156
+ tmpModal.toast('No route segments to build a wildcard from.', { type: 'error' });
1157
+ }
1158
+ if (typeof fOnSelect === 'function') { fOnSelect(null); }
1159
+ return;
1160
+ }
1161
+
1162
+ // Default selection: last segment before end (or first if only one)
1163
+ let tmpSelectedIndex = Math.max(0, tmpSegments.length - 2);
1164
+ let tmpContent = this._buildWildcardBuilderHTML(tmpSegments, pCurrentRoute, tmpSelectedIndex);
1165
+
1166
+ tmpModal.show(
1167
+ {
1168
+ title: 'Build Wildcard Pattern',
1169
+ content: tmpContent,
1170
+ closeable: true,
1171
+ width: '520px',
1172
+ buttons:
1173
+ [
1174
+ { Hash: 'cancel', Label: 'Cancel' },
1175
+ { Hash: 'exact', Label: 'Use Exact Route' },
1176
+ { Hash: 'pattern', Label: 'Use Pattern', Style: 'primary' }
1177
+ ],
1178
+ onOpen: (pDialog) =>
1179
+ {
1180
+ this._wireWildcardBuilderHandlers(pDialog, tmpSegments, tmpSelectedIndex);
1181
+ }
1182
+ }).then((pResult) =>
1183
+ {
1184
+ if (pResult === 'pattern')
1185
+ {
1186
+ // Get the current selection
1187
+ let tmpPreview = (typeof document !== 'undefined') ? document.getElementById('tm-wc-preview-value') : null;
1188
+ let tmpPattern = tmpPreview ? tmpPreview.textContent : tmpSegments[tmpSelectedIndex].WildcardPattern;
1189
+ if (typeof fOnSelect === 'function') { fOnSelect(tmpPattern); }
1190
+ }
1191
+ else if (pResult === 'exact')
1192
+ {
1193
+ if (typeof fOnSelect === 'function') { fOnSelect(pCurrentRoute); }
1194
+ }
1195
+ else
1196
+ {
1197
+ if (typeof fOnSelect === 'function') { fOnSelect(null); }
1198
+ }
1199
+ });
1200
+ }
1201
+
1202
+ /**
1203
+ * Build the HTML for the wildcard builder.
1204
+ *
1205
+ * @param {Array} pSegments - Segment objects from getRouteSegments()
1206
+ * @param {string} pCurrentRoute - The original route
1207
+ * @param {number} pSelectedIndex - The initially selected segment index
1208
+ * @returns {string} HTML content
1209
+ */
1210
+ _buildWildcardBuilderHTML(pSegments, pCurrentRoute, pSelectedIndex)
1211
+ {
1212
+ let tmpHTML = '<div class="pict-inline-doc-tm-wc-container">';
1213
+
1214
+ tmpHTML += '<div class="pict-inline-doc-tm-wc-label">Click a segment to set the wildcard boundary. Everything after the selected segment will match any path.</div>';
1215
+
1216
+ tmpHTML += '<div class="pict-inline-doc-tm-wc-segments" id="tm-wc-segments">';
1217
+
1218
+ for (let i = 0; i < pSegments.length; i++)
1219
+ {
1220
+ let tmpClass = 'pict-inline-doc-tm-wc-segment';
1221
+ if (i === pSelectedIndex)
1222
+ {
1223
+ tmpClass += ' selected';
1224
+ }
1225
+ else if (i > pSelectedIndex)
1226
+ {
1227
+ tmpClass += ' after-wildcard';
1228
+ }
1229
+
1230
+ tmpHTML += '<span class="pict-inline-doc-tm-wc-slash">/</span>';
1231
+ tmpHTML += '<span class="' + tmpClass + '" data-segment-index="' + i + '">';
1232
+ tmpHTML += this._escapeHTML(pSegments[i].Segment);
1233
+ tmpHTML += '</span>';
1234
+ }
1235
+
1236
+ tmpHTML += '<span class="pict-inline-doc-tm-wc-slash">/</span>';
1237
+ tmpHTML += '<span class="pict-inline-doc-tm-wc-wildcard-star" id="tm-wc-star">*</span>';
1238
+
1239
+ tmpHTML += '</div>';
1240
+
1241
+ tmpHTML += '<div class="pict-inline-doc-tm-wc-preview-label">Pattern</div>';
1242
+ tmpHTML += '<div class="pict-inline-doc-tm-wc-preview" id="tm-wc-preview-value">';
1243
+ tmpHTML += this._escapeHTML(pSegments[pSelectedIndex].WildcardPattern);
1244
+ tmpHTML += '</div>';
1245
+
1246
+ tmpHTML += '</div>';
1247
+
1248
+ return tmpHTML;
1249
+ }
1250
+
1251
+ /**
1252
+ * Wire click handlers for the wildcard builder segments.
1253
+ *
1254
+ * @param {HTMLElement} pDialog - The modal dialog element
1255
+ * @param {Array} pSegments - Segment objects
1256
+ * @param {number} pInitialIndex - Initially selected index
1257
+ */
1258
+ _wireWildcardBuilderHandlers(pDialog, pSegments, pInitialIndex)
1259
+ {
1260
+ let tmpSelectedIndex = pInitialIndex;
1261
+
1262
+ let tmpUpdateSelection = (pNewIndex) =>
1263
+ {
1264
+ tmpSelectedIndex = pNewIndex;
1265
+ let tmpSegmentEls = pDialog.querySelectorAll('.pict-inline-doc-tm-wc-segment');
1266
+ for (let i = 0; i < tmpSegmentEls.length; i++)
1267
+ {
1268
+ let tmpIdx = parseInt(tmpSegmentEls[i].getAttribute('data-segment-index'), 10);
1269
+ tmpSegmentEls[i].classList.remove('selected', 'after-wildcard');
1270
+ if (tmpIdx === pNewIndex)
1271
+ {
1272
+ tmpSegmentEls[i].classList.add('selected');
1273
+ }
1274
+ else if (tmpIdx > pNewIndex)
1275
+ {
1276
+ tmpSegmentEls[i].classList.add('after-wildcard');
1277
+ }
1278
+ }
1279
+
1280
+ let tmpPreview = pDialog.querySelector('#tm-wc-preview-value');
1281
+ if (tmpPreview)
1282
+ {
1283
+ tmpPreview.textContent = pSegments[pNewIndex].WildcardPattern;
1284
+ }
1285
+ };
1286
+
1287
+ let tmpSegmentEls = pDialog.querySelectorAll('.pict-inline-doc-tm-wc-segment');
1288
+ for (let i = 0; i < tmpSegmentEls.length; i++)
1289
+ {
1290
+ tmpSegmentEls[i].addEventListener('click', () =>
1291
+ {
1292
+ let tmpIdx = parseInt(tmpSegmentEls[i].getAttribute('data-segment-index'), 10);
1293
+ tmpUpdateSelection(tmpIdx);
1294
+ });
1295
+ }
1296
+ }
1297
+
1298
+ // -- Bind Topic to Route --
1299
+
1300
+ /**
1301
+ * Show the quick-bind flow for connecting a topic to the current route.
1302
+ */
1303
+ showBindTopicToRoute()
1304
+ {
1305
+ let tmpModal = this._getModal();
1306
+ let tmpProvider = this._getProvider();
1307
+
1308
+ if (!tmpModal || !tmpProvider)
1309
+ {
1310
+ return;
1311
+ }
1312
+
1313
+ let tmpState = this.pict.AppData.InlineDocumentation;
1314
+ if (!tmpState || !tmpState.CurrentRoute)
1315
+ {
1316
+ if (tmpModal.toast)
1317
+ {
1318
+ tmpModal.toast('No current route to bind to.', { type: 'error' });
1319
+ }
1320
+ return;
1321
+ }
1322
+
1323
+ let tmpCurrentRoute = tmpState.CurrentRoute;
1324
+ let tmpTopics = tmpProvider.getTopicList();
1325
+ let tmpContent = this._buildBindHTML(tmpCurrentRoute, tmpTopics);
1326
+
1327
+ // Track selection state
1328
+ let tmpSelectedTopicCode = null;
1329
+ let tmpRouteType = 'exact'; // 'exact' or 'wildcard'
1330
+
1331
+ tmpModal.show(
1332
+ {
1333
+ title: 'Bind Topic to Route',
1334
+ content: tmpContent,
1335
+ closeable: true,
1336
+ width: '480px',
1337
+ buttons:
1338
+ [
1339
+ { Hash: 'cancel', Label: 'Cancel' },
1340
+ { Hash: 'bind', Label: 'Bind Route', Style: 'primary' }
1341
+ ],
1342
+ onOpen: (pDialog) =>
1343
+ {
1344
+ this._wireBindHandlers(pDialog, tmpTopics, tmpCurrentRoute,
1345
+ (pCode) => { tmpSelectedTopicCode = pCode; },
1346
+ (pType) => { tmpRouteType = pType; }
1347
+ );
1348
+ }
1349
+ }).then((pResult) =>
1350
+ {
1351
+ if (pResult !== 'bind' || !tmpSelectedTopicCode)
1352
+ {
1353
+ return;
1354
+ }
1355
+
1356
+ if (tmpSelectedTopicCode === '__NEW__')
1357
+ {
1358
+ // Open new topic editor with current route pre-filled
1359
+ this.showTopicEditor(null);
1360
+ return;
1361
+ }
1362
+
1363
+ if (tmpRouteType === 'wildcard')
1364
+ {
1365
+ // Open wildcard builder, then bind
1366
+ this.showWildcardBuilder(tmpCurrentRoute, (pPattern) =>
1367
+ {
1368
+ if (pPattern)
1369
+ {
1370
+ tmpProvider.addRouteToTopic(tmpSelectedTopicCode, pPattern);
1371
+ tmpProvider.saveTopics();
1372
+
1373
+ let tmpNavView = this.pict.views['InlineDoc-Nav'];
1374
+ if (tmpNavView) { tmpNavView.render(); }
1375
+
1376
+ if (tmpModal.toast)
1377
+ {
1378
+ tmpModal.toast('Route bound to topic.', { type: 'success' });
1379
+ }
1380
+ }
1381
+ });
1382
+ }
1383
+ else
1384
+ {
1385
+ // Exact match bind
1386
+ tmpProvider.addRouteToTopic(tmpSelectedTopicCode, tmpCurrentRoute);
1387
+ tmpProvider.saveTopics();
1388
+
1389
+ let tmpNavView = this.pict.views['InlineDoc-Nav'];
1390
+ if (tmpNavView) { tmpNavView.render(); }
1391
+
1392
+ if (tmpModal.toast)
1393
+ {
1394
+ tmpModal.toast('Route bound to topic.', { type: 'success' });
1395
+ }
1396
+ }
1397
+ });
1398
+ }
1399
+
1400
+ /**
1401
+ * Build the HTML for the bind-topic-to-route modal.
1402
+ *
1403
+ * @param {string} pCurrentRoute - The current route
1404
+ * @param {Array} pTopics - Topic list
1405
+ * @returns {string} HTML content
1406
+ */
1407
+ _buildBindHTML(pCurrentRoute, pTopics)
1408
+ {
1409
+ let tmpHTML = '';
1410
+
1411
+ tmpHTML += '<div class="pict-inline-doc-tm-bind-route-display">' + this._escapeHTML(pCurrentRoute) + '</div>';
1412
+
1413
+ // Route type selection
1414
+ tmpHTML += '<div class="pict-inline-doc-tm-form-group">';
1415
+ tmpHTML += '<label class="pict-inline-doc-tm-form-label">Route Match Type</label>';
1416
+ tmpHTML += '<div class="pict-inline-doc-tm-bind-route-type">';
1417
+ tmpHTML += '<div class="pict-inline-doc-tm-bind-route-type-btn selected" data-route-type="exact">Exact Match</div>';
1418
+ tmpHTML += '<div class="pict-inline-doc-tm-bind-route-type-btn" data-route-type="wildcard">Wildcard Pattern</div>';
1419
+ tmpHTML += '</div>';
1420
+ tmpHTML += '</div>';
1421
+
1422
+ // Topic selection
1423
+ tmpHTML += '<div class="pict-inline-doc-tm-form-group">';
1424
+ tmpHTML += '<label class="pict-inline-doc-tm-form-label">Select Topic</label>';
1425
+ tmpHTML += '<div class="pict-inline-doc-tm-bind-topic-list">';
1426
+
1427
+ for (let i = 0; i < pTopics.length; i++)
1428
+ {
1429
+ let tmpTopic = pTopics[i];
1430
+ tmpHTML += '<div class="pict-inline-doc-tm-bind-topic-option" data-topic-code="' + this._escapeHTML(tmpTopic.TopicCode) + '">';
1431
+ tmpHTML += '<div class="pict-inline-doc-tm-bind-radio"></div>';
1432
+ tmpHTML += '<div class="pict-inline-doc-tm-topic-info">';
1433
+ tmpHTML += '<div class="pict-inline-doc-tm-topic-title">' + this._escapeHTML(tmpTopic.TopicTitle) + '</div>';
1434
+ tmpHTML += '<div class="pict-inline-doc-tm-topic-meta">' + this._escapeHTML(tmpTopic.TopicCode) + '</div>';
1435
+ tmpHTML += '</div>';
1436
+ tmpHTML += '</div>';
1437
+ }
1438
+
1439
+ // Create new option
1440
+ tmpHTML += '<div class="pict-inline-doc-tm-bind-topic-option" data-topic-code="__NEW__">';
1441
+ tmpHTML += '<div class="pict-inline-doc-tm-bind-radio"></div>';
1442
+ tmpHTML += '<div class="pict-inline-doc-tm-topic-info">';
1443
+ tmpHTML += '<div class="pict-inline-doc-tm-topic-title" style="color:#2E7D74;">+ Create New Topic</div>';
1444
+ tmpHTML += '</div>';
1445
+ tmpHTML += '</div>';
1446
+
1447
+ tmpHTML += '</div>';
1448
+ tmpHTML += '</div>';
1449
+
1450
+ return tmpHTML;
1451
+ }
1452
+
1453
+ /**
1454
+ * Wire handlers for the bind-topic-to-route modal.
1455
+ *
1456
+ * @param {HTMLElement} pDialog - The modal dialog element
1457
+ * @param {Array} pTopics - Topic list
1458
+ * @param {string} pCurrentRoute - The current route
1459
+ * @param {Function} fOnTopicSelect - Called with selected topic code
1460
+ * @param {Function} fOnRouteTypeSelect - Called with 'exact' or 'wildcard'
1461
+ */
1462
+ _wireBindHandlers(pDialog, pTopics, pCurrentRoute, fOnTopicSelect, fOnRouteTypeSelect)
1463
+ {
1464
+ // Topic selection
1465
+ let tmpTopicOptions = pDialog.querySelectorAll('.pict-inline-doc-tm-bind-topic-option');
1466
+ for (let i = 0; i < tmpTopicOptions.length; i++)
1467
+ {
1468
+ tmpTopicOptions[i].addEventListener('click', () =>
1469
+ {
1470
+ // Deselect all
1471
+ for (let j = 0; j < tmpTopicOptions.length; j++)
1472
+ {
1473
+ tmpTopicOptions[j].classList.remove('selected');
1474
+ }
1475
+ tmpTopicOptions[i].classList.add('selected');
1476
+ fOnTopicSelect(tmpTopicOptions[i].getAttribute('data-topic-code'));
1477
+ });
1478
+ }
1479
+
1480
+ // Route type selection
1481
+ let tmpRouteTypeBtns = pDialog.querySelectorAll('.pict-inline-doc-tm-bind-route-type-btn');
1482
+ for (let i = 0; i < tmpRouteTypeBtns.length; i++)
1483
+ {
1484
+ tmpRouteTypeBtns[i].addEventListener('click', () =>
1485
+ {
1486
+ for (let j = 0; j < tmpRouteTypeBtns.length; j++)
1487
+ {
1488
+ tmpRouteTypeBtns[j].classList.remove('selected');
1489
+ }
1490
+ tmpRouteTypeBtns[i].classList.add('selected');
1491
+ fOnRouteTypeSelect(tmpRouteTypeBtns[i].getAttribute('data-route-type'));
1492
+ });
1493
+ }
1494
+ }
1495
+
1496
+ // -- Sidebar Picker --
1497
+
1498
+ /**
1499
+ * Show a sidebar document picker modal.
1500
+ *
1501
+ * @param {Function} fOnSelect - Callback receiving the selected path (or null)
1502
+ */
1503
+ _showSidebarPicker(fOnSelect)
1504
+ {
1505
+ let tmpModal = this._getModal();
1506
+ let tmpState = this.pict.AppData.InlineDocumentation;
1507
+
1508
+ if (!tmpModal)
1509
+ {
1510
+ if (typeof fOnSelect === 'function') { fOnSelect(null); }
1511
+ return;
1512
+ }
1513
+
1514
+ let tmpGroups = (tmpState && tmpState.SidebarGroups) ? tmpState.SidebarGroups : [];
1515
+ let tmpContent = this._buildSidebarPickerHTML(tmpGroups);
1516
+
1517
+ tmpModal.show(
1518
+ {
1519
+ title: 'Select Document',
1520
+ content: tmpContent,
1521
+ closeable: true,
1522
+ width: '400px',
1523
+ buttons:
1524
+ [
1525
+ { Hash: 'cancel', Label: 'Cancel' }
1526
+ ],
1527
+ onOpen: (pDialog) =>
1528
+ {
1529
+ let tmpItems = pDialog.querySelectorAll('.pict-inline-doc-tm-sidebar-item');
1530
+ for (let i = 0; i < tmpItems.length; i++)
1531
+ {
1532
+ tmpItems[i].addEventListener('click', () =>
1533
+ {
1534
+ let tmpPath = tmpItems[i].getAttribute('data-path');
1535
+ if (tmpModal.dismissModals)
1536
+ {
1537
+ tmpModal.dismissModals();
1538
+ }
1539
+ if (typeof fOnSelect === 'function') { fOnSelect(tmpPath); }
1540
+ });
1541
+ }
1542
+ }
1543
+ }).then((pResult) =>
1544
+ {
1545
+ if (pResult === 'cancel' || pResult === null)
1546
+ {
1547
+ if (typeof fOnSelect === 'function') { fOnSelect(null); }
1548
+ }
1549
+ });
1550
+ }
1551
+
1552
+ /**
1553
+ * Build the HTML for the sidebar document picker.
1554
+ *
1555
+ * @param {Array} pGroups - SidebarGroups array
1556
+ * @returns {string} HTML content
1557
+ */
1558
+ _buildSidebarPickerHTML(pGroups)
1559
+ {
1560
+ let tmpHTML = '<div class="pict-inline-doc-tm-sidebar-list">';
1561
+ let tmpHasItems = false;
1562
+
1563
+ for (let i = 0; i < pGroups.length; i++)
1564
+ {
1565
+ let tmpGroup = pGroups[i];
1566
+
1567
+ if (tmpGroup.Path)
1568
+ {
1569
+ tmpHasItems = true;
1570
+ tmpHTML += '<div class="pict-inline-doc-tm-sidebar-item" data-path="' + this._escapeHTML(tmpGroup.Path) + '">';
1571
+ tmpHTML += this._escapeHTML(tmpGroup.Name);
1572
+ tmpHTML += '<span class="path">' + this._escapeHTML(tmpGroup.Path) + '</span>';
1573
+ tmpHTML += '</div>';
1574
+ }
1575
+
1576
+ let tmpItems = tmpGroup.Items || [];
1577
+ for (let j = 0; j < tmpItems.length; j++)
1578
+ {
1579
+ if (tmpItems[j].Path)
1580
+ {
1581
+ tmpHasItems = true;
1582
+ tmpHTML += '<div class="pict-inline-doc-tm-sidebar-item" data-path="' + this._escapeHTML(tmpItems[j].Path) + '">';
1583
+ tmpHTML += this._escapeHTML(tmpItems[j].Name);
1584
+ tmpHTML += '<span class="path">' + this._escapeHTML(tmpItems[j].Path) + '</span>';
1585
+ tmpHTML += '</div>';
1586
+ }
1587
+ }
1588
+ }
1589
+
1590
+ if (!tmpHasItems)
1591
+ {
1592
+ tmpHTML += '<div class="pict-inline-doc-tm-empty">No sidebar documents found.</div>';
1593
+ }
1594
+
1595
+ tmpHTML += '</div>';
1596
+ return tmpHTML;
1597
+ }
1598
+
1599
+ // -- Utilities --
1600
+
1601
+ /**
1602
+ * Escape HTML special characters.
1603
+ *
1604
+ * @param {string} pText - Text to escape
1605
+ * @returns {string} Escaped text
1606
+ */
1607
+ _escapeHTML(pText)
1608
+ {
1609
+ if (!pText)
1610
+ {
1611
+ return '';
1612
+ }
1613
+ return pText
1614
+ .replace(/&/g, '&amp;')
1615
+ .replace(/</g, '&lt;')
1616
+ .replace(/>/g, '&gt;')
1617
+ .replace(/"/g, '&quot;');
1618
+ }
1619
+ }
1620
+
1621
+ module.exports = InlineDocumentationTopicManagerView;
1622
+
1623
+ module.exports.default_configuration = _ViewConfiguration;