retold-content-system 1.0.15 → 1.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "retold-content-system",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "Retold Content System - Markdown content viewer and editor",
5
5
  "main": "source/Pict-ContentSystem-Bundle.js",
6
6
  "bin": {
@@ -31,17 +31,19 @@
31
31
  "ultravisor-beacon": "^0.0.11"
32
32
  },
33
33
  "dependencies": {
34
- "fable": "^3.1.68",
34
+ "fable": "^3.1.70",
35
35
  "orator": "^6.0.4",
36
36
  "orator-serviceserver-restify": "^2.0.10",
37
37
  "pict": "^1.0.363",
38
38
  "pict-application": "^1.0.33",
39
39
  "pict-provider": "^1.0.12",
40
+ "pict-provider-vocabulary": "^0.0.6",
40
41
  "pict-section-code": "^1.0.4",
41
- "pict-provider-vocabulary": "file:../../pict/pict-provider-vocabulary",
42
- "pict-section-content": "^0.1.4",
42
+ "pict-section-content": "^0.1.5",
43
43
  "pict-section-filebrowser": "^0.0.2",
44
- "pict-section-markdowneditor": "^1.0.9",
44
+ "pict-section-inlinedocumentation": "^0.0.5",
45
+ "pict-section-markdowneditor": "^1.0.10",
46
+ "pict-section-modal": "^0.0.4",
45
47
  "pict-service-commandlineutility": "^1.0.19",
46
48
  "pict-view": "^1.0.68"
47
49
  },
@@ -6,6 +6,15 @@ const libPictSectionFileBrowser = require('pict-section-filebrowser');
6
6
  // Vocabulary auto-linking + popover system (shared with retold-labs)
7
7
  const libPictProviderVocabulary = require('pict-provider-vocabulary');
8
8
 
9
+ // Inline documentation (right-side panel)
10
+ const libPictSectionInlineDocumentation = require('pict-section-inlinedocumentation');
11
+
12
+ // Modal system (panels, dialogs, tooltips, toasts)
13
+ const libPictSectionModal = require('pict-section-modal');
14
+
15
+ // Content rendering
16
+ const libPictSectionContent = require('pict-section-content');
17
+
9
18
  // Provider
10
19
  const libContentEditorProvider = require('./providers/Pict-Provider-ContentEditor.js');
11
20
 
@@ -44,16 +53,44 @@ class ContentEditorApplication extends libPictApplication
44
53
  libPictProviderVocabulary);
45
54
 
46
55
  // Register the vocabulary management view from the provider.
47
- // Mount it at the vocabulary panel container (added to the
48
- // layout by the host app see the layout view below).
56
+ // Mount it at the vocabulary panel container in the sidebar.
57
+ // The onEditTerm callback opens the term file in the main
58
+ // markdown editor instead of rendering an inline textarea.
59
+ let tmpPictRef = this.pict;
49
60
  this.pict.addView('ContentEditor-Vocabulary',
50
61
  Object.assign({}, libPictProviderVocabulary.VocabularyManagerView.default_configuration,
51
62
  {
52
63
  DefaultDestinationAddress: '#ContentEditor-Vocabulary-Container',
53
- VocabularyRoute: '#/vocabulary'
64
+ VocabularyRoute: '#/vocabulary',
65
+ VocabularyFolderPath: 'vocabulary/',
66
+ onEditTerm: function (pSlug, pFilePath)
67
+ {
68
+ // Open the vocabulary term file in the main editor
69
+ if (tmpPictRef && tmpPictRef.PictApplication && typeof tmpPictRef.PictApplication.navigateToFile === 'function')
70
+ {
71
+ tmpPictRef.PictApplication.navigateToFile(pFilePath);
72
+ }
73
+ }
54
74
  }),
55
75
  libPictProviderVocabulary.VocabularyManagerView);
56
76
 
77
+ // Inline documentation provider — powers the right-side
78
+ // documentation panel. Renders markdown docs from the content
79
+ // tree's docs/ folder with topic navigation, editing, and
80
+ // vocabulary auto-linking.
81
+ this.pict.addProvider('Pict-InlineDocumentation',
82
+ libPictSectionInlineDocumentation.default_configuration,
83
+ libPictSectionInlineDocumentation);
84
+
85
+ // Content rendering provider (for markdown parsing with
86
+ // vocabulary resolver in the documentation panel).
87
+ this.pict.addProvider('Pict-Content',
88
+ libPictSectionContent.PictContentProvider.default_configuration,
89
+ libPictSectionContent.PictContentProvider);
90
+
91
+ // Register the modal system (panels, dialogs, tooltips, toasts)
92
+ this.pict.addView('Pict-Section-Modal', libPictSectionModal.default_configuration, libPictSectionModal);
93
+
57
94
  // Register views
58
95
  this.pict.addView('ContentEditor-Layout', libViewLayout.default_configuration, libViewLayout);
59
96
  this.pict.addView('ContentEditor-TopBar', libViewTopBar.default_configuration, libViewTopBar);
@@ -118,6 +155,17 @@ class ContentEditorApplication extends libPictApplication
118
155
  if (typeof (window) !== 'undefined')
119
156
  {
120
157
  window.pict = this.pict;
158
+
159
+ // Warn the user before closing the tab/window with unsaved changes
160
+ let tmpPictRef = this.pict;
161
+ window.addEventListener('beforeunload', function (pEvent)
162
+ {
163
+ if (tmpPictRef.AppData.ContentEditor && tmpPictRef.AppData.ContentEditor.IsDirty)
164
+ {
165
+ pEvent.preventDefault();
166
+ pEvent.returnValue = '';
167
+ }
168
+ });
121
169
  }
122
170
 
123
171
  // Initialize application state
@@ -225,6 +273,76 @@ class ContentEditorApplication extends libPictApplication
225
273
  tmpVocabProvider.loadFromURL('/api/vocabulary/index');
226
274
  }
227
275
 
276
+ // Initialize the inline documentation panel. It reads from
277
+ // the content tree's /content/ path via the same server.
278
+ // The panel starts collapsed — the user expands it via the
279
+ // edge tab or the Docs button in the top bar.
280
+ // Initialize the inline documentation panel, then attach the
281
+ // resizable/collapsible panel behavior once the layout has
282
+ // rendered into the container. The panel() call must happen
283
+ // AFTER the async render so the edge element isn't wiped out.
284
+ let tmpDocProvider = this.pict.providers && this.pict.providers['Pict-InlineDocumentation'];
285
+ if (tmpDocProvider && typeof tmpDocProvider.initializeDocumentation === 'function')
286
+ {
287
+ let tmpSelfApp = this;
288
+ tmpDocProvider.initializeDocumentation(
289
+ {
290
+ DocsBaseURL: '/content/',
291
+ ContainerAddress: '#ContentEditor-Documentation-Panel',
292
+ SearchIndexURL: '/content/retold-keyword-index.json',
293
+ ExternalDocBaseURL: 'https://stevenvelozo.github.io/retold/#/doc/',
294
+ EditEnabled: false,
295
+ TopicManagerEnabled: false
296
+ },
297
+ function ()
298
+ {
299
+ // Layout has rendered — attach resize + collapse.
300
+ // Full persistence: width and collapsed state both
301
+ // survive across page loads via localStorage.
302
+ let tmpModal = tmpSelfApp.pict.views['Pict-Section-Modal'];
303
+ if (tmpModal && typeof tmpModal.panel === 'function')
304
+ {
305
+ tmpSelfApp._docPanel = tmpModal.panel('#ContentEditor-Documentation-Panel',
306
+ {
307
+ position: 'right',
308
+ width: 340,
309
+ minWidth: 260,
310
+ maxWidth: 600,
311
+ collapsible: true,
312
+ collapsed: true,
313
+ persist: true,
314
+ persistKey: 'ContentEditor-DocPanel'
315
+ });
316
+ }
317
+
318
+ // Restore the last-viewed document from localStorage,
319
+ // or default to README.md. This ensures the panel
320
+ // shows the right content whether it starts collapsed
321
+ // or expanded from persisted state.
322
+ let tmpInlineDoc = tmpSelfApp.pict.providers['Pict-InlineDocumentation'];
323
+ if (tmpInlineDoc && typeof tmpInlineDoc.loadDocument === 'function')
324
+ {
325
+ let tmpLastDoc = 'README.md';
326
+ try
327
+ {
328
+ let tmpStored = localStorage.getItem('ContentEditor-DocPanel-LastDoc');
329
+ if (tmpStored) tmpLastDoc = tmpStored;
330
+ }
331
+ catch (e) { /* ignore */ }
332
+ tmpInlineDoc.loadDocument(tmpLastDoc);
333
+
334
+ // Persist the current document path on each navigation
335
+ let tmpOrigLoad = tmpInlineDoc.loadDocument.bind(tmpInlineDoc);
336
+ tmpInlineDoc.loadDocument = function (pPath, fCb)
337
+ {
338
+ try { localStorage.setItem('ContentEditor-DocPanel-LastDoc', pPath); }
339
+ catch (e) { /* ignore */ }
340
+ return tmpOrigLoad(pPath, fCb);
341
+ };
342
+ }
343
+ });
344
+ }
345
+
228
346
  return super.onAfterInitializeAsync(fCallback);
229
347
  }
230
348
 
@@ -488,7 +606,7 @@ class ContentEditorApplication extends libPictApplication
488
606
  */
489
607
  loadMediaPreview(pMediaType, pContentURL, pFileName)
490
608
  {
491
- let tmpContainer = document.getElementById('ContentEditor-MediaPreviewPlaceholder');
609
+ let tmpContainer = this.pict.ContentAssignment.getElement('#ContentEditor-MediaPreviewPlaceholder')[0];
492
610
  if (!tmpContainer)
493
611
  {
494
612
  return;
@@ -704,6 +822,37 @@ class ContentEditorApplication extends libPictApplication
704
822
  return;
705
823
  }
706
824
 
825
+ // Guard: if the current file has unsaved changes, confirm
826
+ // before navigating away. This catches all entry points —
827
+ // file browser clicks, vocabulary edits, topic navigation,
828
+ // hash changes, and new file creation.
829
+ if (this.pict.AppData.ContentEditor.IsDirty)
830
+ {
831
+ let tmpSelf = this;
832
+ let tmpModal = this.pict.views['Pict-Section-Modal'];
833
+ if (tmpModal && typeof tmpModal.confirm === 'function')
834
+ {
835
+ tmpModal.confirm(
836
+ 'You have unsaved changes to ' + this.pict.AppData.ContentEditor.CurrentFile + '. Discard and open a different file?',
837
+ { title: 'Unsaved Changes', confirmLabel: 'Discard', dangerous: true })
838
+ .then(function (pConfirmed)
839
+ {
840
+ if (pConfirmed)
841
+ {
842
+ tmpSelf.pict.AppData.ContentEditor.IsDirty = false;
843
+ tmpSelf.navigateToFile(pFilePath);
844
+ }
845
+ });
846
+ return;
847
+ }
848
+ // Fallback: native confirm
849
+ if (typeof confirm !== 'undefined' && !confirm('You have unsaved changes. Discard and open a different file?'))
850
+ {
851
+ return;
852
+ }
853
+ this.pict.AppData.ContentEditor.IsDirty = false;
854
+ }
855
+
707
856
  let tmpSelf = this;
708
857
 
709
858
  // Determine which editor to use before fetching content
@@ -981,7 +1130,7 @@ class ContentEditorApplication extends libPictApplication
981
1130
  */
982
1131
  _showCloseConfirmation()
983
1132
  {
984
- let tmpOverlay = document.getElementById('ContentEditor-ConfirmOverlay');
1133
+ let tmpOverlay = this.pict.ContentAssignment.getElement('#ContentEditor-ConfirmOverlay')[0];
985
1134
  if (tmpOverlay)
986
1135
  {
987
1136
  tmpOverlay.classList.add('open');
@@ -1015,7 +1164,7 @@ class ContentEditorApplication extends libPictApplication
1015
1164
  */
1016
1165
  _hideCloseConfirmation()
1017
1166
  {
1018
- let tmpOverlay = document.getElementById('ContentEditor-ConfirmOverlay');
1167
+ let tmpOverlay = this.pict.ContentAssignment.getElement('#ContentEditor-ConfirmOverlay')[0];
1019
1168
  if (tmpOverlay)
1020
1169
  {
1021
1170
  tmpOverlay.classList.remove('open');
@@ -1355,7 +1504,7 @@ class ContentEditorApplication extends libPictApplication
1355
1504
  */
1356
1505
  updateStats()
1357
1506
  {
1358
- let tmpStatsEl = document.getElementById('ContentEditor-Stats');
1507
+ let tmpStatsEl = this.pict.ContentAssignment.getElement('#ContentEditor-Stats')[0];
1359
1508
  if (!tmpStatsEl)
1360
1509
  {
1361
1510
  return;
@@ -466,6 +466,94 @@ function setupContentSystemServer(pOptions, fCallback)
466
466
  return fNext();
467
467
  });
468
468
 
469
+ // --- GET /api/vocabulary/term/:slug ---
470
+ // Read a single vocabulary term's markdown body.
471
+ tmpServiceServer.get('/api/vocabulary/term/:slug',
472
+ (pRequest, pResponse, fNext) =>
473
+ {
474
+ try
475
+ {
476
+ let tmpSlug = sanitizePath(pRequest.params.slug);
477
+ if (!tmpSlug)
478
+ {
479
+ pResponse.send(400, { Error: 'Invalid slug' });
480
+ return fNext();
481
+ }
482
+ let tmpFile = libPath.join(tmpContentPath, 'vocabulary', tmpSlug + '.md');
483
+ if (!libFs.existsSync(tmpFile))
484
+ {
485
+ pResponse.send(404, { Error: 'Term not found: ' + tmpSlug });
486
+ return fNext();
487
+ }
488
+ let tmpBody = libFs.readFileSync(tmpFile, 'utf8');
489
+ pResponse.send({ Slug: tmpSlug, Body: tmpBody });
490
+ }
491
+ catch (pError)
492
+ {
493
+ pResponse.send(500, { Error: pError.message });
494
+ }
495
+ return fNext();
496
+ });
497
+
498
+ // --- PUT /api/vocabulary/term/:slug ---
499
+ // Create or update a vocabulary term.
500
+ tmpServiceServer.put('/api/vocabulary/term/:slug',
501
+ (pRequest, pResponse, fNext) =>
502
+ {
503
+ try
504
+ {
505
+ let tmpSlug = sanitizePath(pRequest.params.slug);
506
+ if (!tmpSlug)
507
+ {
508
+ pResponse.send(400, { Error: 'Invalid slug' });
509
+ return fNext();
510
+ }
511
+ let tmpVocabDir = libPath.join(tmpContentPath, 'vocabulary');
512
+ if (!libFs.existsSync(tmpVocabDir))
513
+ {
514
+ libFs.mkdirSync(tmpVocabDir, { recursive: true });
515
+ }
516
+ let tmpBody = (pRequest.body && pRequest.body.body) || '';
517
+ let tmpFile = libPath.join(tmpVocabDir, tmpSlug + '.md');
518
+ libFs.writeFileSync(tmpFile, tmpBody, 'utf8');
519
+ pResponse.send({ Success: true, Slug: tmpSlug });
520
+ }
521
+ catch (pError)
522
+ {
523
+ pResponse.send(500, { Error: pError.message });
524
+ }
525
+ return fNext();
526
+ });
527
+
528
+ // --- DELETE /api/vocabulary/term/:slug ---
529
+ // Delete a vocabulary term's markdown file.
530
+ tmpServiceServer.del('/api/vocabulary/term/:slug',
531
+ (pRequest, pResponse, fNext) =>
532
+ {
533
+ try
534
+ {
535
+ let tmpSlug = sanitizePath(pRequest.params.slug);
536
+ if (!tmpSlug)
537
+ {
538
+ pResponse.send(400, { Error: 'Invalid slug' });
539
+ return fNext();
540
+ }
541
+ let tmpFile = libPath.join(tmpContentPath, 'vocabulary', tmpSlug + '.md');
542
+ if (!libFs.existsSync(tmpFile))
543
+ {
544
+ pResponse.send(404, { Error: 'Term not found: ' + tmpSlug });
545
+ return fNext();
546
+ }
547
+ libFs.unlinkSync(tmpFile);
548
+ pResponse.send({ Success: true, Slug: tmpSlug });
549
+ }
550
+ catch (pError)
551
+ {
552
+ pResponse.send(500, { Error: pError.message });
553
+ }
554
+ return fNext();
555
+ });
556
+
469
557
  // Serve content files (markdown, images, etc.) at /content/
470
558
  tmpOrator.addStaticRoute(`${tmpContentPath}/`, 'index.html', '/content/*', '/content/');
471
559
 
@@ -29,7 +29,7 @@ class ContentEditorProvider extends libPictProvider
29
29
  return tmpCallback('No file path specified', '');
30
30
  }
31
31
 
32
- fetch('/api/content/read/' + encodeURIComponent(pFilePath))
32
+ fetch('/api/content/read/' + pFilePath.split('/').map(encodeURIComponent).join('/'))
33
33
  .then((pResponse) =>
34
34
  {
35
35
  if (!pResponse.ok)
@@ -69,7 +69,7 @@ class ContentEditorProvider extends libPictProvider
69
69
  return tmpCallback('No file path specified');
70
70
  }
71
71
 
72
- fetch('/api/content/save/' + encodeURIComponent(pFilePath),
72
+ fetch('/api/content/save/' + pFilePath.split('/').map(encodeURIComponent).join('/'),
73
73
  {
74
74
  method: 'PUT',
75
75
  headers: { 'Content-Type': 'application/json' },