@lemonppt/renderer 0.1.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/dist/editor-script.d.ts +2 -0
- package/dist/editor-script.d.ts.map +1 -0
- package/dist/editor-script.js +876 -0
- package/dist/editor-script.js.map +1 -0
- package/dist/export-pdf.d.ts +11 -0
- package/dist/export-pdf.d.ts.map +1 -0
- package/dist/export-pdf.js +28 -0
- package/dist/export-pdf.js.map +1 -0
- package/dist/export-pptx.d.ts +9 -0
- package/dist/export-pptx.d.ts.map +1 -0
- package/dist/export-pptx.js +845 -0
- package/dist/export-pptx.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/render.d.ts +11 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +752 -0
- package/dist/render.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
export const editorScript = `
|
|
2
|
+
(function () {
|
|
3
|
+
const STORAGE_KEY = 'lemonppt:editor:' + (window.__lemonPPT_goal?.randomSeed || window.__lemonPPT_goal?.title || 'default');
|
|
4
|
+
const MAX_HISTORY = 50;
|
|
5
|
+
|
|
6
|
+
let goal = window.__lemonPPT_goal;
|
|
7
|
+
if (!goal) return;
|
|
8
|
+
|
|
9
|
+
// 优先从 localStorage 恢复
|
|
10
|
+
try {
|
|
11
|
+
const saved = localStorage.getItem(STORAGE_KEY);
|
|
12
|
+
if (saved) {
|
|
13
|
+
const parsed = JSON.parse(saved);
|
|
14
|
+
if (parsed && parsed.slides) {
|
|
15
|
+
goal = parsed;
|
|
16
|
+
window.__lemonPPT_goal = goal;
|
|
17
|
+
syncDomFromGoal();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.warn('自动恢复失败', err);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const history = [deepClone(goal)];
|
|
25
|
+
let historyIndex = 0;
|
|
26
|
+
|
|
27
|
+
function deepClone(obj) {
|
|
28
|
+
return JSON.parse(JSON.stringify(obj));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function recordHistory() {
|
|
32
|
+
// 截断 redo 分支
|
|
33
|
+
if (historyIndex < history.length - 1) {
|
|
34
|
+
history.splice(historyIndex + 1);
|
|
35
|
+
}
|
|
36
|
+
history.push(deepClone(goal));
|
|
37
|
+
if (history.length > MAX_HISTORY) {
|
|
38
|
+
history.shift();
|
|
39
|
+
} else {
|
|
40
|
+
historyIndex++;
|
|
41
|
+
}
|
|
42
|
+
updateUndoRedoButtons();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function undo() {
|
|
46
|
+
if (historyIndex <= 0) return;
|
|
47
|
+
historyIndex--;
|
|
48
|
+
restoreGoal(history[historyIndex]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function redo() {
|
|
52
|
+
if (historyIndex >= history.length - 1) return;
|
|
53
|
+
historyIndex++;
|
|
54
|
+
restoreGoal(history[historyIndex]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function restoreGoal(newGoal) {
|
|
58
|
+
goal = newGoal;
|
|
59
|
+
window.__lemonPPT_goal = goal;
|
|
60
|
+
Object.assign(window.__lemonPPT_goal, newGoal);
|
|
61
|
+
syncDomFromGoal();
|
|
62
|
+
autoSave();
|
|
63
|
+
updateUndoRedoButtons();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function autoSave() {
|
|
67
|
+
try {
|
|
68
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(goal));
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.warn('自动保存失败', err);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function syncDomFromGoal() {
|
|
75
|
+
// 文本
|
|
76
|
+
document.querySelectorAll('[data-lp-editable="true"]').forEach((el) => {
|
|
77
|
+
const slideIdx = Number(el.getAttribute('data-lp-slide-idx'));
|
|
78
|
+
const prop = el.getAttribute('data-lp-prop');
|
|
79
|
+
if (Number.isNaN(slideIdx) || !prop) return;
|
|
80
|
+
const slide = goal.slides[slideIdx];
|
|
81
|
+
if (!slide) return;
|
|
82
|
+
const value = getProp(slide.props, prop);
|
|
83
|
+
el.textContent = value == null ? '' : String(value);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// 图片
|
|
87
|
+
document.querySelectorAll('[data-lp-editable-image="true"]').forEach((img) => {
|
|
88
|
+
const slideIdx = Number(img.getAttribute('data-lp-slide-idx'));
|
|
89
|
+
const prop = img.getAttribute('data-lp-prop') || 'image';
|
|
90
|
+
if (Number.isNaN(slideIdx)) return;
|
|
91
|
+
const slide = goal.slides[slideIdx];
|
|
92
|
+
if (!slide) return;
|
|
93
|
+
const value = getProp(slide.props, prop);
|
|
94
|
+
if (value) {
|
|
95
|
+
img.setAttribute('src', String(value));
|
|
96
|
+
} else {
|
|
97
|
+
img.remove();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getProp(obj, path) {
|
|
103
|
+
const keys = path.split('.');
|
|
104
|
+
let target = obj;
|
|
105
|
+
for (const k of keys) {
|
|
106
|
+
if (target == null || typeof target !== 'object') return undefined;
|
|
107
|
+
target = target[k];
|
|
108
|
+
}
|
|
109
|
+
return target;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function setProp(obj, path, value) {
|
|
113
|
+
const keys = path.split('.');
|
|
114
|
+
let target = obj;
|
|
115
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
116
|
+
const k = keys[i];
|
|
117
|
+
if (!(k in target)) target[k] = [];
|
|
118
|
+
target = target[k];
|
|
119
|
+
}
|
|
120
|
+
target[keys[keys.length - 1]] = value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 图片编辑弹窗
|
|
124
|
+
function openImageEditor(img, slideIdx, prop) {
|
|
125
|
+
const slide = goal.slides[slideIdx];
|
|
126
|
+
if (!slide) return;
|
|
127
|
+
|
|
128
|
+
const overlay = document.createElement('div');
|
|
129
|
+
overlay.className = 'lp-image-editor-overlay';
|
|
130
|
+
overlay.innerHTML = \`
|
|
131
|
+
<div class="lp-image-editor">
|
|
132
|
+
<h3>编辑图片</h3>
|
|
133
|
+
<label>
|
|
134
|
+
<span>图片 URL</span>
|
|
135
|
+
<input type="text" class="lp-image-url" placeholder="https://..." value="\${escapeHtml(img.getAttribute('src') || '')}">
|
|
136
|
+
</label>
|
|
137
|
+
<div class="lp-image-or">或</div>
|
|
138
|
+
<label class="lp-image-file-label">
|
|
139
|
+
<span>上传本地图片</span>
|
|
140
|
+
<input type="file" class="lp-image-file" accept="image/*">
|
|
141
|
+
</label>
|
|
142
|
+
<div class="lp-image-preview-wrap">
|
|
143
|
+
<img class="lp-image-preview" src="\${escapeHtml(img.getAttribute('src') || '')}" alt="">
|
|
144
|
+
</div>
|
|
145
|
+
<div class="lp-image-actions">
|
|
146
|
+
<button class="lp-image-delete lp-image-btn-secondary">删除图片</button>
|
|
147
|
+
<div class="lp-image-actions-right">
|
|
148
|
+
<button class="lp-image-cancel lp-image-btn-secondary">取消</button>
|
|
149
|
+
<button class="lp-image-confirm">确认</button>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
\`;
|
|
154
|
+
|
|
155
|
+
document.body.appendChild(overlay);
|
|
156
|
+
|
|
157
|
+
const urlInput = overlay.querySelector('.lp-image-url');
|
|
158
|
+
const fileInput = overlay.querySelector('.lp-image-file');
|
|
159
|
+
const preview = overlay.querySelector('.lp-image-preview');
|
|
160
|
+
const confirmBtn = overlay.querySelector('.lp-image-confirm');
|
|
161
|
+
const cancelBtn = overlay.querySelector('.lp-image-cancel');
|
|
162
|
+
const deleteBtn = overlay.querySelector('.lp-image-delete');
|
|
163
|
+
|
|
164
|
+
let pendingValue = img.getAttribute('src') || '';
|
|
165
|
+
|
|
166
|
+
urlInput.addEventListener('input', () => {
|
|
167
|
+
pendingValue = urlInput.value;
|
|
168
|
+
preview.src = pendingValue;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
fileInput.addEventListener('change', () => {
|
|
172
|
+
const file = fileInput.files[0];
|
|
173
|
+
if (!file) return;
|
|
174
|
+
const reader = new FileReader();
|
|
175
|
+
reader.onload = (e) => {
|
|
176
|
+
pendingValue = e.target.result;
|
|
177
|
+
urlInput.value = '';
|
|
178
|
+
preview.src = pendingValue;
|
|
179
|
+
};
|
|
180
|
+
reader.readAsDataURL(file);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
function close() {
|
|
184
|
+
overlay.remove();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function apply() {
|
|
188
|
+
recordHistory();
|
|
189
|
+
if (pendingValue) {
|
|
190
|
+
img.setAttribute('src', pendingValue);
|
|
191
|
+
setProp(slide.props, prop, pendingValue);
|
|
192
|
+
} else {
|
|
193
|
+
img.remove();
|
|
194
|
+
setProp(slide.props, prop, undefined);
|
|
195
|
+
}
|
|
196
|
+
autoSave();
|
|
197
|
+
close();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function remove() {
|
|
201
|
+
recordHistory();
|
|
202
|
+
img.remove();
|
|
203
|
+
setProp(slide.props, prop, undefined);
|
|
204
|
+
autoSave();
|
|
205
|
+
close();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
confirmBtn.addEventListener('click', apply);
|
|
209
|
+
cancelBtn.addEventListener('click', close);
|
|
210
|
+
deleteBtn.addEventListener('click', remove);
|
|
211
|
+
overlay.addEventListener('click', (e) => {
|
|
212
|
+
if (e.target === overlay) close();
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function escapeHtml(str) {
|
|
217
|
+
return String(str)
|
|
218
|
+
.replace(/&/g, '&')
|
|
219
|
+
.replace(/</g, '<')
|
|
220
|
+
.replace(/>/g, '>')
|
|
221
|
+
.replace(/"/g, '"')
|
|
222
|
+
.replace(/'/g, ''');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const editableEls = document.querySelectorAll('[data-lp-editable="true"]');
|
|
226
|
+
|
|
227
|
+
editableEls.forEach((el) => {
|
|
228
|
+
el.setAttribute('contenteditable', 'true');
|
|
229
|
+
|
|
230
|
+
el.addEventListener('focus', () => {
|
|
231
|
+
recordHistory();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
el.addEventListener('blur', () => {
|
|
235
|
+
const slideIdx = Number(el.getAttribute('data-lp-slide-idx'));
|
|
236
|
+
const prop = el.getAttribute('data-lp-prop');
|
|
237
|
+
if (Number.isNaN(slideIdx) || !prop) return;
|
|
238
|
+
|
|
239
|
+
const slide = goal.slides[slideIdx];
|
|
240
|
+
if (!slide) return;
|
|
241
|
+
|
|
242
|
+
const value = el.textContent || '';
|
|
243
|
+
setProp(slide.props, prop, value);
|
|
244
|
+
autoSave();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
el.addEventListener('keydown', (e) => {
|
|
248
|
+
if (e.key === 'Enter') {
|
|
249
|
+
e.preventDefault();
|
|
250
|
+
el.blur();
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// 图片换图
|
|
256
|
+
const imageEls = document.querySelectorAll('[data-lp-editable-image="true"]');
|
|
257
|
+
imageEls.forEach((img) => {
|
|
258
|
+
img.addEventListener('click', () => {
|
|
259
|
+
const slideIdx = Number(img.getAttribute('data-lp-slide-idx'));
|
|
260
|
+
const prop = img.getAttribute('data-lp-prop') || 'image';
|
|
261
|
+
if (Number.isNaN(slideIdx)) return;
|
|
262
|
+
openImageEditor(img, slideIdx, prop);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const downloadBtn = document.getElementById('lp-download-goal');
|
|
267
|
+
if (downloadBtn) {
|
|
268
|
+
downloadBtn.addEventListener('click', () => {
|
|
269
|
+
const blob = new Blob([JSON.stringify(goal, null, 2)], { type: 'application/json' });
|
|
270
|
+
const url = URL.createObjectURL(blob);
|
|
271
|
+
const a = document.createElement('a');
|
|
272
|
+
a.href = url;
|
|
273
|
+
a.download = 'goal.json';
|
|
274
|
+
a.click();
|
|
275
|
+
URL.revokeObjectURL(url);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const exportPptxBtn = document.getElementById('lp-export-pptx');
|
|
280
|
+
if (exportPptxBtn) {
|
|
281
|
+
exportPptxBtn.addEventListener('click', async () => {
|
|
282
|
+
exportPptxBtn.textContent = '导出中...';
|
|
283
|
+
exportPptxBtn.disabled = true;
|
|
284
|
+
try {
|
|
285
|
+
const res = await fetch('/api/export/pptx', {
|
|
286
|
+
method: 'POST',
|
|
287
|
+
headers: { 'Content-Type': 'application/json' },
|
|
288
|
+
body: JSON.stringify(goal),
|
|
289
|
+
});
|
|
290
|
+
if (!res.ok) throw new Error('导出失败: ' + res.status);
|
|
291
|
+
const blob = await res.blob();
|
|
292
|
+
const url = URL.createObjectURL(blob);
|
|
293
|
+
const a = document.createElement('a');
|
|
294
|
+
a.href = url;
|
|
295
|
+
a.download = 'presentation.pptx';
|
|
296
|
+
a.click();
|
|
297
|
+
URL.revokeObjectURL(url);
|
|
298
|
+
} catch (err) {
|
|
299
|
+
alert(err instanceof Error ? err.message : String(err));
|
|
300
|
+
} finally {
|
|
301
|
+
exportPptxBtn.textContent = '导出 PPTX';
|
|
302
|
+
exportPptxBtn.disabled = false;
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const exportPdfBtn = document.getElementById('lp-export-pdf');
|
|
308
|
+
if (exportPdfBtn) {
|
|
309
|
+
exportPdfBtn.addEventListener('click', async () => {
|
|
310
|
+
exportPdfBtn.textContent = '导出中...';
|
|
311
|
+
exportPdfBtn.disabled = true;
|
|
312
|
+
try {
|
|
313
|
+
const res = await fetch('/api/export/pdf', {
|
|
314
|
+
method: 'POST',
|
|
315
|
+
headers: { 'Content-Type': 'application/json' },
|
|
316
|
+
body: JSON.stringify(goal),
|
|
317
|
+
});
|
|
318
|
+
if (!res.ok) throw new Error('导出失败: ' + res.status);
|
|
319
|
+
const blob = await res.blob();
|
|
320
|
+
const url = URL.createObjectURL(blob);
|
|
321
|
+
const a = document.createElement('a');
|
|
322
|
+
a.href = url;
|
|
323
|
+
a.download = 'presentation.pdf';
|
|
324
|
+
a.click();
|
|
325
|
+
URL.revokeObjectURL(url);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
alert(err instanceof Error ? err.message : String(err));
|
|
328
|
+
} finally {
|
|
329
|
+
exportPdfBtn.textContent = '导出 PDF';
|
|
330
|
+
exportPdfBtn.disabled = false;
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 撤销 / 重做
|
|
336
|
+
const undoBtn = document.getElementById('lp-undo');
|
|
337
|
+
const redoBtn = document.getElementById('lp-redo');
|
|
338
|
+
|
|
339
|
+
function updateUndoRedoButtons() {
|
|
340
|
+
if (undoBtn) undoBtn.disabled = historyIndex <= 0;
|
|
341
|
+
if (redoBtn) redoBtn.disabled = historyIndex >= history.length - 1;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (undoBtn) undoBtn.addEventListener('click', undo);
|
|
345
|
+
if (redoBtn) redoBtn.addEventListener('click', redo);
|
|
346
|
+
|
|
347
|
+
// 主题切换
|
|
348
|
+
const themeSwitcher = document.getElementById('lp-theme-switcher');
|
|
349
|
+
if (themeSwitcher) {
|
|
350
|
+
themeSwitcher.value = goal.theme || 'minimal';
|
|
351
|
+
themeSwitcher.addEventListener('change', () => {
|
|
352
|
+
const newTheme = themeSwitcher.value;
|
|
353
|
+
if (newTheme === goal.theme) return;
|
|
354
|
+
goal.theme = newTheme;
|
|
355
|
+
autoSave();
|
|
356
|
+
window.location.href = '/editor?theme=' + encodeURIComponent(newTheme);
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
document.addEventListener('keydown', (e) => {
|
|
361
|
+
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
|
362
|
+
const mod = isMac ? e.metaKey : e.ctrlKey;
|
|
363
|
+
if (mod && e.key.toLowerCase() === 'z') {
|
|
364
|
+
e.preventDefault();
|
|
365
|
+
if (e.shiftKey) redo();
|
|
366
|
+
else undo();
|
|
367
|
+
}
|
|
368
|
+
if (mod && e.key.toLowerCase() === 'y') {
|
|
369
|
+
e.preventDefault();
|
|
370
|
+
redo();
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
updateUndoRedoButtons();
|
|
375
|
+
|
|
376
|
+
// 翻页脚本
|
|
377
|
+
const slides = document.querySelectorAll('.lp-slide-wrapper');
|
|
378
|
+
const thumbnails = document.querySelectorAll('.lp-thumbnail');
|
|
379
|
+
const prevBtn = document.getElementById('lp-prev');
|
|
380
|
+
const nextBtn = document.getElementById('lp-next');
|
|
381
|
+
const currentLabel = document.getElementById('lp-current');
|
|
382
|
+
let current = 0;
|
|
383
|
+
|
|
384
|
+
function updateClasses() {
|
|
385
|
+
slides.forEach((slide, index) => {
|
|
386
|
+
slide.classList.remove('active', 'prev');
|
|
387
|
+
if (index === current) slide.classList.add('active');
|
|
388
|
+
else if (index < current) slide.classList.add('prev');
|
|
389
|
+
});
|
|
390
|
+
thumbnails.forEach((thumb, index) => {
|
|
391
|
+
thumb.classList.toggle('active', index === current);
|
|
392
|
+
});
|
|
393
|
+
const activeThumb = thumbnails[current];
|
|
394
|
+
if (activeThumb) activeThumb.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
395
|
+
if (currentLabel) currentLabel.textContent = String(current + 1);
|
|
396
|
+
if (prevBtn) prevBtn.disabled = current === 0;
|
|
397
|
+
if (nextBtn) nextBtn.disabled = current === slides.length - 1;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function goTo(index) {
|
|
401
|
+
if (index < 0 || index >= slides.length) return;
|
|
402
|
+
current = index;
|
|
403
|
+
updateClasses();
|
|
404
|
+
if (typeof selectSlide === 'function') selectSlide(current);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (prevBtn) prevBtn.addEventListener('click', () => goTo(current - 1));
|
|
408
|
+
if (nextBtn) nextBtn.addEventListener('click', () => goTo(current + 1));
|
|
409
|
+
thumbnails.forEach((thumb) => {
|
|
410
|
+
thumb.addEventListener('click', () => goTo(Number(thumb.dataset.index)));
|
|
411
|
+
});
|
|
412
|
+
function isEditingTarget(target) {
|
|
413
|
+
if (!target || !target.closest) return false;
|
|
414
|
+
return target.closest('[contenteditable="true"]') || target.closest('.lp-image-editor');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
document.addEventListener('keydown', (e) => {
|
|
418
|
+
if (isEditingTarget(e.target)) return;
|
|
419
|
+
if (e.key === 'ArrowRight' || e.key === ' ' || e.key === 'PageDown') goTo(current + 1);
|
|
420
|
+
if (e.key === 'ArrowLeft' || e.key === 'PageUp') goTo(current - 1);
|
|
421
|
+
if (e.key === 'Home') goTo(0);
|
|
422
|
+
if (e.key === 'End') goTo(slides.length - 1);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
updateClasses();
|
|
426
|
+
|
|
427
|
+
// 缩放控制
|
|
428
|
+
const stage = document.querySelector('.lp-editor-stage');
|
|
429
|
+
const scaler = document.querySelector('.lp-editor-stage-scaler');
|
|
430
|
+
const zoomSlider = document.getElementById('lp-zoom-slider');
|
|
431
|
+
const zoomValue = document.getElementById('lp-zoom-value');
|
|
432
|
+
const zoomOutBtn = document.getElementById('lp-zoom-out');
|
|
433
|
+
const zoomInBtn = document.getElementById('lp-zoom-in');
|
|
434
|
+
const zoomFitBtn = document.getElementById('lp-zoom-fit');
|
|
435
|
+
let userZoom = null;
|
|
436
|
+
|
|
437
|
+
function fitScale() {
|
|
438
|
+
if (!stage || !scaler) return 1;
|
|
439
|
+
return Math.min(stage.clientWidth / 1280, stage.clientHeight / 720) * 0.92;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function updateScale() {
|
|
443
|
+
if (!stage || !scaler) return;
|
|
444
|
+
const scale = userZoom == null ? fitScale() : userZoom;
|
|
445
|
+
scaler.style.transform = 'scale(' + Math.max(scale, 0.35) + ')';
|
|
446
|
+
if (zoomValue) zoomValue.textContent = Math.round(scale * 100) + '%';
|
|
447
|
+
if (zoomSlider && userZoom != null) zoomSlider.value = String(Math.round(scale * 100));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (zoomSlider) {
|
|
451
|
+
zoomSlider.addEventListener('input', () => {
|
|
452
|
+
userZoom = Number(zoomSlider.value) / 100;
|
|
453
|
+
updateScale();
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
if (zoomOutBtn) {
|
|
457
|
+
zoomOutBtn.addEventListener('click', () => {
|
|
458
|
+
userZoom = (userZoom == null ? fitScale() : userZoom) - 0.1;
|
|
459
|
+
if (userZoom < 0.35) userZoom = 0.35;
|
|
460
|
+
updateScale();
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
if (zoomInBtn) {
|
|
464
|
+
zoomInBtn.addEventListener('click', () => {
|
|
465
|
+
userZoom = (userZoom == null ? fitScale() : userZoom) + 0.1;
|
|
466
|
+
if (userZoom > 1.5) userZoom = 1.5;
|
|
467
|
+
updateScale();
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
if (zoomFitBtn) {
|
|
471
|
+
zoomFitBtn.addEventListener('click', () => {
|
|
472
|
+
userZoom = null;
|
|
473
|
+
updateScale();
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
window.addEventListener('resize', updateScale);
|
|
477
|
+
updateScale();
|
|
478
|
+
|
|
479
|
+
// 播放:先渲染静态版本再打开
|
|
480
|
+
const playBtn = document.getElementById('lp-play');
|
|
481
|
+
if (playBtn) {
|
|
482
|
+
playBtn.addEventListener('click', async () => {
|
|
483
|
+
playBtn.textContent = '准备中...';
|
|
484
|
+
playBtn.disabled = true;
|
|
485
|
+
try {
|
|
486
|
+
const res = await fetch('/api/render', {
|
|
487
|
+
method: 'POST',
|
|
488
|
+
headers: { 'Content-Type': 'application/json' },
|
|
489
|
+
body: JSON.stringify(goal),
|
|
490
|
+
});
|
|
491
|
+
if (!res.ok) throw new Error('渲染失败: ' + res.status);
|
|
492
|
+
window.open('/deck/index.html', '_blank');
|
|
493
|
+
} catch (err) {
|
|
494
|
+
alert(err instanceof Error ? err.message : String(err));
|
|
495
|
+
} finally {
|
|
496
|
+
playBtn.textContent = '▶ 播放';
|
|
497
|
+
playBtn.disabled = false;
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// 内容结构变更后重新渲染当前编辑器(仅用于数组增删等模板内容调整)
|
|
503
|
+
function saveCurrentForReload() {
|
|
504
|
+
try {
|
|
505
|
+
localStorage.setItem('lemonppt:editor:currentSlide', String(current));
|
|
506
|
+
} catch (e) {}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function reloadEditor() {
|
|
510
|
+
saveCurrentForReload();
|
|
511
|
+
try {
|
|
512
|
+
const res = await fetch('/api/render-editor', {
|
|
513
|
+
method: 'POST',
|
|
514
|
+
headers: { 'Content-Type': 'application/json' },
|
|
515
|
+
body: JSON.stringify(goal),
|
|
516
|
+
});
|
|
517
|
+
if (!res.ok) throw new Error('重新渲染失败: ' + res.status);
|
|
518
|
+
window.location.reload();
|
|
519
|
+
} catch (err) {
|
|
520
|
+
alert(err instanceof Error ? err.message : String(err));
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// 元素选中与右侧属性面板
|
|
525
|
+
let selectedEl = null;
|
|
526
|
+
let selectedSlideIdx = 0;
|
|
527
|
+
const propertyContent = document.getElementById('lp-property-content');
|
|
528
|
+
|
|
529
|
+
const FIELD_LABELS = {
|
|
530
|
+
kicker: '标签',
|
|
531
|
+
title: '标题',
|
|
532
|
+
subtitle: '副标题',
|
|
533
|
+
date: '日期',
|
|
534
|
+
quote: '引用',
|
|
535
|
+
author: '作者',
|
|
536
|
+
role: '职位',
|
|
537
|
+
company: '公司',
|
|
538
|
+
value: '数值',
|
|
539
|
+
unit: '单位',
|
|
540
|
+
label: '指标名',
|
|
541
|
+
description: '说明',
|
|
542
|
+
change: '变化',
|
|
543
|
+
imageUrl: '图片',
|
|
544
|
+
url: '图片',
|
|
545
|
+
logoUrl: 'Logo',
|
|
546
|
+
avatarUrl: '头像',
|
|
547
|
+
caption: '说明',
|
|
548
|
+
q: '问题',
|
|
549
|
+
a: '回答',
|
|
550
|
+
question: '问题',
|
|
551
|
+
answer: '回答',
|
|
552
|
+
name: '名称',
|
|
553
|
+
price: '价格',
|
|
554
|
+
period: '周期',
|
|
555
|
+
cta: '按钮文案',
|
|
556
|
+
type: '图表类型',
|
|
557
|
+
status: '状态',
|
|
558
|
+
bio: '简介',
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
function getFieldLabel(path) {
|
|
562
|
+
const key = String(path).split('.').pop() || '';
|
|
563
|
+
return FIELD_LABELS[key] || key;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function inferFieldType(path, value) {
|
|
567
|
+
const key = String(path).split('.').pop() || '';
|
|
568
|
+
if (/image|url|logo|avatar/i.test(key) && typeof value === 'string') return 'image';
|
|
569
|
+
if (typeof value === 'boolean') return 'boolean';
|
|
570
|
+
if (typeof value === 'number') return 'number';
|
|
571
|
+
if (key === 'type' && ['bar', 'line', 'pie'].includes(String(value))) return 'select';
|
|
572
|
+
if (key === 'status') return 'select';
|
|
573
|
+
if (['description', 'bio', 'quote', 'answer', 'a', 'subtitle'].includes(key)) return 'textarea';
|
|
574
|
+
return 'text';
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function getSelectOptions(path) {
|
|
578
|
+
const key = String(path).split('.').pop() || '';
|
|
579
|
+
if (key === 'type') return [{ value: 'bar', label: '柱状' }, { value: 'line', label: '折线' }, { value: 'pie', label: '饼图' }];
|
|
580
|
+
if (key === 'status') return [{ value: '已完成', label: '已完成' }, { value: '进行中', label: '进行中' }, { value: '规划中', label: '规划中' }];
|
|
581
|
+
return [];
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function clearSelection() {
|
|
585
|
+
if (selectedEl) {
|
|
586
|
+
selectedEl.classList.remove('lp-selected');
|
|
587
|
+
selectedEl = null;
|
|
588
|
+
}
|
|
589
|
+
selectedSlideIdx = current;
|
|
590
|
+
renderSlidePanel();
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function setField(path, value) {
|
|
594
|
+
const slide = goal.slides[selectedSlideIdx];
|
|
595
|
+
if (!slide) return;
|
|
596
|
+
recordHistory();
|
|
597
|
+
setProp(slide.props, path, value);
|
|
598
|
+
syncDomFromGoal();
|
|
599
|
+
autoSave();
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function createEl(tag, className, parent) {
|
|
603
|
+
const el = document.createElement(tag);
|
|
604
|
+
if (className) el.className = className;
|
|
605
|
+
if (parent) parent.appendChild(el);
|
|
606
|
+
return el;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function createTextField(label, value, onChange) {
|
|
610
|
+
const wrap = createEl('div', 'lp-property-field');
|
|
611
|
+
createEl('label', 'lp-property-label', wrap).textContent = label;
|
|
612
|
+
const input = createEl('input', 'lp-property-input', wrap);
|
|
613
|
+
input.type = 'text';
|
|
614
|
+
input.value = value == null ? '' : String(value);
|
|
615
|
+
input.addEventListener('input', () => onChange(input.value));
|
|
616
|
+
return wrap;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function createTextareaField(label, value, onChange) {
|
|
620
|
+
const wrap = createEl('div', 'lp-property-field');
|
|
621
|
+
createEl('label', 'lp-property-label', wrap).textContent = label;
|
|
622
|
+
const textarea = createEl('textarea', 'lp-property-textarea', wrap);
|
|
623
|
+
textarea.value = value == null ? '' : String(value);
|
|
624
|
+
textarea.addEventListener('input', () => onChange(textarea.value));
|
|
625
|
+
return wrap;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function createNumberField(label, value, onChange) {
|
|
629
|
+
const wrap = createEl('div', 'lp-property-field');
|
|
630
|
+
createEl('label', 'lp-property-label', wrap).textContent = label;
|
|
631
|
+
const input = createEl('input', 'lp-property-input', wrap);
|
|
632
|
+
input.type = 'number';
|
|
633
|
+
input.value = value == null ? '' : String(value);
|
|
634
|
+
input.addEventListener('input', () => onChange(Number(input.value)));
|
|
635
|
+
return wrap;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function createToggleField(label, value, onChange) {
|
|
639
|
+
const wrap = createEl('div', 'lp-property-field');
|
|
640
|
+
const labelEl = createEl('label', 'lp-property-toggle', wrap);
|
|
641
|
+
const text = createEl('span', '', labelEl);
|
|
642
|
+
text.textContent = label;
|
|
643
|
+
const input = createEl('input', '', labelEl);
|
|
644
|
+
input.type = 'checkbox';
|
|
645
|
+
input.checked = !!value;
|
|
646
|
+
const track = createEl('div', 'lp-property-toggle-track', labelEl);
|
|
647
|
+
createEl('div', 'lp-property-toggle-thumb', track);
|
|
648
|
+
input.addEventListener('change', () => onChange(input.checked));
|
|
649
|
+
return wrap;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function createSelectField(label, value, options, onChange) {
|
|
653
|
+
const wrap = createEl('div', 'lp-property-field');
|
|
654
|
+
createEl('label', 'lp-property-label', wrap).textContent = label;
|
|
655
|
+
const segmented = createEl('div', 'lp-property-segmented', wrap);
|
|
656
|
+
options.forEach((opt) => {
|
|
657
|
+
const btn = createEl('button', '', segmented);
|
|
658
|
+
btn.textContent = opt.label;
|
|
659
|
+
btn.type = 'button';
|
|
660
|
+
if (opt.value === value) btn.classList.add('active');
|
|
661
|
+
btn.addEventListener('click', () => {
|
|
662
|
+
Array.from(segmented.children).forEach((b) => b.classList.remove('active'));
|
|
663
|
+
btn.classList.add('active');
|
|
664
|
+
onChange(opt.value);
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
return wrap;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function createImageField(label, value, onChange) {
|
|
671
|
+
const wrap = createEl('div', 'lp-property-field');
|
|
672
|
+
createEl('label', 'lp-property-label', wrap).textContent = label;
|
|
673
|
+
const urlInput = createEl('input', 'lp-property-input', wrap);
|
|
674
|
+
urlInput.type = 'text';
|
|
675
|
+
urlInput.value = value == null ? '' : String(value);
|
|
676
|
+
urlInput.placeholder = 'https://...';
|
|
677
|
+
urlInput.addEventListener('input', () => onChange(urlInput.value));
|
|
678
|
+
|
|
679
|
+
const fileWrap = createEl('div', 'lp-property-field');
|
|
680
|
+
fileWrap.style.marginTop = '8px';
|
|
681
|
+
const fileInput = createEl('input', 'lp-property-input', fileWrap);
|
|
682
|
+
fileInput.type = 'file';
|
|
683
|
+
fileInput.accept = 'image/*';
|
|
684
|
+
fileInput.addEventListener('change', () => {
|
|
685
|
+
const file = fileInput.files[0];
|
|
686
|
+
if (!file) return;
|
|
687
|
+
const reader = new FileReader();
|
|
688
|
+
reader.onload = (e) => {
|
|
689
|
+
const dataUrl = e.target.result;
|
|
690
|
+
urlInput.value = dataUrl;
|
|
691
|
+
onChange(dataUrl);
|
|
692
|
+
};
|
|
693
|
+
reader.readAsDataURL(file);
|
|
694
|
+
});
|
|
695
|
+
wrap.appendChild(fileWrap);
|
|
696
|
+
return wrap;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function createFieldControl(path, value) {
|
|
700
|
+
const type = inferFieldType(path, value);
|
|
701
|
+
const label = getFieldLabel(path);
|
|
702
|
+
if (type === 'image') return createImageField(label, value, (v) => setField(path, v || undefined));
|
|
703
|
+
if (type === 'textarea') return createTextareaField(label, value, (v) => setField(path, v || undefined));
|
|
704
|
+
if (type === 'number') return createNumberField(label, value, (v) => setField(path, v));
|
|
705
|
+
if (type === 'boolean') return createToggleField(label, value, (v) => setField(path, v));
|
|
706
|
+
if (type === 'select') return createSelectField(label, value, getSelectOptions(path), (v) => setField(path, v));
|
|
707
|
+
return createTextField(label, value, (v) => setField(path, v || undefined));
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function inferEmptyItem(array) {
|
|
711
|
+
if (!array.length) return {};
|
|
712
|
+
const sample = array[0];
|
|
713
|
+
if (typeof sample === 'string') return '';
|
|
714
|
+
if (typeof sample === 'object' && sample !== null) {
|
|
715
|
+
const item = {};
|
|
716
|
+
Object.keys(sample).forEach((k) => {
|
|
717
|
+
const v = sample[k];
|
|
718
|
+
item[k] = typeof v === 'boolean' ? false : (typeof v === 'number' ? 0 : '');
|
|
719
|
+
});
|
|
720
|
+
return item;
|
|
721
|
+
}
|
|
722
|
+
return '';
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function createArraySection(path, array, parent) {
|
|
726
|
+
const section = createEl('div', 'lp-property-section', parent);
|
|
727
|
+
createEl('div', 'lp-property-section-title', section).textContent = getFieldLabel(path) + ' (' + array.length + ')';
|
|
728
|
+
|
|
729
|
+
const list = createEl('div', 'lp-property-array', section);
|
|
730
|
+
array.forEach((item, index) => {
|
|
731
|
+
const itemWrap = createEl('div', 'lp-property-array-item', list);
|
|
732
|
+
const header = createEl('div', 'lp-property-array-header', itemWrap);
|
|
733
|
+
header.textContent = '第 ' + (index + 1) + ' 项';
|
|
734
|
+
const removeBtn = createEl('button', 'lp-property-btn lp-property-btn-sm lp-property-btn-danger', header);
|
|
735
|
+
removeBtn.textContent = '删除';
|
|
736
|
+
removeBtn.type = 'button';
|
|
737
|
+
removeBtn.addEventListener('click', () => {
|
|
738
|
+
recordHistory();
|
|
739
|
+
const arr = getProp(goal.slides[selectedSlideIdx].props, path);
|
|
740
|
+
if (Array.isArray(arr)) {
|
|
741
|
+
arr.splice(index, 1);
|
|
742
|
+
autoSave();
|
|
743
|
+
reloadEditor();
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
if (typeof item === 'string') {
|
|
748
|
+
const textarea = createEl('textarea', 'lp-property-textarea', itemWrap);
|
|
749
|
+
textarea.value = item;
|
|
750
|
+
textarea.addEventListener('input', () => {
|
|
751
|
+
setField(path + '.' + index, textarea.value);
|
|
752
|
+
});
|
|
753
|
+
} else if (typeof item === 'object' && item !== null) {
|
|
754
|
+
Object.keys(item).forEach((key) => {
|
|
755
|
+
itemWrap.appendChild(createFieldControl(path + '.' + index + '.' + key, item[key]));
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
const addBtn = createEl('button', 'lp-property-btn lp-property-btn-primary', section);
|
|
761
|
+
addBtn.textContent = '+ 添加一项';
|
|
762
|
+
addBtn.type = 'button';
|
|
763
|
+
addBtn.addEventListener('click', () => {
|
|
764
|
+
recordHistory();
|
|
765
|
+
const arr = getProp(goal.slides[selectedSlideIdx].props, path);
|
|
766
|
+
if (Array.isArray(arr)) {
|
|
767
|
+
arr.push(inferEmptyItem(arr));
|
|
768
|
+
autoSave();
|
|
769
|
+
reloadEditor();
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function renderSlideFields(props, parent) {
|
|
775
|
+
Object.keys(props).forEach((key) => {
|
|
776
|
+
if (key === '_style') return;
|
|
777
|
+
const value = props[key];
|
|
778
|
+
if (Array.isArray(value)) {
|
|
779
|
+
createArraySection(key, value, parent);
|
|
780
|
+
} else if (value !== null && typeof value === 'object') {
|
|
781
|
+
// 嵌套对象直接展开(目前较少)
|
|
782
|
+
const section = createEl('div', 'lp-property-section', parent);
|
|
783
|
+
createEl('div', 'lp-property-section-title', section).textContent = getFieldLabel(key);
|
|
784
|
+
Object.keys(value).forEach((subKey) => {
|
|
785
|
+
section.appendChild(createFieldControl(key + '.' + subKey, value[subKey]));
|
|
786
|
+
});
|
|
787
|
+
} else {
|
|
788
|
+
parent.appendChild(createFieldControl(key, value));
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function renderSlidePanel() {
|
|
794
|
+
if (!propertyContent) return;
|
|
795
|
+
propertyContent.innerHTML = '';
|
|
796
|
+
const slide = goal.slides[selectedSlideIdx];
|
|
797
|
+
if (!slide) return;
|
|
798
|
+
|
|
799
|
+
const info = createEl('div', 'lp-property-section', propertyContent);
|
|
800
|
+
createEl('div', 'lp-property-section-title', info).textContent = '幻灯片 ' + (selectedSlideIdx + 1);
|
|
801
|
+
const layoutLabel = createEl('div', 'lp-property-help', info);
|
|
802
|
+
layoutLabel.textContent = '版式:' + slide.layout;
|
|
803
|
+
|
|
804
|
+
if (selectedEl) {
|
|
805
|
+
const prop = selectedEl.getAttribute('data-lp-prop');
|
|
806
|
+
const quickSection = createEl('div', 'lp-property-section', propertyContent);
|
|
807
|
+
createEl('div', 'lp-property-section-title', quickSection).textContent = '当前选中';
|
|
808
|
+
const fieldValue = getProp(slide.props, prop);
|
|
809
|
+
quickSection.appendChild(createFieldControl(prop, fieldValue));
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const fieldsSection = createEl('div', 'lp-property-section', propertyContent);
|
|
813
|
+
createEl('div', 'lp-property-section-title', fieldsSection).textContent = '内容属性';
|
|
814
|
+
renderSlideFields(slide.props, fieldsSection);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function selectEl(el) {
|
|
818
|
+
if (selectedEl === el) return;
|
|
819
|
+
if (selectedEl) selectedEl.classList.remove('lp-selected');
|
|
820
|
+
selectedEl = el;
|
|
821
|
+
selectedEl.classList.add('lp-selected');
|
|
822
|
+
selectedSlideIdx = Number(el.getAttribute('data-lp-slide-idx')) || current;
|
|
823
|
+
renderSlidePanel();
|
|
824
|
+
// 滚动到当前选中字段
|
|
825
|
+
const path = el.getAttribute('data-lp-prop');
|
|
826
|
+
if (path && propertyContent) {
|
|
827
|
+
const label = propertyContent.querySelector('.lp-property-label');
|
|
828
|
+
// 简单高亮:暂时不做自动滚动,避免复杂度
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function selectSlide(index) {
|
|
833
|
+
if (selectedEl) {
|
|
834
|
+
selectedEl.classList.remove('lp-selected');
|
|
835
|
+
selectedEl = null;
|
|
836
|
+
}
|
|
837
|
+
selectedSlideIdx = index;
|
|
838
|
+
renderSlidePanel();
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
document.addEventListener('focusin', (e) => {
|
|
842
|
+
const el = e.target.closest && e.target.closest('[data-lp-editable="true"], [data-lp-editable-image="true"]');
|
|
843
|
+
if (el) selectEl(el);
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
document.addEventListener('click', (e) => {
|
|
847
|
+
const editableEl = e.target.closest && e.target.closest('[data-lp-editable="true"], [data-lp-editable-image="true"]');
|
|
848
|
+
if (editableEl) {
|
|
849
|
+
selectEl(editableEl);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const wrapper = e.target.closest && e.target.closest('.lp-slide-wrapper');
|
|
853
|
+
if (wrapper) {
|
|
854
|
+
selectSlide(Number(wrapper.getAttribute('data-slide-index')));
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
if (e.target.closest && !e.target.closest('.lp-editor-right-panel')) {
|
|
858
|
+
clearSelection();
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
// 恢复上次停留的幻灯片(结构变更后重载用)
|
|
863
|
+
const savedCurrent = localStorage.getItem('lemonppt:editor:currentSlide');
|
|
864
|
+
if (savedCurrent) {
|
|
865
|
+
const savedIndex = Number(savedCurrent);
|
|
866
|
+
if (!Number.isNaN(savedIndex) && savedIndex >= 0 && savedIndex < slides.length) {
|
|
867
|
+
goTo(savedIndex);
|
|
868
|
+
}
|
|
869
|
+
localStorage.removeItem('lemonppt:editor:currentSlide');
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// 默认选中当前幻灯片
|
|
873
|
+
selectSlide(current);
|
|
874
|
+
})();
|
|
875
|
+
`;
|
|
876
|
+
//# sourceMappingURL=editor-script.js.map
|