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,1361 @@
|
|
|
1
|
+
const libPictApplication = require('pict-application');
|
|
2
|
+
|
|
3
|
+
// File browser
|
|
4
|
+
const libPictSectionFileBrowser = require('pict-section-filebrowser');
|
|
5
|
+
|
|
6
|
+
// Provider
|
|
7
|
+
const libContentEditorProvider = require('./providers/Pict-Provider-ContentEditor.js');
|
|
8
|
+
|
|
9
|
+
// Views
|
|
10
|
+
const libViewLayout = require('./views/PictView-Editor-Layout.js');
|
|
11
|
+
const libViewTopBar = require('./views/PictView-Editor-TopBar.js');
|
|
12
|
+
const libViewMarkdownEditor = require('./views/PictView-Editor-MarkdownEditor.js');
|
|
13
|
+
const libViewCodeEditor = require('./views/PictView-Editor-CodeEditor.js');
|
|
14
|
+
const libViewSettingsPanel = require('./views/PictView-Editor-SettingsPanel.js');
|
|
15
|
+
const libViewMarkdownReference = require('./views/PictView-Editor-MarkdownReference.js');
|
|
16
|
+
const libViewTopics = require('./views/PictView-Editor-Topics.js');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Content Editor Application
|
|
20
|
+
*
|
|
21
|
+
* A Pict application for editing markdown files served by the
|
|
22
|
+
* retold-content-system Orator server. Uses pict-section-markdowneditor
|
|
23
|
+
* for the editing experience and pict-section-filebrowser for file browsing.
|
|
24
|
+
*/
|
|
25
|
+
class ContentEditorApplication extends libPictApplication
|
|
26
|
+
{
|
|
27
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
28
|
+
{
|
|
29
|
+
super(pFable, pOptions, pServiceHash);
|
|
30
|
+
|
|
31
|
+
// Register the content editor provider
|
|
32
|
+
this.pict.addProvider('ContentEditor-Provider', libContentEditorProvider.default_configuration, libContentEditorProvider);
|
|
33
|
+
|
|
34
|
+
// Register views
|
|
35
|
+
this.pict.addView('ContentEditor-Layout', libViewLayout.default_configuration, libViewLayout);
|
|
36
|
+
this.pict.addView('ContentEditor-TopBar', libViewTopBar.default_configuration, libViewTopBar);
|
|
37
|
+
this.pict.addView('ContentEditor-MarkdownEditor', libViewMarkdownEditor.default_configuration, libViewMarkdownEditor);
|
|
38
|
+
this.pict.addView('ContentEditor-CodeEditor', libViewCodeEditor.default_configuration, libViewCodeEditor);
|
|
39
|
+
this.pict.addView('ContentEditor-SettingsPanel', libViewSettingsPanel.default_configuration, libViewSettingsPanel);
|
|
40
|
+
this.pict.addView('ContentEditor-MarkdownReference', libViewMarkdownReference.default_configuration, libViewMarkdownReference);
|
|
41
|
+
this.pict.addView('ContentEditor-Topics', libViewTopics.default_configuration, libViewTopics);
|
|
42
|
+
|
|
43
|
+
// Register the file browser -- override destination and layout for sidebar use
|
|
44
|
+
let tmpFileBrowserConfig = JSON.parse(JSON.stringify(libPictSectionFileBrowser.default_configuration));
|
|
45
|
+
tmpFileBrowserConfig.DefaultDestinationAddress = '#ContentEditor-Sidebar-Container';
|
|
46
|
+
tmpFileBrowserConfig.DefaultState.Layout = 'list-only';
|
|
47
|
+
this.pict.addView('Pict-FileBrowser', tmpFileBrowserConfig, libPictSectionFileBrowser);
|
|
48
|
+
|
|
49
|
+
// Register the list detail sub-view for the file list pane
|
|
50
|
+
this.pict.addView('Pict-FileBrowser-ListDetail',
|
|
51
|
+
libPictSectionFileBrowser.PictViewListDetail.default_configuration,
|
|
52
|
+
libPictSectionFileBrowser.PictViewListDetail);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
onAfterInitializeAsync(fCallback)
|
|
56
|
+
{
|
|
57
|
+
// Expose the pict instance as window.pict for inline onclick handlers
|
|
58
|
+
// (pict-section-filebrowser templates reference pict.views[...])
|
|
59
|
+
if (typeof (window) !== 'undefined')
|
|
60
|
+
{
|
|
61
|
+
window.pict = this.pict;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Initialize application state
|
|
65
|
+
this.pict.AppData.ContentEditor =
|
|
66
|
+
{
|
|
67
|
+
CurrentFile: '',
|
|
68
|
+
ActiveEditor: 'markdown', // 'markdown' or 'code'
|
|
69
|
+
IsDirty: false,
|
|
70
|
+
IsSaving: false,
|
|
71
|
+
IsLoading: false,
|
|
72
|
+
Files: [],
|
|
73
|
+
Document:
|
|
74
|
+
{
|
|
75
|
+
Segments: [{ Content: '' }]
|
|
76
|
+
},
|
|
77
|
+
CodeContent: '',
|
|
78
|
+
SaveStatus: '',
|
|
79
|
+
SaveStatusClass: '',
|
|
80
|
+
|
|
81
|
+
// Settings
|
|
82
|
+
AutoSegmentMarkdown: false,
|
|
83
|
+
AutoSegmentDepth: 1,
|
|
84
|
+
AutoContentPreview: false,
|
|
85
|
+
MarkdownEditingControls: true,
|
|
86
|
+
MarkdownWordWrap: true,
|
|
87
|
+
CodeWordWrap: false,
|
|
88
|
+
SidebarCollapsed: false,
|
|
89
|
+
SidebarWidth: 250,
|
|
90
|
+
AutoPreviewImages: true,
|
|
91
|
+
AutoPreviewVideo: false,
|
|
92
|
+
AutoPreviewAudio: false,
|
|
93
|
+
ShowHiddenFiles: false,
|
|
94
|
+
TopicsFilePath: '.pict_documentation_topics.json'
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Restore persisted settings from localStorage
|
|
98
|
+
this._loadSettings();
|
|
99
|
+
|
|
100
|
+
// Render the layout shell
|
|
101
|
+
this.pict.views['ContentEditor-Layout'].render();
|
|
102
|
+
|
|
103
|
+
// Wire up file selection from the file browser
|
|
104
|
+
let tmpSelf = this;
|
|
105
|
+
let tmpListProvider = this.pict.providers['Pict-FileBrowser-List'];
|
|
106
|
+
if (tmpListProvider)
|
|
107
|
+
{
|
|
108
|
+
let tmpOriginalSelectFile = tmpListProvider.selectFile.bind(tmpListProvider);
|
|
109
|
+
tmpListProvider.selectFile = function (pFileEntry)
|
|
110
|
+
{
|
|
111
|
+
tmpOriginalSelectFile(pFileEntry);
|
|
112
|
+
if (pFileEntry && pFileEntry.Type === 'file')
|
|
113
|
+
{
|
|
114
|
+
tmpSelf.navigateToFile(pFileEntry.Path);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Wire up folder navigation to fetch subfolder contents from the server
|
|
120
|
+
let tmpBrowseProvider = this.pict.providers['Pict-FileBrowser-Browse'];
|
|
121
|
+
if (tmpBrowseProvider)
|
|
122
|
+
{
|
|
123
|
+
let tmpOriginalNavigate = tmpBrowseProvider.navigateToFolder.bind(tmpBrowseProvider);
|
|
124
|
+
tmpBrowseProvider.navigateToFolder = function (pPath)
|
|
125
|
+
{
|
|
126
|
+
// Update the CurrentLocation state (breadcrumb, etc.)
|
|
127
|
+
tmpOriginalNavigate(pPath);
|
|
128
|
+
// Fetch the new folder's contents from the server
|
|
129
|
+
tmpSelf.loadFileList(pPath);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Sync the hidden files setting to the server before loading files
|
|
134
|
+
this.syncHiddenFilesSetting(() =>
|
|
135
|
+
{
|
|
136
|
+
// Load the file list into the file browser
|
|
137
|
+
tmpSelf.loadFileList(null, () =>
|
|
138
|
+
{
|
|
139
|
+
// Check if there is a hash route to load
|
|
140
|
+
tmpSelf.resolveHash();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Silently attempt to load the topics file
|
|
145
|
+
let tmpTopicsPath = this.pict.AppData.ContentEditor.TopicsFilePath;
|
|
146
|
+
if (tmpTopicsPath)
|
|
147
|
+
{
|
|
148
|
+
let tmpTopicsView = this.pict.views['ContentEditor-Topics'];
|
|
149
|
+
if (tmpTopicsView)
|
|
150
|
+
{
|
|
151
|
+
tmpTopicsView.loadTopicsFile(tmpTopicsPath, () =>
|
|
152
|
+
{
|
|
153
|
+
// Silently ignore errors — the file may not exist yet
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return super.onAfterInitializeAsync(fCallback);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Push the ShowHiddenFiles setting to the server so the file
|
|
163
|
+
* browser API includes or excludes dotfiles accordingly.
|
|
164
|
+
*
|
|
165
|
+
* @param {Function} [fCallback] - Optional callback when done
|
|
166
|
+
*/
|
|
167
|
+
syncHiddenFilesSetting(fCallback)
|
|
168
|
+
{
|
|
169
|
+
let tmpCallback = (typeof (fCallback) === 'function') ? fCallback : () => {};
|
|
170
|
+
let tmpShow = this.pict.AppData.ContentEditor.ShowHiddenFiles;
|
|
171
|
+
|
|
172
|
+
fetch('/api/filebrowser/settings',
|
|
173
|
+
{
|
|
174
|
+
method: 'PUT',
|
|
175
|
+
headers: { 'Content-Type': 'application/json' },
|
|
176
|
+
body: JSON.stringify({ IncludeHiddenFiles: !!tmpShow })
|
|
177
|
+
})
|
|
178
|
+
.then(() => tmpCallback())
|
|
179
|
+
.catch(() => tmpCallback());
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Load the file list from the server into the file browser.
|
|
184
|
+
*
|
|
185
|
+
* @param {string} [pPath] - Optional relative path to list (defaults to root)
|
|
186
|
+
* @param {Function} [fCallback] - Optional callback when done
|
|
187
|
+
*/
|
|
188
|
+
loadFileList(pPath, fCallback)
|
|
189
|
+
{
|
|
190
|
+
let tmpCallback = (typeof (fCallback) === 'function') ? fCallback :
|
|
191
|
+
(typeof (pPath) === 'function') ? pPath : () => {};
|
|
192
|
+
let tmpSelf = this;
|
|
193
|
+
|
|
194
|
+
// Use the provided path, or fall back to the current browse location
|
|
195
|
+
let tmpPath = (typeof (pPath) === 'string') ? pPath : null;
|
|
196
|
+
if (tmpPath === null && this.pict.AppData.PictFileBrowser && this.pict.AppData.PictFileBrowser.CurrentLocation)
|
|
197
|
+
{
|
|
198
|
+
tmpPath = this.pict.AppData.PictFileBrowser.CurrentLocation;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let tmpURL = '/api/filebrowser/list';
|
|
202
|
+
if (tmpPath && tmpPath.length > 0)
|
|
203
|
+
{
|
|
204
|
+
tmpURL += '?path=' + encodeURIComponent(tmpPath);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fetch(tmpURL)
|
|
208
|
+
.then((pResponse) => pResponse.json())
|
|
209
|
+
.then((pFileList) =>
|
|
210
|
+
{
|
|
211
|
+
// FileBrowserService returns an array directly
|
|
212
|
+
tmpSelf.pict.AppData.PictFileBrowser =
|
|
213
|
+
tmpSelf.pict.AppData.PictFileBrowser || {};
|
|
214
|
+
tmpSelf.pict.AppData.PictFileBrowser.FileList = pFileList || [];
|
|
215
|
+
|
|
216
|
+
// Render the file browser container (creates pane structure)
|
|
217
|
+
let tmpFileBrowserView = tmpSelf.pict.views['Pict-FileBrowser'];
|
|
218
|
+
if (tmpFileBrowserView)
|
|
219
|
+
{
|
|
220
|
+
tmpFileBrowserView.render();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Render the list detail sub-view (populates the list pane with file rows)
|
|
224
|
+
let tmpListDetailView = tmpSelf.pict.views['Pict-FileBrowser-ListDetail'];
|
|
225
|
+
if (tmpListDetailView)
|
|
226
|
+
{
|
|
227
|
+
tmpListDetailView.render();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return tmpCallback();
|
|
231
|
+
})
|
|
232
|
+
.catch((pError) =>
|
|
233
|
+
{
|
|
234
|
+
tmpSelf.log.error(`Failed to load file list: ${pError.message}`);
|
|
235
|
+
return tmpCallback();
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Resolve the current hash route.
|
|
241
|
+
*
|
|
242
|
+
* Supports:
|
|
243
|
+
* #/edit/<filepath> — Load a file for editing
|
|
244
|
+
* (empty) — Show welcome state
|
|
245
|
+
*/
|
|
246
|
+
resolveHash()
|
|
247
|
+
{
|
|
248
|
+
let tmpHash = (window.location.hash || '').replace(/^#\/?/, '');
|
|
249
|
+
|
|
250
|
+
if (!tmpHash)
|
|
251
|
+
{
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let tmpParts = tmpHash.split('/');
|
|
256
|
+
|
|
257
|
+
if (tmpParts[0] === 'edit' && tmpParts.length >= 2)
|
|
258
|
+
{
|
|
259
|
+
let tmpFilePath = tmpParts.slice(1).join('/');
|
|
260
|
+
// Guard against duplicate navigation when navigateToFile()
|
|
261
|
+
// programmatically sets the hash and triggers hashchange.
|
|
262
|
+
if (this.pict.AppData.ContentEditor.CurrentFile === tmpFilePath)
|
|
263
|
+
{
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
this.navigateToFile(tmpFilePath);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Determine whether a file should use the markdown editor, the code
|
|
272
|
+
* editor, or the binary file preview.
|
|
273
|
+
*
|
|
274
|
+
* @param {string} pFilePath - The file path
|
|
275
|
+
* @returns {string} 'markdown', 'code', or 'binary'
|
|
276
|
+
*/
|
|
277
|
+
getEditorTypeForFile(pFilePath)
|
|
278
|
+
{
|
|
279
|
+
if (!pFilePath)
|
|
280
|
+
{
|
|
281
|
+
return 'markdown';
|
|
282
|
+
}
|
|
283
|
+
let tmpExt = pFilePath.replace(/^.*\./, '').toLowerCase();
|
|
284
|
+
if (tmpExt === 'md' || tmpExt === 'markdown')
|
|
285
|
+
{
|
|
286
|
+
return 'markdown';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Known binary / non-editable file extensions
|
|
290
|
+
let tmpBinaryExtensions =
|
|
291
|
+
{
|
|
292
|
+
// Images
|
|
293
|
+
'png': true, 'jpg': true, 'jpeg': true, 'gif': true, 'bmp': true,
|
|
294
|
+
'webp': true, 'ico': true, 'svg': true, 'tiff': true, 'tif': true,
|
|
295
|
+
'avif': true, 'heic': true, 'heif': true,
|
|
296
|
+
// Audio
|
|
297
|
+
'mp3': true, 'wav': true, 'ogg': true, 'flac': true, 'aac': true,
|
|
298
|
+
'm4a': true, 'wma': true,
|
|
299
|
+
// Video
|
|
300
|
+
'mp4': true, 'avi': true, 'mkv': true, 'mov': true, 'wmv': true,
|
|
301
|
+
'webm': true, 'flv': true, 'm4v': true,
|
|
302
|
+
// Documents / archives
|
|
303
|
+
'pdf': true, 'doc': true, 'docx': true, 'xls': true, 'xlsx': true,
|
|
304
|
+
'ppt': true, 'pptx': true, 'odt': true, 'ods': true, 'odp': true,
|
|
305
|
+
'zip': true, 'tar': true, 'gz': true, 'bz2': true, 'xz': true,
|
|
306
|
+
'7z': true, 'rar': true,
|
|
307
|
+
// Fonts
|
|
308
|
+
'ttf': true, 'otf': true, 'woff': true, 'woff2': true, 'eot': true,
|
|
309
|
+
// Executables / compiled
|
|
310
|
+
'exe': true, 'dll': true, 'so': true, 'dylib': true, 'o': true,
|
|
311
|
+
'class': true, 'pyc': true, 'wasm': true
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
if (tmpBinaryExtensions[tmpExt])
|
|
315
|
+
{
|
|
316
|
+
return 'binary';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return 'code';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Tear down whichever editor is currently active so the container
|
|
324
|
+
* is clean before showing a different view.
|
|
325
|
+
*/
|
|
326
|
+
_cleanupEditors()
|
|
327
|
+
{
|
|
328
|
+
let tmpCodeEditorView = this.pict.views['ContentEditor-CodeEditor'];
|
|
329
|
+
if (tmpCodeEditorView)
|
|
330
|
+
{
|
|
331
|
+
if (tmpCodeEditorView.codeJar)
|
|
332
|
+
{
|
|
333
|
+
tmpCodeEditorView.destroy();
|
|
334
|
+
}
|
|
335
|
+
// Always reset so the next render() triggers onAfterInitialRender
|
|
336
|
+
tmpCodeEditorView.initialRenderComplete = false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Clear the container
|
|
340
|
+
let tmpEditorContainer = this.pict.ContentAssignment.getElement('#ContentEditor-Editor-Container');
|
|
341
|
+
if (tmpEditorContainer && tmpEditorContainer[0])
|
|
342
|
+
{
|
|
343
|
+
tmpEditorContainer[0].innerHTML = '';
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Format a byte count into a human-readable size string.
|
|
349
|
+
*
|
|
350
|
+
* @param {number} pBytes - The byte count
|
|
351
|
+
* @returns {string} Formatted string (e.g. "1.4 MB")
|
|
352
|
+
*/
|
|
353
|
+
_formatFileSize(pBytes)
|
|
354
|
+
{
|
|
355
|
+
if (pBytes === 0) return '0 B';
|
|
356
|
+
let tmpUnits = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
357
|
+
let tmpIndex = Math.floor(Math.log(pBytes) / Math.log(1024));
|
|
358
|
+
if (tmpIndex >= tmpUnits.length) tmpIndex = tmpUnits.length - 1;
|
|
359
|
+
let tmpSize = pBytes / Math.pow(1024, tmpIndex);
|
|
360
|
+
return tmpSize.toFixed(tmpIndex === 0 ? 0 : 1) + ' ' + tmpUnits[tmpIndex];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Show the binary file preview card for a file that cannot be edited.
|
|
365
|
+
*
|
|
366
|
+
* @param {string} pFilePath - The relative path
|
|
367
|
+
*/
|
|
368
|
+
/**
|
|
369
|
+
* Determine the media type of a binary file.
|
|
370
|
+
*
|
|
371
|
+
* @param {string} pExtension - Lowercase file extension
|
|
372
|
+
* @returns {string} 'image', 'video', 'audio', or 'other'
|
|
373
|
+
*/
|
|
374
|
+
_getMediaType(pExtension)
|
|
375
|
+
{
|
|
376
|
+
let tmpImageExtensions = { 'png': true, 'jpg': true, 'jpeg': true, 'gif': true, 'webp': true, 'svg': true, 'bmp': true, 'ico': true, 'avif': true };
|
|
377
|
+
let tmpVideoExtensions = { 'mp4': true, 'webm': true, 'mov': true, 'mkv': true, 'avi': true, 'wmv': true, 'flv': true, 'm4v': true, 'ogv': true };
|
|
378
|
+
let tmpAudioExtensions = { 'mp3': true, 'wav': true, 'ogg': true, 'flac': true, 'aac': true, 'm4a': true, 'wma': true, 'oga': true };
|
|
379
|
+
|
|
380
|
+
if (tmpImageExtensions[pExtension]) return 'image';
|
|
381
|
+
if (tmpVideoExtensions[pExtension]) return 'video';
|
|
382
|
+
if (tmpAudioExtensions[pExtension]) return 'audio';
|
|
383
|
+
return 'other';
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Build the inline media preview HTML for image, video, or audio.
|
|
388
|
+
*
|
|
389
|
+
* @param {string} pMediaType - 'image', 'video', or 'audio'
|
|
390
|
+
* @param {string} pContentURL - The URL to the media file
|
|
391
|
+
* @param {string} pFileName - The display file name
|
|
392
|
+
* @returns {string} HTML string
|
|
393
|
+
*/
|
|
394
|
+
_buildMediaPreviewHTML(pMediaType, pContentURL, pFileName)
|
|
395
|
+
{
|
|
396
|
+
if (pMediaType === 'image')
|
|
397
|
+
{
|
|
398
|
+
return '<div class="binary-preview-image-wrap"><div class="binary-preview-image"><img src="' + pContentURL + '" alt="' + pFileName + '"></div></div>';
|
|
399
|
+
}
|
|
400
|
+
if (pMediaType === 'video')
|
|
401
|
+
{
|
|
402
|
+
return '<div class="binary-preview-media-wrap"><video class="binary-preview-video" controls preload="metadata"><source src="' + pContentURL + '">Your browser does not support the video tag.</video></div>';
|
|
403
|
+
}
|
|
404
|
+
if (pMediaType === 'audio')
|
|
405
|
+
{
|
|
406
|
+
return '<div class="binary-preview-media-wrap"><audio class="binary-preview-audio" controls preload="metadata"><source src="' + pContentURL + '">Your browser does not support the audio tag.</audio></div>';
|
|
407
|
+
}
|
|
408
|
+
return '';
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Load a media preview into the placeholder container.
|
|
413
|
+
* Called when the user clicks the Preview button on a media card.
|
|
414
|
+
*
|
|
415
|
+
* @param {string} pMediaType - 'image', 'video', or 'audio'
|
|
416
|
+
* @param {string} pContentURL - The URL to the media file
|
|
417
|
+
* @param {string} pFileName - The display file name
|
|
418
|
+
*/
|
|
419
|
+
loadMediaPreview(pMediaType, pContentURL, pFileName)
|
|
420
|
+
{
|
|
421
|
+
let tmpContainer = document.getElementById('ContentEditor-MediaPreviewPlaceholder');
|
|
422
|
+
if (!tmpContainer)
|
|
423
|
+
{
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
tmpContainer.innerHTML = this._buildMediaPreviewHTML(pMediaType, pContentURL, pFileName);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
_showBinaryPreview(pFilePath)
|
|
430
|
+
{
|
|
431
|
+
let tmpSelf = this;
|
|
432
|
+
let tmpFileName = pFilePath.replace(/^.*\//, '');
|
|
433
|
+
let tmpExtension = pFilePath.replace(/^.*\./, '').toLowerCase();
|
|
434
|
+
let tmpContentURL = '/content/' + encodeURIComponent(pFilePath);
|
|
435
|
+
|
|
436
|
+
let tmpMediaType = this._getMediaType(tmpExtension);
|
|
437
|
+
let tmpSettings = this.pict.AppData.ContentEditor;
|
|
438
|
+
|
|
439
|
+
// Determine whether to auto-preview based on settings
|
|
440
|
+
let tmpAutoPreview = false;
|
|
441
|
+
if (tmpMediaType === 'image') tmpAutoPreview = tmpSettings.AutoPreviewImages;
|
|
442
|
+
if (tmpMediaType === 'video') tmpAutoPreview = tmpSettings.AutoPreviewVideo;
|
|
443
|
+
if (tmpMediaType === 'audio') tmpAutoPreview = tmpSettings.AutoPreviewAudio;
|
|
444
|
+
|
|
445
|
+
// Fetch file info from the file browser API
|
|
446
|
+
fetch('/api/filebrowser/info?path=' + encodeURIComponent(pFilePath))
|
|
447
|
+
.then((pResponse) => pResponse.json())
|
|
448
|
+
.then((pInfo) =>
|
|
449
|
+
{
|
|
450
|
+
let tmpSize = (pInfo && typeof (pInfo.Size) === 'number') ? tmpSelf._formatFileSize(pInfo.Size) : 'Unknown';
|
|
451
|
+
let tmpModified = (pInfo && pInfo.Modified) ? new Date(pInfo.Modified).toLocaleString() : 'Unknown';
|
|
452
|
+
|
|
453
|
+
let tmpEditorContainer = tmpSelf.pict.ContentAssignment.getElement('#ContentEditor-Editor-Container');
|
|
454
|
+
if (!tmpEditorContainer || !tmpEditorContainer[0])
|
|
455
|
+
{
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
let tmpPreviewHTML = '';
|
|
460
|
+
|
|
461
|
+
if (tmpMediaType !== 'other')
|
|
462
|
+
{
|
|
463
|
+
if (tmpAutoPreview)
|
|
464
|
+
{
|
|
465
|
+
tmpPreviewHTML += tmpSelf._buildMediaPreviewHTML(tmpMediaType, tmpContentURL, tmpFileName);
|
|
466
|
+
}
|
|
467
|
+
else
|
|
468
|
+
{
|
|
469
|
+
// Placeholder with a Preview button
|
|
470
|
+
tmpPreviewHTML += '<div id="ContentEditor-MediaPreviewPlaceholder">';
|
|
471
|
+
tmpPreviewHTML += '<button class="binary-preview-btn binary-preview-btn-preview"';
|
|
472
|
+
tmpPreviewHTML += ' onclick="pict.PictApplication.loadMediaPreview(';
|
|
473
|
+
tmpPreviewHTML += "'" + tmpMediaType + "','" + tmpContentURL + "','" + tmpFileName.replace(/'/g, "\\'") + "'";
|
|
474
|
+
tmpPreviewHTML += ')">Preview ' + tmpMediaType.charAt(0).toUpperCase() + tmpMediaType.slice(1) + '</button>';
|
|
475
|
+
tmpPreviewHTML += '</div>';
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
tmpPreviewHTML += '<div class="binary-preview-card">';
|
|
480
|
+
tmpPreviewHTML += '<div class="binary-preview-icon">' + tmpExtension.toUpperCase() + '</div>';
|
|
481
|
+
tmpPreviewHTML += '<div class="binary-preview-info">';
|
|
482
|
+
tmpPreviewHTML += '<div class="binary-preview-name">' + tmpFileName + '</div>';
|
|
483
|
+
tmpPreviewHTML += '<div class="binary-preview-meta">Size: ' + tmpSize + '</div>';
|
|
484
|
+
tmpPreviewHTML += '<div class="binary-preview-meta">Modified: ' + tmpModified + '</div>';
|
|
485
|
+
tmpPreviewHTML += '<div class="binary-preview-meta">Type: .' + tmpExtension + '</div>';
|
|
486
|
+
tmpPreviewHTML += '</div>';
|
|
487
|
+
tmpPreviewHTML += '<div class="binary-preview-actions">';
|
|
488
|
+
tmpPreviewHTML += '<a class="binary-preview-btn" href="' + tmpContentURL + '" download="' + tmpFileName + '">Download</a>';
|
|
489
|
+
tmpPreviewHTML += '<a class="binary-preview-btn binary-preview-btn-secondary" href="' + tmpContentURL + '" target="_blank">Open in New Tab</a>';
|
|
490
|
+
tmpPreviewHTML += '</div>';
|
|
491
|
+
tmpPreviewHTML += '</div>';
|
|
492
|
+
|
|
493
|
+
tmpEditorContainer[0].innerHTML = tmpPreviewHTML;
|
|
494
|
+
})
|
|
495
|
+
.catch(() =>
|
|
496
|
+
{
|
|
497
|
+
// Fallback if info fetch fails
|
|
498
|
+
let tmpEditorContainer = tmpSelf.pict.ContentAssignment.getElement('#ContentEditor-Editor-Container');
|
|
499
|
+
if (tmpEditorContainer && tmpEditorContainer[0])
|
|
500
|
+
{
|
|
501
|
+
tmpEditorContainer[0].innerHTML =
|
|
502
|
+
'<div class="binary-preview-card">' +
|
|
503
|
+
'<div class="binary-preview-icon">' + tmpExtension.toUpperCase() + '</div>' +
|
|
504
|
+
'<div class="binary-preview-info">' +
|
|
505
|
+
'<div class="binary-preview-name">' + tmpFileName + '</div>' +
|
|
506
|
+
'<div class="binary-preview-meta">Binary file — cannot be edited in the browser</div>' +
|
|
507
|
+
'</div>' +
|
|
508
|
+
'<div class="binary-preview-actions">' +
|
|
509
|
+
'<a class="binary-preview-btn" href="' + tmpContentURL + '" download="' + tmpFileName + '">Download</a>' +
|
|
510
|
+
'<a class="binary-preview-btn binary-preview-btn-secondary" href="' + tmpContentURL + '" target="_blank">Open in New Tab</a>' +
|
|
511
|
+
'</div></div>';
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Segment markdown content based on the Auto Segment settings.
|
|
518
|
+
*
|
|
519
|
+
* When AutoSegmentMarkdown is enabled, splits the content into
|
|
520
|
+
* segments at the configured heading depth.
|
|
521
|
+
*
|
|
522
|
+
* Depth 1 splits every top-level block (paragraphs, code fences,
|
|
523
|
+
* headings, etc.) into its own segment. Depth 2+ splits at the
|
|
524
|
+
* corresponding heading level, keeping everything between two
|
|
525
|
+
* headings of that level (or higher) in the same segment.
|
|
526
|
+
*
|
|
527
|
+
* @param {string} pContent - Raw markdown text
|
|
528
|
+
* @returns {Array} Array of { Content: string } segment objects
|
|
529
|
+
*/
|
|
530
|
+
segmentMarkdownContent(pContent)
|
|
531
|
+
{
|
|
532
|
+
let tmpSettings = this.pict.AppData.ContentEditor;
|
|
533
|
+
|
|
534
|
+
if (!tmpSettings.AutoSegmentMarkdown || !pContent)
|
|
535
|
+
{
|
|
536
|
+
return [{ Content: pContent || '' }];
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
let tmpDepth = parseInt(tmpSettings.AutoSegmentDepth, 10) || 1;
|
|
540
|
+
|
|
541
|
+
if (tmpDepth === 1)
|
|
542
|
+
{
|
|
543
|
+
// Depth 1: every block is its own segment.
|
|
544
|
+
// Split on blank lines, preserving fenced code blocks as
|
|
545
|
+
// single units.
|
|
546
|
+
let tmpLines = pContent.split('\n');
|
|
547
|
+
let tmpSegments = [];
|
|
548
|
+
let tmpCurrent = [];
|
|
549
|
+
let tmpInFence = false;
|
|
550
|
+
|
|
551
|
+
for (let i = 0; i < tmpLines.length; i++)
|
|
552
|
+
{
|
|
553
|
+
let tmpLine = tmpLines[i];
|
|
554
|
+
|
|
555
|
+
// Detect fenced code block boundaries
|
|
556
|
+
if (/^(`{3,}|~{3,})/.test(tmpLine.trim()))
|
|
557
|
+
{
|
|
558
|
+
tmpInFence = !tmpInFence;
|
|
559
|
+
tmpCurrent.push(tmpLine);
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (tmpInFence)
|
|
564
|
+
{
|
|
565
|
+
tmpCurrent.push(tmpLine);
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Outside a fence, a blank line ends the current block
|
|
570
|
+
if (tmpLine.trim() === '')
|
|
571
|
+
{
|
|
572
|
+
if (tmpCurrent.length > 0)
|
|
573
|
+
{
|
|
574
|
+
tmpSegments.push({ Content: tmpCurrent.join('\n') });
|
|
575
|
+
tmpCurrent = [];
|
|
576
|
+
}
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
tmpCurrent.push(tmpLine);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Push any trailing content
|
|
584
|
+
if (tmpCurrent.length > 0)
|
|
585
|
+
{
|
|
586
|
+
tmpSegments.push({ Content: tmpCurrent.join('\n') });
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return tmpSegments.length > 0 ? tmpSegments : [{ Content: '' }];
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Depth 2+: split at headings of that level or higher.
|
|
593
|
+
// A heading like "## Foo" is depth 2. We split when we see a
|
|
594
|
+
// heading whose depth is <= the configured depth.
|
|
595
|
+
let tmpHeadingPattern = new RegExp('^(#{1,' + tmpDepth + '})\\s');
|
|
596
|
+
let tmpLines = pContent.split('\n');
|
|
597
|
+
let tmpSegments = [];
|
|
598
|
+
let tmpCurrent = [];
|
|
599
|
+
|
|
600
|
+
for (let i = 0; i < tmpLines.length; i++)
|
|
601
|
+
{
|
|
602
|
+
let tmpLine = tmpLines[i];
|
|
603
|
+
|
|
604
|
+
if (tmpHeadingPattern.test(tmpLine.trim()) && tmpCurrent.length > 0)
|
|
605
|
+
{
|
|
606
|
+
tmpSegments.push({ Content: tmpCurrent.join('\n') });
|
|
607
|
+
tmpCurrent = [];
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
tmpCurrent.push(tmpLine);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (tmpCurrent.length > 0)
|
|
614
|
+
{
|
|
615
|
+
tmpSegments.push({ Content: tmpCurrent.join('\n') });
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return tmpSegments.length > 0 ? tmpSegments : [{ Content: '' }];
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Navigate to a file for editing.
|
|
623
|
+
*
|
|
624
|
+
* Automatically selects the markdown editor for .md files, the code
|
|
625
|
+
* editor (with highlight.js) for text files, or a binary preview
|
|
626
|
+
* card for images, archives, and other non-editable files.
|
|
627
|
+
*
|
|
628
|
+
* @param {string} pFilePath - The relative path of the file to edit
|
|
629
|
+
*/
|
|
630
|
+
navigateToFile(pFilePath)
|
|
631
|
+
{
|
|
632
|
+
if (!pFilePath)
|
|
633
|
+
{
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
let tmpSelf = this;
|
|
638
|
+
|
|
639
|
+
// Determine which editor to use before fetching content
|
|
640
|
+
let tmpEditorType = this.getEditorTypeForFile(pFilePath);
|
|
641
|
+
|
|
642
|
+
this.pict.AppData.ContentEditor.SaveStatus = '';
|
|
643
|
+
this.pict.AppData.ContentEditor.SaveStatusClass = '';
|
|
644
|
+
|
|
645
|
+
// Update the hash without triggering resolveHash again
|
|
646
|
+
window.location.hash = '#/edit/' + pFilePath;
|
|
647
|
+
|
|
648
|
+
// Set the current file and editor type
|
|
649
|
+
this.pict.AppData.ContentEditor.CurrentFile = pFilePath;
|
|
650
|
+
this.pict.AppData.ContentEditor.IsDirty = false;
|
|
651
|
+
this.pict.AppData.ContentEditor.ActiveEditor = tmpEditorType;
|
|
652
|
+
|
|
653
|
+
// Clean up existing editors
|
|
654
|
+
this._cleanupEditors();
|
|
655
|
+
|
|
656
|
+
// Re-render top bar
|
|
657
|
+
this.pict.views['ContentEditor-TopBar'].render();
|
|
658
|
+
|
|
659
|
+
// Binary files: show preview card without loading content
|
|
660
|
+
if (tmpEditorType === 'binary')
|
|
661
|
+
{
|
|
662
|
+
this._showBinaryPreview(pFilePath);
|
|
663
|
+
this.updateStats();
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Text files: load content from the server
|
|
668
|
+
let tmpProvider = this.pict.providers['ContentEditor-Provider'];
|
|
669
|
+
|
|
670
|
+
this.pict.AppData.ContentEditor.IsLoading = true;
|
|
671
|
+
|
|
672
|
+
tmpProvider.loadFile(pFilePath, (pError, pContent) =>
|
|
673
|
+
{
|
|
674
|
+
tmpSelf.pict.AppData.ContentEditor.IsLoading = false;
|
|
675
|
+
|
|
676
|
+
if (pError)
|
|
677
|
+
{
|
|
678
|
+
tmpSelf.pict.AppData.ContentEditor.SaveStatus = 'Error loading file: ' + pError;
|
|
679
|
+
tmpSelf.pict.AppData.ContentEditor.SaveStatusClass = 'content-editor-status-error';
|
|
680
|
+
tmpSelf.pict.views['ContentEditor-TopBar'].render();
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (tmpEditorType === 'markdown')
|
|
685
|
+
{
|
|
686
|
+
// Markdown editor: uses segments (auto-segment if enabled)
|
|
687
|
+
tmpSelf.pict.AppData.ContentEditor.Document.Segments = tmpSelf.segmentMarkdownContent(pContent);
|
|
688
|
+
|
|
689
|
+
let tmpEditorView = tmpSelf.pict.views['ContentEditor-MarkdownEditor'];
|
|
690
|
+
if (tmpEditorView)
|
|
691
|
+
{
|
|
692
|
+
tmpEditorView.render();
|
|
693
|
+
tmpEditorView.marshalToView();
|
|
694
|
+
|
|
695
|
+
// Apply the Auto Content Preview setting via the
|
|
696
|
+
// library's own toggle so the per-segment button
|
|
697
|
+
// continues to work.
|
|
698
|
+
tmpEditorView.togglePreview(tmpSelf.pict.AppData.ContentEditor.AutoContentPreview);
|
|
699
|
+
|
|
700
|
+
// Apply the Editing Controls setting (line numbers
|
|
701
|
+
// and right sidebar) via the library's toggleControls.
|
|
702
|
+
tmpEditorView.toggleControls(tmpSelf.pict.AppData.ContentEditor.MarkdownEditingControls);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
tmpSelf.updateStats();
|
|
706
|
+
}
|
|
707
|
+
else
|
|
708
|
+
{
|
|
709
|
+
// Code editor: uses CodeContent
|
|
710
|
+
tmpSelf.pict.AppData.ContentEditor.CodeContent = pContent;
|
|
711
|
+
|
|
712
|
+
// Detect language from file extension
|
|
713
|
+
let tmpExtension = pFilePath.replace(/^.*\./, '').toLowerCase();
|
|
714
|
+
let tmpLanguage = libViewCodeEditor.getLanguageForExtension
|
|
715
|
+
? libViewCodeEditor.getLanguageForExtension(tmpExtension)
|
|
716
|
+
: (libViewCodeEditor.ExtensionLanguageMap[tmpExtension] || 'plaintext');
|
|
717
|
+
|
|
718
|
+
let tmpCodeEditorView = tmpSelf.pict.views['ContentEditor-CodeEditor'];
|
|
719
|
+
if (tmpCodeEditorView)
|
|
720
|
+
{
|
|
721
|
+
tmpCodeEditorView.initialRenderComplete = false;
|
|
722
|
+
tmpCodeEditorView._language = tmpLanguage;
|
|
723
|
+
|
|
724
|
+
// Suppress the dirty signal from the initial content push
|
|
725
|
+
tmpCodeEditorView._suppressNextDirty = true;
|
|
726
|
+
tmpCodeEditorView.render();
|
|
727
|
+
tmpCodeEditorView.marshalToView();
|
|
728
|
+
|
|
729
|
+
// Apply Code Word Wrap setting
|
|
730
|
+
if (tmpSelf.pict.AppData.ContentEditor.CodeWordWrap && tmpCodeEditorView._editorElement)
|
|
731
|
+
{
|
|
732
|
+
tmpCodeEditorView._editorElement.style.whiteSpace = 'pre-wrap';
|
|
733
|
+
tmpCodeEditorView._editorElement.style.overflowWrap = 'break-word';
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
tmpSelf.updateStats();
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Save the currently edited file.
|
|
744
|
+
*/
|
|
745
|
+
saveCurrentFile()
|
|
746
|
+
{
|
|
747
|
+
let tmpFilePath = this.pict.AppData.ContentEditor.CurrentFile;
|
|
748
|
+
if (!tmpFilePath)
|
|
749
|
+
{
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
let tmpProvider = this.pict.providers['ContentEditor-Provider'];
|
|
754
|
+
let tmpSelf = this;
|
|
755
|
+
|
|
756
|
+
let tmpContent = '';
|
|
757
|
+
let tmpActiveEditor = this.pict.AppData.ContentEditor.ActiveEditor;
|
|
758
|
+
|
|
759
|
+
if (tmpActiveEditor === 'code')
|
|
760
|
+
{
|
|
761
|
+
// Marshal content from the code editor
|
|
762
|
+
let tmpCodeEditorView = this.pict.views['ContentEditor-CodeEditor'];
|
|
763
|
+
if (tmpCodeEditorView)
|
|
764
|
+
{
|
|
765
|
+
tmpCodeEditorView.marshalFromView();
|
|
766
|
+
}
|
|
767
|
+
tmpContent = this.pict.AppData.ContentEditor.CodeContent || '';
|
|
768
|
+
}
|
|
769
|
+
else
|
|
770
|
+
{
|
|
771
|
+
// Marshal content from the markdown editor
|
|
772
|
+
let tmpEditorView = this.pict.views['ContentEditor-MarkdownEditor'];
|
|
773
|
+
if (tmpEditorView)
|
|
774
|
+
{
|
|
775
|
+
tmpEditorView.marshalFromView();
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Combine all segments into a single content string
|
|
779
|
+
let tmpSegments = this.pict.AppData.ContentEditor.Document.Segments;
|
|
780
|
+
if (tmpSegments && tmpSegments.length > 0)
|
|
781
|
+
{
|
|
782
|
+
let tmpParts = [];
|
|
783
|
+
for (let i = 0; i < tmpSegments.length; i++)
|
|
784
|
+
{
|
|
785
|
+
tmpParts.push(tmpSegments[i].Content || '');
|
|
786
|
+
}
|
|
787
|
+
tmpContent = tmpParts.join('\n\n');
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
this.pict.AppData.ContentEditor.IsSaving = true;
|
|
792
|
+
this.pict.AppData.ContentEditor.SaveStatus = 'Saving...';
|
|
793
|
+
this.pict.AppData.ContentEditor.SaveStatusClass = 'content-editor-status-saving';
|
|
794
|
+
this.pict.views['ContentEditor-TopBar'].render();
|
|
795
|
+
|
|
796
|
+
tmpProvider.saveFile(tmpFilePath, tmpContent, (pError) =>
|
|
797
|
+
{
|
|
798
|
+
tmpSelf.pict.AppData.ContentEditor.IsSaving = false;
|
|
799
|
+
|
|
800
|
+
if (pError)
|
|
801
|
+
{
|
|
802
|
+
tmpSelf.pict.AppData.ContentEditor.SaveStatus = 'Error: ' + pError;
|
|
803
|
+
tmpSelf.pict.AppData.ContentEditor.SaveStatusClass = 'content-editor-status-error';
|
|
804
|
+
}
|
|
805
|
+
else
|
|
806
|
+
{
|
|
807
|
+
tmpSelf.pict.AppData.ContentEditor.IsDirty = false;
|
|
808
|
+
tmpSelf.pict.AppData.ContentEditor.SaveStatus = 'Saved';
|
|
809
|
+
tmpSelf.pict.AppData.ContentEditor.SaveStatusClass = 'content-editor-status-saved';
|
|
810
|
+
|
|
811
|
+
// Refresh the file list after saving
|
|
812
|
+
tmpSelf.loadFileList();
|
|
813
|
+
|
|
814
|
+
// Clear the save status after a delay
|
|
815
|
+
setTimeout(() =>
|
|
816
|
+
{
|
|
817
|
+
if (tmpSelf.pict.AppData.ContentEditor.SaveStatus === 'Saved')
|
|
818
|
+
{
|
|
819
|
+
tmpSelf.pict.AppData.ContentEditor.SaveStatus = '';
|
|
820
|
+
tmpSelf.pict.AppData.ContentEditor.SaveStatusClass = '';
|
|
821
|
+
tmpSelf.pict.views['ContentEditor-TopBar'].render();
|
|
822
|
+
}
|
|
823
|
+
}, 3000);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
tmpSelf.pict.views['ContentEditor-TopBar'].render();
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Close the currently open file.
|
|
832
|
+
*
|
|
833
|
+
* If the document has unsaved changes, shows a confirmation dialog.
|
|
834
|
+
* Otherwise closes immediately.
|
|
835
|
+
*/
|
|
836
|
+
closeCurrentFile()
|
|
837
|
+
{
|
|
838
|
+
if (!this.pict.AppData.ContentEditor.CurrentFile)
|
|
839
|
+
{
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (this.pict.AppData.ContentEditor.IsDirty)
|
|
844
|
+
{
|
|
845
|
+
this._showCloseConfirmation();
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
this._doCloseFile();
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Perform the actual close: reset editor state to the welcome screen.
|
|
854
|
+
*/
|
|
855
|
+
_doCloseFile()
|
|
856
|
+
{
|
|
857
|
+
this._hideCloseConfirmation();
|
|
858
|
+
|
|
859
|
+
this._cleanupEditors();
|
|
860
|
+
|
|
861
|
+
this.pict.AppData.ContentEditor.CurrentFile = '';
|
|
862
|
+
this.pict.AppData.ContentEditor.ActiveEditor = 'markdown';
|
|
863
|
+
this.pict.AppData.ContentEditor.IsDirty = false;
|
|
864
|
+
this.pict.AppData.ContentEditor.SaveStatus = '';
|
|
865
|
+
this.pict.AppData.ContentEditor.SaveStatusClass = '';
|
|
866
|
+
this.pict.AppData.ContentEditor.Document.Segments = [{ Content: '' }];
|
|
867
|
+
this.pict.AppData.ContentEditor.CodeContent = '';
|
|
868
|
+
|
|
869
|
+
// Clear the hash
|
|
870
|
+
window.location.hash = '';
|
|
871
|
+
|
|
872
|
+
// Re-render top bar (hides save/close buttons)
|
|
873
|
+
this.pict.views['ContentEditor-TopBar'].render();
|
|
874
|
+
|
|
875
|
+
// Show the welcome message
|
|
876
|
+
let tmpEditorContainer = this.pict.ContentAssignment.getElement('#ContentEditor-Editor-Container');
|
|
877
|
+
if (tmpEditorContainer && tmpEditorContainer[0])
|
|
878
|
+
{
|
|
879
|
+
tmpEditorContainer[0].innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:#8A7F72;font-size:1.1em;">Select a file from the sidebar to begin editing</div>';
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
this.updateStats();
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Confirm closing a file with unsaved changes (discard and close).
|
|
887
|
+
*/
|
|
888
|
+
confirmCloseFile()
|
|
889
|
+
{
|
|
890
|
+
this._doCloseFile();
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Cancel the close confirmation dialog.
|
|
895
|
+
*/
|
|
896
|
+
cancelCloseFile()
|
|
897
|
+
{
|
|
898
|
+
this._hideCloseConfirmation();
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Show the unsaved-changes confirmation overlay.
|
|
903
|
+
*/
|
|
904
|
+
_showCloseConfirmation()
|
|
905
|
+
{
|
|
906
|
+
let tmpOverlay = document.getElementById('ContentEditor-ConfirmOverlay');
|
|
907
|
+
if (tmpOverlay)
|
|
908
|
+
{
|
|
909
|
+
tmpOverlay.classList.add('open');
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Set up keyboard listener for Y/N/Esc
|
|
913
|
+
if (!this._confirmKeyHandler)
|
|
914
|
+
{
|
|
915
|
+
let tmpSelf = this;
|
|
916
|
+
this._confirmKeyHandler = (pEvent) =>
|
|
917
|
+
{
|
|
918
|
+
let tmpKey = pEvent.key.toLowerCase();
|
|
919
|
+
if (tmpKey === 'y')
|
|
920
|
+
{
|
|
921
|
+
pEvent.preventDefault();
|
|
922
|
+
tmpSelf.confirmCloseFile();
|
|
923
|
+
}
|
|
924
|
+
else if (tmpKey === 'n' || pEvent.key === 'Escape')
|
|
925
|
+
{
|
|
926
|
+
pEvent.preventDefault();
|
|
927
|
+
tmpSelf.cancelCloseFile();
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
window.addEventListener('keydown', this._confirmKeyHandler);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Hide the unsaved-changes confirmation overlay and remove the keyboard listener.
|
|
937
|
+
*/
|
|
938
|
+
_hideCloseConfirmation()
|
|
939
|
+
{
|
|
940
|
+
let tmpOverlay = document.getElementById('ContentEditor-ConfirmOverlay');
|
|
941
|
+
if (tmpOverlay)
|
|
942
|
+
{
|
|
943
|
+
tmpOverlay.classList.remove('open');
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (this._confirmKeyHandler)
|
|
947
|
+
{
|
|
948
|
+
window.removeEventListener('keydown', this._confirmKeyHandler);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Create a new file.
|
|
954
|
+
*
|
|
955
|
+
* @param {string} pFilePath - The path for the new file
|
|
956
|
+
*/
|
|
957
|
+
createNewFile(pFilePath)
|
|
958
|
+
{
|
|
959
|
+
if (!pFilePath)
|
|
960
|
+
{
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Only add .md extension if the user did not provide any extension
|
|
965
|
+
let tmpBaseName = pFilePath.replace(/^.*\//, '');
|
|
966
|
+
if (tmpBaseName.indexOf('.') < 0)
|
|
967
|
+
{
|
|
968
|
+
pFilePath = pFilePath + '.md';
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
let tmpProvider = this.pict.providers['ContentEditor-Provider'];
|
|
972
|
+
let tmpSelf = this;
|
|
973
|
+
|
|
974
|
+
// Generate sensible default content based on file type
|
|
975
|
+
let tmpDefaultContent = '';
|
|
976
|
+
if (pFilePath.endsWith('.md'))
|
|
977
|
+
{
|
|
978
|
+
tmpDefaultContent = '# ' + pFilePath.replace(/\.[^.]+$/, '').replace(/^.*\//, '') + '\n\n';
|
|
979
|
+
}
|
|
980
|
+
else
|
|
981
|
+
{
|
|
982
|
+
tmpDefaultContent = '// ' + pFilePath.replace(/^.*\//, '') + '\n';
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
tmpProvider.saveFile(pFilePath, tmpDefaultContent, (pError) =>
|
|
986
|
+
{
|
|
987
|
+
if (!pError)
|
|
988
|
+
{
|
|
989
|
+
// Reload the file list and navigate to the new file
|
|
990
|
+
tmpSelf.loadFileList(null, () =>
|
|
991
|
+
{
|
|
992
|
+
tmpSelf.navigateToFile(pFilePath);
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Prompt the user for a new file name and create it.
|
|
1000
|
+
*/
|
|
1001
|
+
promptNewFile()
|
|
1002
|
+
{
|
|
1003
|
+
let tmpFileName = prompt('Enter a name for the new file (e.g. my-page.md, script.js, style.css):');
|
|
1004
|
+
if (tmpFileName && tmpFileName.trim())
|
|
1005
|
+
{
|
|
1006
|
+
this.createNewFile(tmpFileName.trim());
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Handle F4 / Cmd+Shift+T: context-aware topic creation.
|
|
1012
|
+
*
|
|
1013
|
+
* If the markdown editor is active and has focus, creates a new
|
|
1014
|
+
* topic linked to the current file and cursor line, then switches
|
|
1015
|
+
* to the Topics tab with the new entry in edit mode.
|
|
1016
|
+
*
|
|
1017
|
+
* Otherwise, just toggles the Topics sidebar tab.
|
|
1018
|
+
*/
|
|
1019
|
+
handleF4TopicAction()
|
|
1020
|
+
{
|
|
1021
|
+
let tmpLayoutView = this.pict.views['ContentEditor-Layout'];
|
|
1022
|
+
let tmpTopicsView = this.pict.views['ContentEditor-Topics'];
|
|
1023
|
+
|
|
1024
|
+
if (!tmpLayoutView || !tmpTopicsView)
|
|
1025
|
+
{
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
let tmpSettings = this.pict.AppData.ContentEditor;
|
|
1030
|
+
let tmpActiveEditor = tmpSettings.ActiveEditor;
|
|
1031
|
+
let tmpCurrentFile = tmpSettings.CurrentFile;
|
|
1032
|
+
|
|
1033
|
+
// Check if we're in the markdown editor with a file open
|
|
1034
|
+
let tmpInMarkdownEditor = (tmpActiveEditor === 'markdown' && tmpCurrentFile);
|
|
1035
|
+
let tmpLineNumber = 0;
|
|
1036
|
+
let tmpFoundFocus = false;
|
|
1037
|
+
|
|
1038
|
+
if (tmpInMarkdownEditor)
|
|
1039
|
+
{
|
|
1040
|
+
// Try to get the cursor line from the focused CodeMirror editor
|
|
1041
|
+
let tmpEditorView = this.pict.views['ContentEditor-MarkdownEditor'];
|
|
1042
|
+
if (tmpEditorView && tmpEditorView._segmentEditors)
|
|
1043
|
+
{
|
|
1044
|
+
let tmpRunningLines = 0;
|
|
1045
|
+
for (let tmpKey in tmpEditorView._segmentEditors)
|
|
1046
|
+
{
|
|
1047
|
+
let tmpEditor = tmpEditorView._segmentEditors[tmpKey];
|
|
1048
|
+
if (tmpEditor && tmpEditor.hasFocus)
|
|
1049
|
+
{
|
|
1050
|
+
let tmpPos = tmpEditor.state.selection.main.head;
|
|
1051
|
+
let tmpLine = tmpEditor.state.doc.lineAt(tmpPos);
|
|
1052
|
+
tmpLineNumber = tmpRunningLines + tmpLine.number;
|
|
1053
|
+
tmpFoundFocus = true;
|
|
1054
|
+
break;
|
|
1055
|
+
}
|
|
1056
|
+
if (tmpEditor && tmpEditor.state)
|
|
1057
|
+
{
|
|
1058
|
+
tmpRunningLines += tmpEditor.state.doc.lines;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Expand sidebar if collapsed
|
|
1065
|
+
if (tmpSettings.SidebarCollapsed)
|
|
1066
|
+
{
|
|
1067
|
+
tmpLayoutView.toggleSidebar();
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Switch to the Topics tab
|
|
1071
|
+
tmpLayoutView.switchSidebarTab('topics');
|
|
1072
|
+
|
|
1073
|
+
// If we found a focused editor, create a topic from context
|
|
1074
|
+
if (tmpFoundFocus && tmpCurrentFile)
|
|
1075
|
+
{
|
|
1076
|
+
// Extract the document's first biggest heading for a default title
|
|
1077
|
+
let tmpDefaultTitle = this._extractFirstHeading();
|
|
1078
|
+
|
|
1079
|
+
let tmpTopicData =
|
|
1080
|
+
{
|
|
1081
|
+
TopicCode: 'New-Topic',
|
|
1082
|
+
TopicHelpFilePath: tmpCurrentFile,
|
|
1083
|
+
TopicTitle: tmpDefaultTitle || 'New Topic'
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
if (tmpLineNumber > 0)
|
|
1087
|
+
{
|
|
1088
|
+
tmpTopicData.RelevantMarkdownLine = tmpLineNumber;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
tmpTopicsView.addTopic(tmpTopicData);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
/**
|
|
1096
|
+
* Extract the first (highest-level) heading from the markdown
|
|
1097
|
+
* content currently loaded in the editor.
|
|
1098
|
+
*
|
|
1099
|
+
* Scans all segment editors for heading lines (# ... through
|
|
1100
|
+
* ###### ...) and returns the text of the highest-level one
|
|
1101
|
+
* found (preferring H1, then H2, etc.). If multiple headings
|
|
1102
|
+
* share the same level, the first one wins.
|
|
1103
|
+
*
|
|
1104
|
+
* @returns {string} The heading text, or '' if none found
|
|
1105
|
+
*/
|
|
1106
|
+
_extractFirstHeading()
|
|
1107
|
+
{
|
|
1108
|
+
let tmpEditorView = this.pict.views['ContentEditor-MarkdownEditor'];
|
|
1109
|
+
if (!tmpEditorView || !tmpEditorView._segmentEditors)
|
|
1110
|
+
{
|
|
1111
|
+
return '';
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
let tmpBestLevel = 7; // lower is better (1 = H1)
|
|
1115
|
+
let tmpBestText = '';
|
|
1116
|
+
|
|
1117
|
+
for (let tmpKey in tmpEditorView._segmentEditors)
|
|
1118
|
+
{
|
|
1119
|
+
let tmpEditor = tmpEditorView._segmentEditors[tmpKey];
|
|
1120
|
+
if (!tmpEditor || !tmpEditor.state || !tmpEditor.state.doc)
|
|
1121
|
+
{
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
let tmpDoc = tmpEditor.state.doc;
|
|
1126
|
+
for (let i = 1; i <= tmpDoc.lines; i++)
|
|
1127
|
+
{
|
|
1128
|
+
let tmpLine = tmpDoc.line(i).text;
|
|
1129
|
+
let tmpMatch = tmpLine.match(/^(#{1,6})\s+(.+)/);
|
|
1130
|
+
if (tmpMatch)
|
|
1131
|
+
{
|
|
1132
|
+
let tmpLevel = tmpMatch[1].length;
|
|
1133
|
+
if (tmpLevel < tmpBestLevel)
|
|
1134
|
+
{
|
|
1135
|
+
tmpBestLevel = tmpLevel;
|
|
1136
|
+
tmpBestText = tmpMatch[2].trim();
|
|
1137
|
+
|
|
1138
|
+
// Can't do better than H1
|
|
1139
|
+
if (tmpBestLevel === 1)
|
|
1140
|
+
{
|
|
1141
|
+
return tmpBestText;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
return tmpBestText;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Update the document stats display (lines, words, chars).
|
|
1153
|
+
*
|
|
1154
|
+
* Reads directly from the active editor instances so there is no
|
|
1155
|
+
* need to marshal first. CodeMirror exposes line and character
|
|
1156
|
+
* counts on its document model at near-zero cost.
|
|
1157
|
+
*/
|
|
1158
|
+
updateStats()
|
|
1159
|
+
{
|
|
1160
|
+
let tmpStatsEl = document.getElementById('ContentEditor-Stats');
|
|
1161
|
+
if (!tmpStatsEl)
|
|
1162
|
+
{
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
let tmpActiveEditor = this.pict.AppData.ContentEditor.ActiveEditor;
|
|
1167
|
+
let tmpLines = 0;
|
|
1168
|
+
let tmpChars = 0;
|
|
1169
|
+
let tmpWords = 0;
|
|
1170
|
+
|
|
1171
|
+
if (tmpActiveEditor === 'markdown')
|
|
1172
|
+
{
|
|
1173
|
+
let tmpEditorView = this.pict.views['ContentEditor-MarkdownEditor'];
|
|
1174
|
+
if (tmpEditorView && tmpEditorView._segmentEditors)
|
|
1175
|
+
{
|
|
1176
|
+
for (let tmpKey in tmpEditorView._segmentEditors)
|
|
1177
|
+
{
|
|
1178
|
+
let tmpEditor = tmpEditorView._segmentEditors[tmpKey];
|
|
1179
|
+
if (tmpEditor && tmpEditor.state && tmpEditor.state.doc)
|
|
1180
|
+
{
|
|
1181
|
+
tmpLines += tmpEditor.state.doc.lines;
|
|
1182
|
+
tmpChars += tmpEditor.state.doc.length;
|
|
1183
|
+
let tmpText = tmpEditor.state.doc.toString();
|
|
1184
|
+
let tmpMatches = tmpText.match(/\S+/g);
|
|
1185
|
+
if (tmpMatches)
|
|
1186
|
+
{
|
|
1187
|
+
tmpWords += tmpMatches.length;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
else if (tmpActiveEditor === 'code')
|
|
1194
|
+
{
|
|
1195
|
+
let tmpCodeEditorView = this.pict.views['ContentEditor-CodeEditor'];
|
|
1196
|
+
if (tmpCodeEditorView && tmpCodeEditorView.codeJar)
|
|
1197
|
+
{
|
|
1198
|
+
let tmpText = tmpCodeEditorView.codeJar.toString();
|
|
1199
|
+
tmpChars = tmpText.length;
|
|
1200
|
+
tmpLines = tmpText.split('\n').length;
|
|
1201
|
+
let tmpMatches = tmpText.match(/\S+/g);
|
|
1202
|
+
if (tmpMatches)
|
|
1203
|
+
{
|
|
1204
|
+
tmpWords = tmpMatches.length;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
else
|
|
1209
|
+
{
|
|
1210
|
+
// Binary or no file — clear stats
|
|
1211
|
+
tmpStatsEl.textContent = '';
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
tmpStatsEl.textContent = tmpLines + ' lines \u00B7 ' + tmpWords + ' words \u00B7 ' + tmpChars + ' chars';
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* Mark the document as dirty (unsaved changes).
|
|
1220
|
+
*/
|
|
1221
|
+
markDirty()
|
|
1222
|
+
{
|
|
1223
|
+
if (!this.pict.AppData.ContentEditor.IsDirty)
|
|
1224
|
+
{
|
|
1225
|
+
this.pict.AppData.ContentEditor.IsDirty = true;
|
|
1226
|
+
this.pict.views['ContentEditor-TopBar'].render();
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* The localStorage key used for persisting editor settings.
|
|
1232
|
+
*/
|
|
1233
|
+
get _settingsKey()
|
|
1234
|
+
{
|
|
1235
|
+
return 'retold-content-editor-settings';
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Persist the current editor settings to localStorage.
|
|
1240
|
+
*/
|
|
1241
|
+
saveSettings()
|
|
1242
|
+
{
|
|
1243
|
+
if (typeof (window) === 'undefined' || !window.localStorage)
|
|
1244
|
+
{
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
let tmpSettings = this.pict.AppData.ContentEditor;
|
|
1249
|
+
|
|
1250
|
+
let tmpData =
|
|
1251
|
+
{
|
|
1252
|
+
AutoSegmentMarkdown: tmpSettings.AutoSegmentMarkdown,
|
|
1253
|
+
AutoSegmentDepth: tmpSettings.AutoSegmentDepth,
|
|
1254
|
+
AutoContentPreview: tmpSettings.AutoContentPreview,
|
|
1255
|
+
MarkdownEditingControls: tmpSettings.MarkdownEditingControls,
|
|
1256
|
+
MarkdownWordWrap: tmpSettings.MarkdownWordWrap,
|
|
1257
|
+
CodeWordWrap: tmpSettings.CodeWordWrap,
|
|
1258
|
+
SidebarCollapsed: tmpSettings.SidebarCollapsed,
|
|
1259
|
+
SidebarWidth: tmpSettings.SidebarWidth,
|
|
1260
|
+
AutoPreviewImages: tmpSettings.AutoPreviewImages,
|
|
1261
|
+
AutoPreviewVideo: tmpSettings.AutoPreviewVideo,
|
|
1262
|
+
AutoPreviewAudio: tmpSettings.AutoPreviewAudio,
|
|
1263
|
+
ShowHiddenFiles: tmpSettings.ShowHiddenFiles,
|
|
1264
|
+
TopicsFilePath: tmpSettings.TopicsFilePath
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1267
|
+
try
|
|
1268
|
+
{
|
|
1269
|
+
window.localStorage.setItem(this._settingsKey, JSON.stringify(tmpData));
|
|
1270
|
+
}
|
|
1271
|
+
catch (pError)
|
|
1272
|
+
{
|
|
1273
|
+
this.log.warn('Failed to save settings: ' + pError.message);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* Load editor settings from localStorage, overwriting the
|
|
1279
|
+
* current defaults for any keys that are present.
|
|
1280
|
+
*/
|
|
1281
|
+
_loadSettings()
|
|
1282
|
+
{
|
|
1283
|
+
if (typeof (window) === 'undefined' || !window.localStorage)
|
|
1284
|
+
{
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
try
|
|
1289
|
+
{
|
|
1290
|
+
let tmpRaw = window.localStorage.getItem(this._settingsKey);
|
|
1291
|
+
if (!tmpRaw)
|
|
1292
|
+
{
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
let tmpStored = JSON.parse(tmpRaw);
|
|
1297
|
+
let tmpSettings = this.pict.AppData.ContentEditor;
|
|
1298
|
+
|
|
1299
|
+
if (typeof (tmpStored.AutoSegmentMarkdown) === 'boolean')
|
|
1300
|
+
{
|
|
1301
|
+
tmpSettings.AutoSegmentMarkdown = tmpStored.AutoSegmentMarkdown;
|
|
1302
|
+
}
|
|
1303
|
+
if (typeof (tmpStored.AutoSegmentDepth) === 'number')
|
|
1304
|
+
{
|
|
1305
|
+
tmpSettings.AutoSegmentDepth = tmpStored.AutoSegmentDepth;
|
|
1306
|
+
}
|
|
1307
|
+
if (typeof (tmpStored.AutoContentPreview) === 'boolean')
|
|
1308
|
+
{
|
|
1309
|
+
tmpSettings.AutoContentPreview = tmpStored.AutoContentPreview;
|
|
1310
|
+
}
|
|
1311
|
+
if (typeof (tmpStored.MarkdownEditingControls) === 'boolean')
|
|
1312
|
+
{
|
|
1313
|
+
tmpSettings.MarkdownEditingControls = tmpStored.MarkdownEditingControls;
|
|
1314
|
+
}
|
|
1315
|
+
if (typeof (tmpStored.MarkdownWordWrap) === 'boolean')
|
|
1316
|
+
{
|
|
1317
|
+
tmpSettings.MarkdownWordWrap = tmpStored.MarkdownWordWrap;
|
|
1318
|
+
}
|
|
1319
|
+
if (typeof (tmpStored.CodeWordWrap) === 'boolean')
|
|
1320
|
+
{
|
|
1321
|
+
tmpSettings.CodeWordWrap = tmpStored.CodeWordWrap;
|
|
1322
|
+
}
|
|
1323
|
+
if (typeof (tmpStored.SidebarCollapsed) === 'boolean')
|
|
1324
|
+
{
|
|
1325
|
+
tmpSettings.SidebarCollapsed = tmpStored.SidebarCollapsed;
|
|
1326
|
+
}
|
|
1327
|
+
if (typeof (tmpStored.SidebarWidth) === 'number')
|
|
1328
|
+
{
|
|
1329
|
+
tmpSettings.SidebarWidth = tmpStored.SidebarWidth;
|
|
1330
|
+
}
|
|
1331
|
+
if (typeof (tmpStored.AutoPreviewImages) === 'boolean')
|
|
1332
|
+
{
|
|
1333
|
+
tmpSettings.AutoPreviewImages = tmpStored.AutoPreviewImages;
|
|
1334
|
+
}
|
|
1335
|
+
if (typeof (tmpStored.AutoPreviewVideo) === 'boolean')
|
|
1336
|
+
{
|
|
1337
|
+
tmpSettings.AutoPreviewVideo = tmpStored.AutoPreviewVideo;
|
|
1338
|
+
}
|
|
1339
|
+
if (typeof (tmpStored.AutoPreviewAudio) === 'boolean')
|
|
1340
|
+
{
|
|
1341
|
+
tmpSettings.AutoPreviewAudio = tmpStored.AutoPreviewAudio;
|
|
1342
|
+
}
|
|
1343
|
+
if (typeof (tmpStored.ShowHiddenFiles) === 'boolean')
|
|
1344
|
+
{
|
|
1345
|
+
tmpSettings.ShowHiddenFiles = tmpStored.ShowHiddenFiles;
|
|
1346
|
+
}
|
|
1347
|
+
if (typeof (tmpStored.TopicsFilePath) === 'string')
|
|
1348
|
+
{
|
|
1349
|
+
tmpSettings.TopicsFilePath = tmpStored.TopicsFilePath;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
catch (pError)
|
|
1353
|
+
{
|
|
1354
|
+
this.log.warn('Failed to load settings: ' + pError.message);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
module.exports = ContentEditorApplication;
|
|
1360
|
+
|
|
1361
|
+
module.exports.default_configuration = require('./Pict-Application-ContentEditor-Configuration.json');
|