retold-content-system 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/build-codejar-bundle.js +29 -0
- package/build-codemirror-bundle.js +29 -0
- package/codejar-entry.js +10 -0
- package/codemirror-entry.js +16 -0
- package/content/Dogs.txt.md +2 -0
- package/content/README.md +35 -0
- package/content/_sidebar.md +3 -0
- package/content/_topbar.md +1 -0
- package/content/cover.md +12 -0
- package/content/getting-started.md +73 -0
- package/css/content-system.css +42 -0
- package/css/github.css +118 -0
- package/docs/.nojekyll +0 -0
- package/docs/README.md +24 -0
- package/docs/_sidebar.md +16 -0
- package/docs/_topbar.md +6 -0
- package/docs/cli.md +119 -0
- package/docs/cover.md +16 -0
- package/docs/css/docuserve.css +73 -0
- package/docs/editor-guide.md +137 -0
- package/docs/getting-started.md +73 -0
- package/docs/index.html +39 -0
- package/docs/keyboard-shortcuts.md +40 -0
- package/docs/retold-catalog.json +81 -0
- package/docs/retold-keyword-index.json +19 -0
- package/docs/topics.md +83 -0
- package/html/codejar-bundle.js +16 -0
- package/html/codemirror-bundle.js +29982 -0
- package/html/edit.html +25 -0
- package/html/index.html +25 -0
- package/html/preview.html +19 -0
- package/package.json +70 -0
- package/server.js +43 -0
- package/source/Pict-Application-ContentEditor-Configuration.json +15 -0
- package/source/Pict-Application-ContentEditor.js +1361 -0
- package/source/Pict-Application-ContentReader-Configuration.json +15 -0
- package/source/Pict-Application-ContentReader.js +91 -0
- package/source/Pict-ContentSystem-Bundle.js +21 -0
- package/source/cli/ContentSystem-CLI-Program.js +15 -0
- package/source/cli/ContentSystem-CLI-Run.js +3 -0
- package/source/cli/ContentSystem-Server-Setup.js +405 -0
- package/source/cli/commands/ContentSystem-Command-Serve.js +104 -0
- package/source/providers/Pict-Provider-ContentEditor.js +198 -0
- package/source/views/PictView-Editor-CodeEditor.js +271 -0
- package/source/views/PictView-Editor-Layout.js +1194 -0
- package/source/views/PictView-Editor-MarkdownEditor.js +115 -0
- package/source/views/PictView-Editor-MarkdownReference.js +801 -0
- package/source/views/PictView-Editor-SettingsPanel.js +563 -0
- package/source/views/PictView-Editor-TopBar.js +366 -0
- package/source/views/PictView-Editor-Topics.js +1025 -0
|
@@ -0,0 +1,1025 @@
|
|
|
1
|
+
const libPictView = require('pict-view');
|
|
2
|
+
|
|
3
|
+
const _ViewConfiguration =
|
|
4
|
+
{
|
|
5
|
+
ViewIdentifier: "ContentEditor-Topics",
|
|
6
|
+
|
|
7
|
+
DefaultRenderable: "Topics-Wrap",
|
|
8
|
+
DefaultDestinationAddress: "#ContentEditor-SidebarTopics-Container",
|
|
9
|
+
|
|
10
|
+
AutoRender: false,
|
|
11
|
+
|
|
12
|
+
CSS: /*css*/`
|
|
13
|
+
.topics-container
|
|
14
|
+
{
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
height: 100%;
|
|
18
|
+
font-size: 0.82rem;
|
|
19
|
+
color: #3D3229;
|
|
20
|
+
}
|
|
21
|
+
.topics-header
|
|
22
|
+
{
|
|
23
|
+
display: flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
gap: 6px;
|
|
26
|
+
padding: 8px 10px;
|
|
27
|
+
border-bottom: 1px solid #EDE9E3;
|
|
28
|
+
background: #FAF8F4;
|
|
29
|
+
flex-shrink: 0;
|
|
30
|
+
}
|
|
31
|
+
.topics-header-title
|
|
32
|
+
{
|
|
33
|
+
flex: 1;
|
|
34
|
+
font-weight: 600;
|
|
35
|
+
font-size: 0.78rem;
|
|
36
|
+
color: #5E5549;
|
|
37
|
+
white-space: nowrap;
|
|
38
|
+
overflow: hidden;
|
|
39
|
+
text-overflow: ellipsis;
|
|
40
|
+
}
|
|
41
|
+
.topics-header-btn
|
|
42
|
+
{
|
|
43
|
+
background: transparent;
|
|
44
|
+
border: none;
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
font-size: 0.82rem;
|
|
47
|
+
color: #8A7F72;
|
|
48
|
+
padding: 2px 6px;
|
|
49
|
+
border-radius: 3px;
|
|
50
|
+
line-height: 1;
|
|
51
|
+
}
|
|
52
|
+
.topics-header-btn:hover
|
|
53
|
+
{
|
|
54
|
+
color: #3D3229;
|
|
55
|
+
background: #EDE9E3;
|
|
56
|
+
}
|
|
57
|
+
.topics-list
|
|
58
|
+
{
|
|
59
|
+
flex: 1;
|
|
60
|
+
overflow-y: auto;
|
|
61
|
+
overflow-x: hidden;
|
|
62
|
+
}
|
|
63
|
+
.topics-row
|
|
64
|
+
{
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: flex-start;
|
|
67
|
+
gap: 6px;
|
|
68
|
+
padding: 8px 10px;
|
|
69
|
+
border-bottom: 1px solid #F0EDE8;
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
transition: background 0.1s;
|
|
72
|
+
}
|
|
73
|
+
.topics-row:hover
|
|
74
|
+
{
|
|
75
|
+
background: #F5F0EA;
|
|
76
|
+
}
|
|
77
|
+
.topics-row-info
|
|
78
|
+
{
|
|
79
|
+
flex: 1;
|
|
80
|
+
min-width: 0;
|
|
81
|
+
}
|
|
82
|
+
.topics-row-code
|
|
83
|
+
{
|
|
84
|
+
font-weight: 600;
|
|
85
|
+
font-size: 0.78rem;
|
|
86
|
+
color: #2E7D74;
|
|
87
|
+
white-space: nowrap;
|
|
88
|
+
overflow: hidden;
|
|
89
|
+
text-overflow: ellipsis;
|
|
90
|
+
}
|
|
91
|
+
.topics-row-title
|
|
92
|
+
{
|
|
93
|
+
font-size: 0.72rem;
|
|
94
|
+
color: #5E5549;
|
|
95
|
+
white-space: nowrap;
|
|
96
|
+
overflow: hidden;
|
|
97
|
+
text-overflow: ellipsis;
|
|
98
|
+
margin-top: 1px;
|
|
99
|
+
}
|
|
100
|
+
.topics-row-path
|
|
101
|
+
{
|
|
102
|
+
font-size: 0.68rem;
|
|
103
|
+
color: #8A7F72;
|
|
104
|
+
white-space: nowrap;
|
|
105
|
+
overflow: hidden;
|
|
106
|
+
text-overflow: ellipsis;
|
|
107
|
+
margin-top: 1px;
|
|
108
|
+
}
|
|
109
|
+
.topics-row-actions
|
|
110
|
+
{
|
|
111
|
+
flex-shrink: 0;
|
|
112
|
+
display: flex;
|
|
113
|
+
gap: 2px;
|
|
114
|
+
padding-top: 1px;
|
|
115
|
+
}
|
|
116
|
+
.topics-row-btn
|
|
117
|
+
{
|
|
118
|
+
background: transparent;
|
|
119
|
+
border: none;
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
font-size: 0.72rem;
|
|
122
|
+
color: #8A7F72;
|
|
123
|
+
padding: 2px 4px;
|
|
124
|
+
border-radius: 3px;
|
|
125
|
+
line-height: 1;
|
|
126
|
+
}
|
|
127
|
+
.topics-row-btn:hover
|
|
128
|
+
{
|
|
129
|
+
color: #3D3229;
|
|
130
|
+
background: #EDE9E3;
|
|
131
|
+
}
|
|
132
|
+
.topics-row-btn-delete:hover
|
|
133
|
+
{
|
|
134
|
+
color: #D9534F;
|
|
135
|
+
background: #FDF0EF;
|
|
136
|
+
}
|
|
137
|
+
/* Inline edit form */
|
|
138
|
+
.topics-edit
|
|
139
|
+
{
|
|
140
|
+
padding: 8px 10px;
|
|
141
|
+
border-bottom: 1px solid #DDD6CA;
|
|
142
|
+
background: #FFF9F0;
|
|
143
|
+
}
|
|
144
|
+
.topics-edit-field
|
|
145
|
+
{
|
|
146
|
+
margin-bottom: 6px;
|
|
147
|
+
}
|
|
148
|
+
.topics-edit-label
|
|
149
|
+
{
|
|
150
|
+
display: block;
|
|
151
|
+
font-size: 0.68rem;
|
|
152
|
+
font-weight: 600;
|
|
153
|
+
color: #8A7F72;
|
|
154
|
+
margin-bottom: 2px;
|
|
155
|
+
}
|
|
156
|
+
.topics-edit-input
|
|
157
|
+
{
|
|
158
|
+
display: block;
|
|
159
|
+
width: 100%;
|
|
160
|
+
box-sizing: border-box;
|
|
161
|
+
padding: 4px 6px;
|
|
162
|
+
font-size: 0.78rem;
|
|
163
|
+
border: 1px solid #DDD6CA;
|
|
164
|
+
border-radius: 3px;
|
|
165
|
+
background: #FFF;
|
|
166
|
+
color: #3D3229;
|
|
167
|
+
font-family: inherit;
|
|
168
|
+
}
|
|
169
|
+
.topics-edit-input:focus
|
|
170
|
+
{
|
|
171
|
+
outline: none;
|
|
172
|
+
border-color: #2E7D74;
|
|
173
|
+
}
|
|
174
|
+
.topics-edit-actions
|
|
175
|
+
{
|
|
176
|
+
display: flex;
|
|
177
|
+
gap: 6px;
|
|
178
|
+
margin-top: 8px;
|
|
179
|
+
}
|
|
180
|
+
.topics-edit-save
|
|
181
|
+
{
|
|
182
|
+
background: #2E7D74;
|
|
183
|
+
color: #FFF;
|
|
184
|
+
border: none;
|
|
185
|
+
border-radius: 3px;
|
|
186
|
+
padding: 4px 12px;
|
|
187
|
+
font-size: 0.72rem;
|
|
188
|
+
font-weight: 600;
|
|
189
|
+
cursor: pointer;
|
|
190
|
+
}
|
|
191
|
+
.topics-edit-save:hover
|
|
192
|
+
{
|
|
193
|
+
background: #3A9E92;
|
|
194
|
+
}
|
|
195
|
+
.topics-edit-cancel
|
|
196
|
+
{
|
|
197
|
+
background: transparent;
|
|
198
|
+
color: #5E5549;
|
|
199
|
+
border: 1px solid #DDD6CA;
|
|
200
|
+
border-radius: 3px;
|
|
201
|
+
padding: 4px 12px;
|
|
202
|
+
font-size: 0.72rem;
|
|
203
|
+
font-weight: 600;
|
|
204
|
+
cursor: pointer;
|
|
205
|
+
}
|
|
206
|
+
.topics-edit-cancel:hover
|
|
207
|
+
{
|
|
208
|
+
background: #F0EDE8;
|
|
209
|
+
}
|
|
210
|
+
/* Footer add button */
|
|
211
|
+
.topics-footer
|
|
212
|
+
{
|
|
213
|
+
flex-shrink: 0;
|
|
214
|
+
padding: 8px 10px;
|
|
215
|
+
border-top: 1px solid #EDE9E3;
|
|
216
|
+
background: #FAF8F4;
|
|
217
|
+
}
|
|
218
|
+
.topics-add-btn
|
|
219
|
+
{
|
|
220
|
+
display: block;
|
|
221
|
+
width: 100%;
|
|
222
|
+
padding: 6px 0;
|
|
223
|
+
background: #2E7D74;
|
|
224
|
+
color: #FFF;
|
|
225
|
+
border: none;
|
|
226
|
+
border-radius: 4px;
|
|
227
|
+
font-size: 0.78rem;
|
|
228
|
+
font-weight: 600;
|
|
229
|
+
cursor: pointer;
|
|
230
|
+
text-align: center;
|
|
231
|
+
}
|
|
232
|
+
.topics-add-btn:hover
|
|
233
|
+
{
|
|
234
|
+
background: #3A9E92;
|
|
235
|
+
}
|
|
236
|
+
/* Empty state */
|
|
237
|
+
.topics-empty
|
|
238
|
+
{
|
|
239
|
+
display: flex;
|
|
240
|
+
flex-direction: column;
|
|
241
|
+
align-items: center;
|
|
242
|
+
justify-content: center;
|
|
243
|
+
gap: 12px;
|
|
244
|
+
padding: 32px 16px;
|
|
245
|
+
text-align: center;
|
|
246
|
+
color: #8A7F72;
|
|
247
|
+
font-size: 0.82rem;
|
|
248
|
+
}
|
|
249
|
+
.topics-empty-icon
|
|
250
|
+
{
|
|
251
|
+
font-size: 2rem;
|
|
252
|
+
color: #C4BDB3;
|
|
253
|
+
}
|
|
254
|
+
.topics-empty-btn
|
|
255
|
+
{
|
|
256
|
+
display: inline-block;
|
|
257
|
+
padding: 6px 14px;
|
|
258
|
+
background: #2E7D74;
|
|
259
|
+
color: #FFF;
|
|
260
|
+
border: none;
|
|
261
|
+
border-radius: 4px;
|
|
262
|
+
font-size: 0.78rem;
|
|
263
|
+
font-weight: 600;
|
|
264
|
+
cursor: pointer;
|
|
265
|
+
}
|
|
266
|
+
.topics-empty-btn:hover
|
|
267
|
+
{
|
|
268
|
+
background: #3A9E92;
|
|
269
|
+
}
|
|
270
|
+
.topics-empty-btn-secondary
|
|
271
|
+
{
|
|
272
|
+
background: transparent;
|
|
273
|
+
color: #5E5549;
|
|
274
|
+
border: 1px solid #DDD6CA;
|
|
275
|
+
}
|
|
276
|
+
.topics-empty-btn-secondary:hover
|
|
277
|
+
{
|
|
278
|
+
background: #F0EDE8;
|
|
279
|
+
border-color: #8A7F72;
|
|
280
|
+
}
|
|
281
|
+
`,
|
|
282
|
+
|
|
283
|
+
Templates:
|
|
284
|
+
[
|
|
285
|
+
{
|
|
286
|
+
Hash: "Topics-Container-Template",
|
|
287
|
+
Template: /*html*/`
|
|
288
|
+
<div class="topics-container" id="ContentEditor-Topics-Container">
|
|
289
|
+
<div class="topics-header">
|
|
290
|
+
<span class="topics-header-title" id="ContentEditor-Topics-HeaderTitle">Topics</span>
|
|
291
|
+
<button class="topics-header-btn" title="Close topics file"
|
|
292
|
+
onclick="pict.views['ContentEditor-Topics'].closeTopicsFile()">×</button>
|
|
293
|
+
</div>
|
|
294
|
+
<div class="topics-list" id="ContentEditor-Topics-List"></div>
|
|
295
|
+
<div class="topics-footer" id="ContentEditor-Topics-Footer">
|
|
296
|
+
<button class="topics-add-btn"
|
|
297
|
+
onclick="pict.views['ContentEditor-Topics'].addTopic()">+ Add Topic</button>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
`
|
|
301
|
+
}
|
|
302
|
+
],
|
|
303
|
+
|
|
304
|
+
Renderables:
|
|
305
|
+
[
|
|
306
|
+
{
|
|
307
|
+
RenderableHash: "Topics-Wrap",
|
|
308
|
+
TemplateHash: "Topics-Container-Template",
|
|
309
|
+
DestinationAddress: "#ContentEditor-SidebarTopics-Container"
|
|
310
|
+
}
|
|
311
|
+
]
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Content Editor Topics View
|
|
316
|
+
*
|
|
317
|
+
* Manages .pict_documentation_topics.json files — JSON manifests that
|
|
318
|
+
* map topic codes to help file paths and titles for built-in
|
|
319
|
+
* application documentation.
|
|
320
|
+
*
|
|
321
|
+
* Supports full CRUD on topic entries with inline editing.
|
|
322
|
+
*/
|
|
323
|
+
class ContentEditorTopicsView extends libPictView
|
|
324
|
+
{
|
|
325
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
326
|
+
{
|
|
327
|
+
super(pFable, pOptions, pServiceHash);
|
|
328
|
+
|
|
329
|
+
// The parsed topics object (keyed by TopicCode)
|
|
330
|
+
this._topics = {};
|
|
331
|
+
|
|
332
|
+
// The file path of the currently loaded topics file
|
|
333
|
+
this._topicsFilePath = '';
|
|
334
|
+
|
|
335
|
+
// Whether the view has been rendered
|
|
336
|
+
this._hasRendered = false;
|
|
337
|
+
|
|
338
|
+
// The TopicCode currently being edited (null if none)
|
|
339
|
+
this._editingTopicCode = null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
onAfterRender()
|
|
343
|
+
{
|
|
344
|
+
this._hasRendered = true;
|
|
345
|
+
this.pict.CSSMap.injectCSS();
|
|
346
|
+
|
|
347
|
+
// Check if we should show the empty state or the topic list
|
|
348
|
+
if (!this._topicsFilePath)
|
|
349
|
+
{
|
|
350
|
+
this._showEmptyState();
|
|
351
|
+
}
|
|
352
|
+
else
|
|
353
|
+
{
|
|
354
|
+
this._updateHeaderTitle();
|
|
355
|
+
this.renderTopicList();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Load a topics JSON file from the server.
|
|
361
|
+
*
|
|
362
|
+
* @param {string} pPath - Relative path to the topics JSON file
|
|
363
|
+
* @param {Function} [fCallback] - Optional callback (error)
|
|
364
|
+
*/
|
|
365
|
+
loadTopicsFile(pPath, fCallback)
|
|
366
|
+
{
|
|
367
|
+
let tmpCallback = (typeof (fCallback) === 'function') ? fCallback : () => {};
|
|
368
|
+
let tmpSelf = this;
|
|
369
|
+
|
|
370
|
+
if (!pPath)
|
|
371
|
+
{
|
|
372
|
+
return tmpCallback('No path specified');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
let tmpProvider = this.pict.providers['ContentEditor-Provider'];
|
|
376
|
+
if (!tmpProvider)
|
|
377
|
+
{
|
|
378
|
+
return tmpCallback('Provider not available');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
tmpProvider.loadFile(pPath, (pError, pContent) =>
|
|
382
|
+
{
|
|
383
|
+
if (pError)
|
|
384
|
+
{
|
|
385
|
+
// If the default file doesn't exist, that's OK — show empty state
|
|
386
|
+
tmpSelf._topics = {};
|
|
387
|
+
tmpSelf._topicsFilePath = '';
|
|
388
|
+
if (tmpSelf._hasRendered)
|
|
389
|
+
{
|
|
390
|
+
tmpSelf._showEmptyState();
|
|
391
|
+
}
|
|
392
|
+
return tmpCallback(pError);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
try
|
|
396
|
+
{
|
|
397
|
+
let tmpParsed = JSON.parse(pContent);
|
|
398
|
+
if (typeof (tmpParsed) === 'object' && tmpParsed !== null && !Array.isArray(tmpParsed))
|
|
399
|
+
{
|
|
400
|
+
tmpSelf._topics = tmpParsed;
|
|
401
|
+
}
|
|
402
|
+
else
|
|
403
|
+
{
|
|
404
|
+
tmpSelf._topics = {};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
catch (pParseError)
|
|
408
|
+
{
|
|
409
|
+
tmpSelf._topics = {};
|
|
410
|
+
tmpSelf.log.warn('ContentEditor-Topics: Failed to parse topics JSON: ' + pParseError.message);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
tmpSelf._topicsFilePath = pPath;
|
|
414
|
+
|
|
415
|
+
// Persist the path in settings
|
|
416
|
+
tmpSelf.pict.AppData.ContentEditor.TopicsFilePath = pPath;
|
|
417
|
+
tmpSelf.pict.PictApplication.saveSettings();
|
|
418
|
+
|
|
419
|
+
if (tmpSelf._hasRendered)
|
|
420
|
+
{
|
|
421
|
+
tmpSelf._updateHeaderTitle();
|
|
422
|
+
tmpSelf.renderTopicList();
|
|
423
|
+
tmpSelf._showFooter(true);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return tmpCallback(null);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Save the current topics object back to the server.
|
|
432
|
+
*
|
|
433
|
+
* @param {Function} [fCallback] - Optional callback (error)
|
|
434
|
+
*/
|
|
435
|
+
saveTopicsFile(fCallback)
|
|
436
|
+
{
|
|
437
|
+
let tmpCallback = (typeof (fCallback) === 'function') ? fCallback : () => {};
|
|
438
|
+
|
|
439
|
+
if (!this._topicsFilePath)
|
|
440
|
+
{
|
|
441
|
+
return tmpCallback('No topics file loaded');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
let tmpProvider = this.pict.providers['ContentEditor-Provider'];
|
|
445
|
+
if (!tmpProvider)
|
|
446
|
+
{
|
|
447
|
+
return tmpCallback('Provider not available');
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
let tmpContent = JSON.stringify(this._topics, null, '\t');
|
|
451
|
+
tmpProvider.saveFile(this._topicsFilePath, tmpContent, tmpCallback);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Close the currently loaded topics file.
|
|
456
|
+
*/
|
|
457
|
+
closeTopicsFile()
|
|
458
|
+
{
|
|
459
|
+
this._topics = {};
|
|
460
|
+
this._topicsFilePath = '';
|
|
461
|
+
this._editingTopicCode = null;
|
|
462
|
+
|
|
463
|
+
this.pict.AppData.ContentEditor.TopicsFilePath = '';
|
|
464
|
+
this.pict.PictApplication.saveSettings();
|
|
465
|
+
|
|
466
|
+
if (this._hasRendered)
|
|
467
|
+
{
|
|
468
|
+
this._showEmptyState();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Add a new topic entry.
|
|
474
|
+
*
|
|
475
|
+
* @param {Object} [pTopicData] - Optional pre-filled topic data
|
|
476
|
+
*/
|
|
477
|
+
addTopic(pTopicData)
|
|
478
|
+
{
|
|
479
|
+
if (!this._topicsFilePath)
|
|
480
|
+
{
|
|
481
|
+
// If no file loaded, create the default one
|
|
482
|
+
this._createDefaultTopicsFile(() =>
|
|
483
|
+
{
|
|
484
|
+
this.addTopic(pTopicData);
|
|
485
|
+
});
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
let tmpData = pTopicData || {};
|
|
490
|
+
let tmpCode = tmpData.TopicCode || this._generateUniqueCode('New-Topic');
|
|
491
|
+
|
|
492
|
+
let tmpTopic =
|
|
493
|
+
{
|
|
494
|
+
TopicCode: tmpCode,
|
|
495
|
+
TopicHelpFilePath: tmpData.TopicHelpFilePath || '',
|
|
496
|
+
TopicTitle: tmpData.TopicTitle || ''
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
if (typeof (tmpData.RelevantMarkdownLine) === 'number')
|
|
500
|
+
{
|
|
501
|
+
tmpTopic.RelevantMarkdownLine = tmpData.RelevantMarkdownLine;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
this._topics[tmpCode] = tmpTopic;
|
|
505
|
+
|
|
506
|
+
let tmpSelf = this;
|
|
507
|
+
this.saveTopicsFile(() =>
|
|
508
|
+
{
|
|
509
|
+
tmpSelf.renderTopicList();
|
|
510
|
+
tmpSelf.startEditTopic(tmpCode);
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Remove a topic entry after confirmation.
|
|
516
|
+
*
|
|
517
|
+
* @param {string} pTopicCode - The TopicCode to remove
|
|
518
|
+
*/
|
|
519
|
+
removeTopic(pTopicCode)
|
|
520
|
+
{
|
|
521
|
+
if (!pTopicCode || !this._topics[pTopicCode])
|
|
522
|
+
{
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (!confirm('Remove topic "' + pTopicCode + '"?'))
|
|
527
|
+
{
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
delete this._topics[pTopicCode];
|
|
532
|
+
this._editingTopicCode = null;
|
|
533
|
+
|
|
534
|
+
let tmpSelf = this;
|
|
535
|
+
this.saveTopicsFile(() =>
|
|
536
|
+
{
|
|
537
|
+
tmpSelf.renderTopicList();
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Switch a topic row into inline edit mode.
|
|
543
|
+
*
|
|
544
|
+
* @param {string} pTopicCode - The TopicCode to edit
|
|
545
|
+
*/
|
|
546
|
+
startEditTopic(pTopicCode)
|
|
547
|
+
{
|
|
548
|
+
if (!pTopicCode || !this._topics[pTopicCode])
|
|
549
|
+
{
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
this._editingTopicCode = pTopicCode;
|
|
554
|
+
this.renderTopicList();
|
|
555
|
+
|
|
556
|
+
// Focus the first input field
|
|
557
|
+
let tmpInput = document.getElementById('topics-edit-code');
|
|
558
|
+
if (tmpInput)
|
|
559
|
+
{
|
|
560
|
+
tmpInput.focus();
|
|
561
|
+
tmpInput.select();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Save the inline edit form values back into the topics object.
|
|
567
|
+
*
|
|
568
|
+
* @param {string} pOriginalCode - The original TopicCode being edited
|
|
569
|
+
*/
|
|
570
|
+
saveEditTopic(pOriginalCode)
|
|
571
|
+
{
|
|
572
|
+
if (!pOriginalCode || !this._topics[pOriginalCode])
|
|
573
|
+
{
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
let tmpCodeInput = document.getElementById('topics-edit-code');
|
|
578
|
+
let tmpTitleInput = document.getElementById('topics-edit-title');
|
|
579
|
+
let tmpPathInput = document.getElementById('topics-edit-path');
|
|
580
|
+
let tmpLineInput = document.getElementById('topics-edit-line');
|
|
581
|
+
|
|
582
|
+
if (!tmpCodeInput)
|
|
583
|
+
{
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
let tmpNewCode = tmpCodeInput.value.trim();
|
|
588
|
+
let tmpNewTitle = tmpTitleInput ? tmpTitleInput.value.trim() : '';
|
|
589
|
+
let tmpNewPath = tmpPathInput ? tmpPathInput.value.trim() : '';
|
|
590
|
+
let tmpNewLine = tmpLineInput ? parseInt(tmpLineInput.value, 10) : NaN;
|
|
591
|
+
|
|
592
|
+
// Validate: TopicCode must not be empty
|
|
593
|
+
if (!tmpNewCode)
|
|
594
|
+
{
|
|
595
|
+
tmpCodeInput.style.borderColor = '#D9534F';
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Validate: if code changed, it must be unique
|
|
600
|
+
if (tmpNewCode !== pOriginalCode && this._topics[tmpNewCode])
|
|
601
|
+
{
|
|
602
|
+
tmpCodeInput.style.borderColor = '#D9534F';
|
|
603
|
+
alert('A topic with code "' + tmpNewCode + '" already exists.');
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Remove the old entry if the code changed
|
|
608
|
+
if (tmpNewCode !== pOriginalCode)
|
|
609
|
+
{
|
|
610
|
+
delete this._topics[pOriginalCode];
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
let tmpTopic =
|
|
614
|
+
{
|
|
615
|
+
TopicCode: tmpNewCode,
|
|
616
|
+
TopicHelpFilePath: tmpNewPath,
|
|
617
|
+
TopicTitle: tmpNewTitle
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
if (!isNaN(tmpNewLine) && tmpNewLine > 0)
|
|
621
|
+
{
|
|
622
|
+
tmpTopic.RelevantMarkdownLine = tmpNewLine;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
this._topics[tmpNewCode] = tmpTopic;
|
|
626
|
+
this._editingTopicCode = null;
|
|
627
|
+
|
|
628
|
+
let tmpSelf = this;
|
|
629
|
+
this.saveTopicsFile(() =>
|
|
630
|
+
{
|
|
631
|
+
tmpSelf.renderTopicList();
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Cancel inline editing and re-render the list.
|
|
637
|
+
*/
|
|
638
|
+
cancelEditTopic()
|
|
639
|
+
{
|
|
640
|
+
this._editingTopicCode = null;
|
|
641
|
+
this.renderTopicList();
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Navigate to a topic's file in the editor, scrolling to
|
|
646
|
+
* RelevantMarkdownLine if present.
|
|
647
|
+
*
|
|
648
|
+
* @param {string} pTopicCode - The TopicCode to navigate to
|
|
649
|
+
*/
|
|
650
|
+
navigateToTopic(pTopicCode)
|
|
651
|
+
{
|
|
652
|
+
if (!pTopicCode || !this._topics[pTopicCode])
|
|
653
|
+
{
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
let tmpTopic = this._topics[pTopicCode];
|
|
658
|
+
let tmpFilePath = tmpTopic.TopicHelpFilePath;
|
|
659
|
+
|
|
660
|
+
if (!tmpFilePath)
|
|
661
|
+
{
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
this.pict.PictApplication.navigateToFile(tmpFilePath);
|
|
666
|
+
|
|
667
|
+
// If there's a RelevantMarkdownLine, scroll to it after a brief delay
|
|
668
|
+
// to allow the editor to render
|
|
669
|
+
if (typeof (tmpTopic.RelevantMarkdownLine) === 'number' && tmpTopic.RelevantMarkdownLine > 0)
|
|
670
|
+
{
|
|
671
|
+
let tmpLine = tmpTopic.RelevantMarkdownLine;
|
|
672
|
+
setTimeout(() =>
|
|
673
|
+
{
|
|
674
|
+
let tmpEditorView = this.pict.views['ContentEditor-MarkdownEditor'];
|
|
675
|
+
if (tmpEditorView && tmpEditorView._segmentEditors)
|
|
676
|
+
{
|
|
677
|
+
// Find the segment and line to scroll to
|
|
678
|
+
let tmpRunningLines = 0;
|
|
679
|
+
for (let tmpKey in tmpEditorView._segmentEditors)
|
|
680
|
+
{
|
|
681
|
+
let tmpEditor = tmpEditorView._segmentEditors[tmpKey];
|
|
682
|
+
if (tmpEditor && tmpEditor.state && tmpEditor.state.doc)
|
|
683
|
+
{
|
|
684
|
+
let tmpSegmentLines = tmpEditor.state.doc.lines;
|
|
685
|
+
if (tmpRunningLines + tmpSegmentLines >= tmpLine)
|
|
686
|
+
{
|
|
687
|
+
// This segment contains the target line
|
|
688
|
+
let tmpLocalLine = tmpLine - tmpRunningLines;
|
|
689
|
+
if (tmpLocalLine < 1) tmpLocalLine = 1;
|
|
690
|
+
if (tmpLocalLine > tmpSegmentLines) tmpLocalLine = tmpSegmentLines;
|
|
691
|
+
let tmpLineInfo = tmpEditor.state.doc.line(tmpLocalLine);
|
|
692
|
+
tmpEditor.dispatch({
|
|
693
|
+
selection: { anchor: tmpLineInfo.from },
|
|
694
|
+
scrollIntoView: true
|
|
695
|
+
});
|
|
696
|
+
tmpEditor.focus();
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
tmpRunningLines += tmpSegmentLines;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}, 500);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Rebuild the topic list innerHTML from this._topics.
|
|
709
|
+
*/
|
|
710
|
+
renderTopicList()
|
|
711
|
+
{
|
|
712
|
+
let tmpListEl = document.getElementById('ContentEditor-Topics-List');
|
|
713
|
+
if (!tmpListEl)
|
|
714
|
+
{
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
let tmpKeys = Object.keys(this._topics);
|
|
719
|
+
|
|
720
|
+
if (tmpKeys.length === 0)
|
|
721
|
+
{
|
|
722
|
+
tmpListEl.innerHTML = '<div style="padding:16px;text-align:center;color:#8A7F72;font-size:0.78rem;">No topics yet. Click "+ Add Topic" to create one.</div>';
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
let tmpHTML = '';
|
|
727
|
+
|
|
728
|
+
for (let i = 0; i < tmpKeys.length; i++)
|
|
729
|
+
{
|
|
730
|
+
let tmpCode = tmpKeys[i];
|
|
731
|
+
let tmpTopic = this._topics[tmpCode];
|
|
732
|
+
|
|
733
|
+
if (this._editingTopicCode === tmpCode)
|
|
734
|
+
{
|
|
735
|
+
// Render inline edit form
|
|
736
|
+
tmpHTML += this._buildEditFormHTML(tmpTopic);
|
|
737
|
+
}
|
|
738
|
+
else
|
|
739
|
+
{
|
|
740
|
+
// Render topic row
|
|
741
|
+
tmpHTML += this._buildTopicRowHTML(tmpTopic);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
tmpListEl.innerHTML = tmpHTML;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Build the HTML for a topic row.
|
|
750
|
+
*
|
|
751
|
+
* @param {Object} pTopic - The topic object
|
|
752
|
+
* @returns {string} HTML string
|
|
753
|
+
*/
|
|
754
|
+
_buildTopicRowHTML(pTopic)
|
|
755
|
+
{
|
|
756
|
+
let tmpCode = this._escapeHTML(pTopic.TopicCode || '');
|
|
757
|
+
let tmpTitle = this._escapeHTML(pTopic.TopicTitle || '');
|
|
758
|
+
let tmpPath = this._escapeHTML(pTopic.TopicHelpFilePath || '');
|
|
759
|
+
let tmpLine = (typeof (pTopic.RelevantMarkdownLine) === 'number') ? ' :' + pTopic.RelevantMarkdownLine : '';
|
|
760
|
+
let tmpCodeEscaped = this._escapeAttr(pTopic.TopicCode || '');
|
|
761
|
+
|
|
762
|
+
let tmpHTML = '<div class="topics-row" ondblclick="pict.views[\'ContentEditor-Topics\'].startEditTopic(\'' + tmpCodeEscaped + '\')">';
|
|
763
|
+
tmpHTML += '<div class="topics-row-info">';
|
|
764
|
+
tmpHTML += '<div class="topics-row-code">' + tmpCode + '</div>';
|
|
765
|
+
if (tmpTitle)
|
|
766
|
+
{
|
|
767
|
+
tmpHTML += '<div class="topics-row-title">' + tmpTitle + '</div>';
|
|
768
|
+
}
|
|
769
|
+
if (tmpPath)
|
|
770
|
+
{
|
|
771
|
+
tmpHTML += '<div class="topics-row-path">' + tmpPath + tmpLine + '</div>';
|
|
772
|
+
}
|
|
773
|
+
tmpHTML += '</div>';
|
|
774
|
+
tmpHTML += '<div class="topics-row-actions">';
|
|
775
|
+
tmpHTML += '<button class="topics-row-btn" title="Edit" onclick="event.stopPropagation();pict.views[\'ContentEditor-Topics\'].startEditTopic(\'' + tmpCodeEscaped + '\')">\u270E</button>';
|
|
776
|
+
tmpHTML += '<button class="topics-row-btn topics-row-btn-delete" title="Delete" onclick="event.stopPropagation();pict.views[\'ContentEditor-Topics\'].removeTopic(\'' + tmpCodeEscaped + '\')">\u2716</button>';
|
|
777
|
+
if (tmpPath)
|
|
778
|
+
{
|
|
779
|
+
tmpHTML += '<button class="topics-row-btn" title="Go to file" onclick="event.stopPropagation();pict.views[\'ContentEditor-Topics\'].navigateToTopic(\'' + tmpCodeEscaped + '\')">\u2192</button>';
|
|
780
|
+
}
|
|
781
|
+
tmpHTML += '</div>';
|
|
782
|
+
tmpHTML += '</div>';
|
|
783
|
+
|
|
784
|
+
return tmpHTML;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Build the HTML for an inline edit form.
|
|
789
|
+
*
|
|
790
|
+
* @param {Object} pTopic - The topic object being edited
|
|
791
|
+
* @returns {string} HTML string
|
|
792
|
+
*/
|
|
793
|
+
_buildEditFormHTML(pTopic)
|
|
794
|
+
{
|
|
795
|
+
let tmpCode = this._escapeAttr(pTopic.TopicCode || '');
|
|
796
|
+
let tmpTitle = this._escapeAttr(pTopic.TopicTitle || '');
|
|
797
|
+
let tmpPath = this._escapeAttr(pTopic.TopicHelpFilePath || '');
|
|
798
|
+
let tmpLine = (typeof (pTopic.RelevantMarkdownLine) === 'number') ? pTopic.RelevantMarkdownLine : '';
|
|
799
|
+
let tmpOriginalCode = this._escapeAttr(pTopic.TopicCode || '');
|
|
800
|
+
|
|
801
|
+
let tmpHTML = '<div class="topics-edit">';
|
|
802
|
+
tmpHTML += '<div class="topics-edit-field">';
|
|
803
|
+
tmpHTML += '<label class="topics-edit-label">Topic Code</label>';
|
|
804
|
+
tmpHTML += '<input class="topics-edit-input" id="topics-edit-code" type="text" value="' + tmpCode + '" placeholder="My-Topic-Code">';
|
|
805
|
+
tmpHTML += '</div>';
|
|
806
|
+
tmpHTML += '<div class="topics-edit-field">';
|
|
807
|
+
tmpHTML += '<label class="topics-edit-label">Title</label>';
|
|
808
|
+
tmpHTML += '<input class="topics-edit-input" id="topics-edit-title" type="text" value="' + tmpTitle + '" placeholder="Topic title">';
|
|
809
|
+
tmpHTML += '</div>';
|
|
810
|
+
tmpHTML += '<div class="topics-edit-field">';
|
|
811
|
+
tmpHTML += '<label class="topics-edit-label">Help File Path</label>';
|
|
812
|
+
tmpHTML += '<input class="topics-edit-input" id="topics-edit-path" type="text" value="' + tmpPath + '" placeholder="path/to/file.md">';
|
|
813
|
+
tmpHTML += '</div>';
|
|
814
|
+
tmpHTML += '<div class="topics-edit-field">';
|
|
815
|
+
tmpHTML += '<label class="topics-edit-label">Line Number (optional)</label>';
|
|
816
|
+
tmpHTML += '<input class="topics-edit-input" id="topics-edit-line" type="number" value="' + tmpLine + '" placeholder="e.g. 23" min="1">';
|
|
817
|
+
tmpHTML += '</div>';
|
|
818
|
+
tmpHTML += '<div class="topics-edit-actions">';
|
|
819
|
+
tmpHTML += '<button class="topics-edit-save" onclick="pict.views[\'ContentEditor-Topics\'].saveEditTopic(\'' + tmpOriginalCode + '\')">Save</button>';
|
|
820
|
+
tmpHTML += '<button class="topics-edit-cancel" onclick="pict.views[\'ContentEditor-Topics\'].cancelEditTopic()">Cancel</button>';
|
|
821
|
+
tmpHTML += '</div>';
|
|
822
|
+
tmpHTML += '</div>';
|
|
823
|
+
|
|
824
|
+
return tmpHTML;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Show the empty state (no topics file loaded).
|
|
829
|
+
*/
|
|
830
|
+
_showEmptyState()
|
|
831
|
+
{
|
|
832
|
+
let tmpContainer = document.getElementById('ContentEditor-Topics-Container');
|
|
833
|
+
if (!tmpContainer)
|
|
834
|
+
{
|
|
835
|
+
// If the container doesn't exist yet, just render the whole view
|
|
836
|
+
let tmpDestination = document.getElementById('ContentEditor-SidebarTopics-Container');
|
|
837
|
+
if (tmpDestination)
|
|
838
|
+
{
|
|
839
|
+
tmpDestination.innerHTML = this._buildEmptyStateHTML();
|
|
840
|
+
}
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
tmpContainer.innerHTML = this._buildEmptyStateHTML();
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Build the empty state HTML.
|
|
849
|
+
*
|
|
850
|
+
* @returns {string} HTML string
|
|
851
|
+
*/
|
|
852
|
+
_buildEmptyStateHTML()
|
|
853
|
+
{
|
|
854
|
+
let tmpHTML = '<div class="topics-empty">';
|
|
855
|
+
tmpHTML += '<div class="topics-empty-icon">📑</div>';
|
|
856
|
+
tmpHTML += '<div>No topics file loaded</div>';
|
|
857
|
+
tmpHTML += '<button class="topics-empty-btn" onclick="pict.views[\'ContentEditor-Topics\'].loadDefaultTopicsFile()">Load .pict_documentation_topics.json</button>';
|
|
858
|
+
tmpHTML += '<button class="topics-empty-btn topics-empty-btn-secondary" onclick="pict.views[\'ContentEditor-Topics\'].promptSelectTopicsFile()">Select file...</button>';
|
|
859
|
+
tmpHTML += '</div>';
|
|
860
|
+
return tmpHTML;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Attempt to load the default topics file (.pict_documentation_topics.json).
|
|
865
|
+
* If it doesn't exist, create it.
|
|
866
|
+
*/
|
|
867
|
+
loadDefaultTopicsFile()
|
|
868
|
+
{
|
|
869
|
+
let tmpSelf = this;
|
|
870
|
+
let tmpDefaultPath = '.pict_documentation_topics.json';
|
|
871
|
+
|
|
872
|
+
this.loadTopicsFile(tmpDefaultPath, (pError) =>
|
|
873
|
+
{
|
|
874
|
+
if (pError)
|
|
875
|
+
{
|
|
876
|
+
// File doesn't exist — create it
|
|
877
|
+
tmpSelf._createDefaultTopicsFile();
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Prompt the user for a custom topics file path.
|
|
884
|
+
*/
|
|
885
|
+
promptSelectTopicsFile()
|
|
886
|
+
{
|
|
887
|
+
let tmpPath = prompt('Enter the path to a topics JSON file:', '.pict_documentation_topics.json');
|
|
888
|
+
if (tmpPath && tmpPath.trim())
|
|
889
|
+
{
|
|
890
|
+
let tmpSelf = this;
|
|
891
|
+
this.loadTopicsFile(tmpPath.trim(), (pError) =>
|
|
892
|
+
{
|
|
893
|
+
if (pError)
|
|
894
|
+
{
|
|
895
|
+
// File doesn't exist — offer to create it
|
|
896
|
+
if (confirm('File not found. Create "' + tmpPath.trim() + '"?'))
|
|
897
|
+
{
|
|
898
|
+
tmpSelf._topicsFilePath = tmpPath.trim();
|
|
899
|
+
tmpSelf._topics = {};
|
|
900
|
+
tmpSelf.pict.AppData.ContentEditor.TopicsFilePath = tmpPath.trim();
|
|
901
|
+
tmpSelf.pict.PictApplication.saveSettings();
|
|
902
|
+
tmpSelf.saveTopicsFile(() =>
|
|
903
|
+
{
|
|
904
|
+
if (tmpSelf._hasRendered)
|
|
905
|
+
{
|
|
906
|
+
tmpSelf.render();
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Create the default topics file with empty contents.
|
|
917
|
+
*
|
|
918
|
+
* @param {Function} [fCallback] - Optional callback when done
|
|
919
|
+
*/
|
|
920
|
+
_createDefaultTopicsFile(fCallback)
|
|
921
|
+
{
|
|
922
|
+
let tmpCallback = (typeof (fCallback) === 'function') ? fCallback : () => {};
|
|
923
|
+
let tmpSelf = this;
|
|
924
|
+
let tmpDefaultPath = '.pict_documentation_topics.json';
|
|
925
|
+
|
|
926
|
+
this._topicsFilePath = tmpDefaultPath;
|
|
927
|
+
this._topics = {};
|
|
928
|
+
|
|
929
|
+
this.pict.AppData.ContentEditor.TopicsFilePath = tmpDefaultPath;
|
|
930
|
+
this.pict.PictApplication.saveSettings();
|
|
931
|
+
|
|
932
|
+
this.saveTopicsFile(() =>
|
|
933
|
+
{
|
|
934
|
+
if (tmpSelf._hasRendered)
|
|
935
|
+
{
|
|
936
|
+
tmpSelf.render();
|
|
937
|
+
}
|
|
938
|
+
tmpCallback();
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Update the header title bar with the current file name.
|
|
944
|
+
*/
|
|
945
|
+
_updateHeaderTitle()
|
|
946
|
+
{
|
|
947
|
+
let tmpTitle = document.getElementById('ContentEditor-Topics-HeaderTitle');
|
|
948
|
+
if (tmpTitle)
|
|
949
|
+
{
|
|
950
|
+
let tmpFileName = this._topicsFilePath.replace(/^.*\//, '');
|
|
951
|
+
tmpTitle.textContent = tmpFileName || 'Topics';
|
|
952
|
+
tmpTitle.title = this._topicsFilePath;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Show or hide the footer (add button area).
|
|
958
|
+
*
|
|
959
|
+
* @param {boolean} pShow
|
|
960
|
+
*/
|
|
961
|
+
_showFooter(pShow)
|
|
962
|
+
{
|
|
963
|
+
let tmpFooter = document.getElementById('ContentEditor-Topics-Footer');
|
|
964
|
+
if (tmpFooter)
|
|
965
|
+
{
|
|
966
|
+
tmpFooter.style.display = pShow ? '' : 'none';
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Generate a unique topic code by appending a suffix if needed.
|
|
972
|
+
*
|
|
973
|
+
* @param {string} pBase - The base code
|
|
974
|
+
* @returns {string} A unique code
|
|
975
|
+
*/
|
|
976
|
+
_generateUniqueCode(pBase)
|
|
977
|
+
{
|
|
978
|
+
if (!this._topics[pBase])
|
|
979
|
+
{
|
|
980
|
+
return pBase;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
let tmpCounter = 2;
|
|
984
|
+
while (this._topics[pBase + '-' + tmpCounter])
|
|
985
|
+
{
|
|
986
|
+
tmpCounter++;
|
|
987
|
+
}
|
|
988
|
+
return pBase + '-' + tmpCounter;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* HTML-escape a string for safe insertion.
|
|
993
|
+
*
|
|
994
|
+
* @param {string} pStr
|
|
995
|
+
* @returns {string}
|
|
996
|
+
*/
|
|
997
|
+
_escapeHTML(pStr)
|
|
998
|
+
{
|
|
999
|
+
return String(pStr)
|
|
1000
|
+
.replace(/&/g, '&')
|
|
1001
|
+
.replace(/</g, '<')
|
|
1002
|
+
.replace(/>/g, '>')
|
|
1003
|
+
.replace(/"/g, '"');
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Escape a string for use in an HTML attribute value.
|
|
1008
|
+
*
|
|
1009
|
+
* @param {string} pStr
|
|
1010
|
+
* @returns {string}
|
|
1011
|
+
*/
|
|
1012
|
+
_escapeAttr(pStr)
|
|
1013
|
+
{
|
|
1014
|
+
return String(pStr)
|
|
1015
|
+
.replace(/&/g, '&')
|
|
1016
|
+
.replace(/'/g, ''')
|
|
1017
|
+
.replace(/"/g, '"')
|
|
1018
|
+
.replace(/</g, '<')
|
|
1019
|
+
.replace(/>/g, '>');
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
module.exports = ContentEditorTopicsView;
|
|
1024
|
+
|
|
1025
|
+
module.exports.default_configuration = _ViewConfiguration;
|