starry-slides 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 +10 -0
- package/README.md +131 -0
- package/dist/assets/index---ub3Dty.css +1 -0
- package/dist/assets/index-7Xs_Dyp0.js +354 -0
- package/dist/assets/index-8ul7coaw.js +451 -0
- package/dist/assets/index-B4_bJa9t.js +412 -0
- package/dist/assets/index-B9p6L9Wx.js +324 -0
- package/dist/assets/index-BF-Vi1Nh.js +413 -0
- package/dist/assets/index-BKvVbgXz.css +1 -0
- package/dist/assets/index-BNZJ_Wz-.js +417 -0
- package/dist/assets/index-BO2gtiKn.js +413 -0
- package/dist/assets/index-BYpD8kgo.js +456 -0
- package/dist/assets/index-BaZRfz_9.css +1 -0
- package/dist/assets/index-BlsWm4PI.js +412 -0
- package/dist/assets/index-BnUIBFtw.js +393 -0
- package/dist/assets/index-BvhCbfCi.js +412 -0
- package/dist/assets/index-BxEkxfy1.js +412 -0
- package/dist/assets/index-BzrA7O0L.js +422 -0
- package/dist/assets/index-C0xGkdRg.js +413 -0
- package/dist/assets/index-C1Rjncf1.js +456 -0
- package/dist/assets/index-CCgb2gqc.js +413 -0
- package/dist/assets/index-CDFJQRFx.js +413 -0
- package/dist/assets/index-CEOai0RW.js +456 -0
- package/dist/assets/index-CG2uWTey.css +1 -0
- package/dist/assets/index-CLe3_iTu.js +412 -0
- package/dist/assets/index-CS_optob.js +324 -0
- package/dist/assets/index-CXgwXOZH.css +1 -0
- package/dist/assets/index-CcBvAcE2.js +408 -0
- package/dist/assets/index-CllmR_MT.js +408 -0
- package/dist/assets/index-CnMy6wxq.js +412 -0
- package/dist/assets/index-CoWNjYgb.js +422 -0
- package/dist/assets/index-CtfA3BPy.css +1 -0
- package/dist/assets/index-D-JiuJIv.js +324 -0
- package/dist/assets/index-D2uEYXyQ.css +1 -0
- package/dist/assets/index-D70beOHW.js +456 -0
- package/dist/assets/index-D8xuxnF1.js +456 -0
- package/dist/assets/index-DKAtkRd8.css +1 -0
- package/dist/assets/index-DU3l9_k0.css +1 -0
- package/dist/assets/index-DaLY98jk.js +456 -0
- package/dist/assets/index-DkNHJHQl.js +412 -0
- package/dist/assets/index-DmFnifK8.js +456 -0
- package/dist/assets/index-DndZR7Ds.css +1 -0
- package/dist/assets/index-DuRa7Y6g.js +324 -0
- package/dist/assets/index-EnDjTeHF.js +451 -0
- package/dist/assets/index-J3nQgjqJ.js +451 -0
- package/dist/assets/index-wD_iWTBn.js +413 -0
- package/dist/chunk-AK5J4CXH.js +1671 -0
- package/dist/chunk-ARFDESSF.js +1583 -0
- package/dist/chunk-DHWTBXGS.js +985 -0
- package/dist/chunk-FCRRFL7N.js +1571 -0
- package/dist/chunk-J7W4Y7WJ.js +1620 -0
- package/dist/chunk-OCJULB7Z.js +1140 -0
- package/dist/chunk-WCJWV5SO.js +36 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +1119 -0
- package/dist/cli/index.test.d.ts +1 -0
- package/dist/core/deck-slide-operations.d.ts +5 -0
- package/dist/core/generated-deck.d.ts +8 -0
- package/dist/core/generated-deck.test.d.ts +1 -0
- package/dist/core/group-operations.d.ts +24 -0
- package/dist/core/history.d.ts +20 -0
- package/dist/core/history.test.d.ts +1 -0
- package/dist/core/html-export.d.ts +20 -0
- package/dist/core/index.d.ts +11 -0
- package/dist/core/layout.d.ts +36 -0
- package/dist/core/layout.test.d.ts +1 -0
- package/dist/core/pdf-export.d.ts +20 -0
- package/dist/core/presentation.d.ts +7 -0
- package/dist/core/slide-contract.d.ts +41 -0
- package/dist/core/slide-document.d.ts +5 -0
- package/dist/core/slide-document.test.d.ts +1 -0
- package/dist/core/slide-html-document.d.ts +3 -0
- package/dist/core/slide-operation-reducer.d.ts +4 -0
- package/dist/core/slide-operation-reducer.test.d.ts +1 -0
- package/dist/core/slide-operation-types.d.ts +123 -0
- package/dist/core/slide-operations-helpers.d.ts +14 -0
- package/dist/core/slide-operations.d.ts +5 -0
- package/dist/core/slide-operations.test.d.ts +1 -0
- package/dist/core/verify-deck.d.ts +48 -0
- package/dist/core/verify-deck.test.d.ts +1 -0
- package/dist/editor/app/App.d.ts +2 -0
- package/dist/editor/app/main.d.ts +1 -0
- package/dist/editor/app/use-slides-data.d.ts +14 -0
- package/dist/editor/components/block-manipulation-overlay.d.ts +32 -0
- package/dist/editor/components/color-picker.d.ts +10 -0
- package/dist/editor/components/context-menu.d.ts +26 -0
- package/dist/editor/components/editor-header.d.ts +18 -0
- package/dist/editor/components/floating-toolbar-feature.d.ts +8 -0
- package/dist/editor/components/floating-toolbar-parts.d.ts +46 -0
- package/dist/editor/components/floating-toolbar.d.ts +27 -0
- package/dist/editor/components/presenter-view.d.ts +13 -0
- package/dist/editor/components/slide-sidebar.d.ts +18 -0
- package/dist/editor/components/stage-canvas.d.ts +77 -0
- package/dist/editor/components/ui/accordion.d.ts +7 -0
- package/dist/editor/components/ui/button.d.ts +10 -0
- package/dist/editor/components/ui/context-menu.d.ts +25 -0
- package/dist/editor/components/ui/dialog.d.ts +10 -0
- package/dist/editor/components/ui/input.d.ts +3 -0
- package/dist/editor/components/ui/popover.d.ts +10 -0
- package/dist/editor/components/ui/scroll-area.d.ts +5 -0
- package/dist/editor/components/ui/select.d.ts +15 -0
- package/dist/editor/components/ui/separator.d.ts +4 -0
- package/dist/editor/components/ui/tabs.d.ts +11 -0
- package/dist/editor/components/ui/textarea.d.ts +3 -0
- package/dist/editor/components/ui/toggle-group.d.ts +9 -0
- package/dist/editor/components/ui/toggle.d.ts +9 -0
- package/dist/editor/components/ui/tooltip.d.ts +7 -0
- package/dist/editor/editor-operations.d.ts +15 -0
- package/dist/editor/hooks/block-manipulation-geometry.d.ts +16 -0
- package/dist/editor/hooks/block-manipulation-operations.d.ts +10 -0
- package/dist/editor/hooks/block-manipulation-overlay.d.ts +11 -0
- package/dist/editor/hooks/block-manipulation-types.d.ts +73 -0
- package/dist/editor/hooks/editor-keyboard-geometry.d.ts +22 -0
- package/dist/editor/hooks/editor-keyboard-operations.d.ts +10 -0
- package/dist/editor/hooks/editor-keyboard-types.d.ts +35 -0
- package/dist/editor/hooks/iframe-editing-session.d.ts +10 -0
- package/dist/editor/hooks/iframe-text-editing-dom.d.ts +6 -0
- package/dist/editor/hooks/iframe-text-editing-types.d.ts +38 -0
- package/dist/editor/hooks/object-clipboard-commands.d.ts +28 -0
- package/dist/editor/hooks/use-block-manipulation.d.ts +3 -0
- package/dist/editor/hooks/use-editor-keyboard-shortcuts.d.ts +3 -0
- package/dist/editor/hooks/use-iframe-text-editing.d.ts +3 -0
- package/dist/editor/hooks/use-slide-history.d.ts +17 -0
- package/dist/editor/hooks/use-slide-inspector.d.ts +24 -0
- package/dist/editor/hooks/use-slide-thumbnails.d.ts +2 -0
- package/dist/editor/hooks/use-stage-viewport.d.ts +13 -0
- package/dist/editor/index.d.ts +13 -0
- package/dist/editor/index.js +8260 -0
- package/dist/editor/lib/block-snap-constants.d.ts +8 -0
- package/dist/editor/lib/block-snap-guides.d.ts +6 -0
- package/dist/editor/lib/block-snap-targets.d.ts +13 -0
- package/dist/editor/lib/block-snap-types.d.ts +30 -0
- package/dist/editor/lib/block-snapping.d.ts +17 -0
- package/dist/editor/lib/collect-css-properties.d.ts +5 -0
- package/dist/editor/lib/element-tool-commit.d.ts +18 -0
- package/dist/editor/lib/element-tool-model.d.ts +16 -0
- package/dist/editor/lib/element-tool-types.d.ts +35 -0
- package/dist/editor/lib/element-tool-values.d.ts +23 -0
- package/dist/editor/lib/motion.d.ts +8 -0
- package/dist/editor/lib/selection-overlay.d.ts +4 -0
- package/dist/editor/lib/style-controls.d.ts +17 -0
- package/dist/editor/lib/thumbnail-renderer.d.ts +2 -0
- package/dist/editor/lib/utils.d.ts +3 -0
- package/dist/index.html +13 -0
- package/dist/node/deck-runtime-middleware.d.ts +8 -0
- package/dist/node/deck-source.d.ts +1 -0
- package/dist/node/html-export.d.ts +16 -0
- package/dist/node/open-browser.d.ts +1 -0
- package/dist/node/pdf-export.d.ts +20 -0
- package/dist/node/ports.d.ts +1 -0
- package/dist/node/view-renderer.d.ts +31 -0
- package/dist/runtime/deck-source.d.ts +1 -0
- package/dist/runtime/html-export.d.ts +16 -0
- package/dist/runtime/open-browser.d.ts +1 -0
- package/dist/runtime/pdf-export.d.ts +20 -0
- package/dist/runtime/ports.d.ts +1 -0
- package/dist/runtime/view-renderer.d.ts +31 -0
- package/dist/runtime/view-renderer.test.d.ts +1 -0
- package/dist/test/deck-fixtures.d.ts +14 -0
- package/package.json +94 -0
|
@@ -0,0 +1,1571 @@
|
|
|
1
|
+
// src/core/slide-contract.ts
|
|
2
|
+
var SELECTOR_ATTR = "data-editor-id";
|
|
3
|
+
var SLIDE_ROOT_ATTR = "data-slide-root";
|
|
4
|
+
var DEFAULT_SLIDE_WIDTH = 1920;
|
|
5
|
+
var DEFAULT_SLIDE_HEIGHT = 1080;
|
|
6
|
+
function getSlideElementSelector(elementId) {
|
|
7
|
+
return `[${SELECTOR_ATTR}="${elementId}"]`;
|
|
8
|
+
}
|
|
9
|
+
function getSlideRootSelector(rootId) {
|
|
10
|
+
return `[${SELECTOR_ATTR}="${rootId}"]`;
|
|
11
|
+
}
|
|
12
|
+
function slugify(value) {
|
|
13
|
+
return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "slide";
|
|
14
|
+
}
|
|
15
|
+
function createElementId(index, type) {
|
|
16
|
+
return `${type}-${index + 1}`;
|
|
17
|
+
}
|
|
18
|
+
function parseDimension(value, fallback) {
|
|
19
|
+
const numericValue = Number.parseInt(value || "", 10);
|
|
20
|
+
return Number.isFinite(numericValue) && numericValue > 0 ? numericValue : fallback;
|
|
21
|
+
}
|
|
22
|
+
function normalizeSlideId(slideId) {
|
|
23
|
+
return slugify(slideId);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/core/slide-document.ts
|
|
27
|
+
function parseHtmlDocument(html) {
|
|
28
|
+
if (typeof DOMParser === "undefined") {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const parser = new DOMParser();
|
|
32
|
+
return parser.parseFromString(html, "text/html");
|
|
33
|
+
}
|
|
34
|
+
function ensureSlideRoot(doc) {
|
|
35
|
+
const existingRoot = doc.querySelector(`[${SLIDE_ROOT_ATTR}]`);
|
|
36
|
+
if (existingRoot) {
|
|
37
|
+
if (!existingRoot.getAttribute("data-slide-width")) {
|
|
38
|
+
existingRoot.setAttribute("data-slide-width", String(DEFAULT_SLIDE_WIDTH));
|
|
39
|
+
}
|
|
40
|
+
if (!existingRoot.getAttribute("data-slide-height")) {
|
|
41
|
+
existingRoot.setAttribute("data-slide-height", String(DEFAULT_SLIDE_HEIGHT));
|
|
42
|
+
}
|
|
43
|
+
return existingRoot;
|
|
44
|
+
}
|
|
45
|
+
const container = doc.querySelector(".slide-container");
|
|
46
|
+
if (container) {
|
|
47
|
+
container.setAttribute(SLIDE_ROOT_ATTR, "true");
|
|
48
|
+
if (!container.getAttribute("data-slide-width")) {
|
|
49
|
+
container.setAttribute("data-slide-width", String(DEFAULT_SLIDE_WIDTH));
|
|
50
|
+
}
|
|
51
|
+
if (!container.getAttribute("data-slide-height")) {
|
|
52
|
+
container.setAttribute("data-slide-height", String(DEFAULT_SLIDE_HEIGHT));
|
|
53
|
+
}
|
|
54
|
+
return container;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function serializeHtmlDocument(doc) {
|
|
59
|
+
return `<!DOCTYPE html>
|
|
60
|
+
${doc.documentElement.outerHTML}`;
|
|
61
|
+
}
|
|
62
|
+
function ensureEditableSelectors(html) {
|
|
63
|
+
const doc = parseHtmlDocument(html);
|
|
64
|
+
if (!doc) {
|
|
65
|
+
return html;
|
|
66
|
+
}
|
|
67
|
+
const root = ensureSlideRoot(doc);
|
|
68
|
+
const editableNodes = Array.from(doc.querySelectorAll("[data-editable]"));
|
|
69
|
+
if (root && !root.getAttribute(SELECTOR_ATTR)) {
|
|
70
|
+
root.setAttribute(SELECTOR_ATTR, "slide-root");
|
|
71
|
+
}
|
|
72
|
+
editableNodes.forEach((node, index) => {
|
|
73
|
+
if (!node.getAttribute(SELECTOR_ATTR)) {
|
|
74
|
+
const type = node.getAttribute("data-editable") || "block";
|
|
75
|
+
node.setAttribute(SELECTOR_ATTR, createElementId(index, type));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
return serializeHtmlDocument(doc);
|
|
79
|
+
}
|
|
80
|
+
function parseSlide(html, slideId = "slide-1") {
|
|
81
|
+
const doc = parseHtmlDocument(html);
|
|
82
|
+
if (!doc) {
|
|
83
|
+
return {
|
|
84
|
+
id: slideId,
|
|
85
|
+
title: "Untitled Slide",
|
|
86
|
+
htmlSource: html,
|
|
87
|
+
rootSelector: `[${SLIDE_ROOT_ATTR}]`,
|
|
88
|
+
width: DEFAULT_SLIDE_WIDTH,
|
|
89
|
+
height: DEFAULT_SLIDE_HEIGHT,
|
|
90
|
+
elements: []
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const normalizedHtml = ensureEditableSelectors(html);
|
|
94
|
+
const normalizedDoc = parseHtmlDocument(normalizedHtml);
|
|
95
|
+
if (!normalizedDoc) {
|
|
96
|
+
return {
|
|
97
|
+
id: slideId,
|
|
98
|
+
title: "Untitled Slide",
|
|
99
|
+
htmlSource: normalizedHtml,
|
|
100
|
+
rootSelector: `[${SLIDE_ROOT_ATTR}]`,
|
|
101
|
+
width: DEFAULT_SLIDE_WIDTH,
|
|
102
|
+
height: DEFAULT_SLIDE_HEIGHT,
|
|
103
|
+
elements: []
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const root = ensureSlideRoot(normalizedDoc);
|
|
107
|
+
const editableNodes = Array.from(normalizedDoc.querySelectorAll("[data-editable]"));
|
|
108
|
+
const rootSelector = root?.getAttribute(SELECTOR_ATTR) ? `[${SELECTOR_ATTR}="${root.getAttribute(SELECTOR_ATTR)}"]` : `[${SLIDE_ROOT_ATTR}]`;
|
|
109
|
+
const width = parseDimension(root?.getAttribute("data-slide-width") ?? null, DEFAULT_SLIDE_WIDTH);
|
|
110
|
+
const height = parseDimension(
|
|
111
|
+
root?.getAttribute("data-slide-height") ?? null,
|
|
112
|
+
DEFAULT_SLIDE_HEIGHT
|
|
113
|
+
);
|
|
114
|
+
const elements = editableNodes.map((node, index) => {
|
|
115
|
+
const type = node.getAttribute("data-editable") || "block";
|
|
116
|
+
const selectorValue = node.getAttribute(SELECTOR_ATTR) || createElementId(index, type);
|
|
117
|
+
return {
|
|
118
|
+
id: selectorValue,
|
|
119
|
+
selector: getSlideElementSelector(selectorValue),
|
|
120
|
+
type: type === "block" && node.getAttribute("data-group") === "true" ? "group" : type,
|
|
121
|
+
content: node instanceof HTMLImageElement ? node.src : node.textContent || "",
|
|
122
|
+
tagName: node.tagName.toLowerCase()
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
const firstHeading = normalizedDoc.querySelector("h1, h2, title");
|
|
126
|
+
const title = firstHeading?.textContent?.trim() || `Slide ${slideId}`;
|
|
127
|
+
return {
|
|
128
|
+
id: normalizeSlideId(slideId),
|
|
129
|
+
title,
|
|
130
|
+
htmlSource: normalizedHtml,
|
|
131
|
+
rootSelector,
|
|
132
|
+
width,
|
|
133
|
+
height,
|
|
134
|
+
elements
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function querySlideElement(doc, elementId) {
|
|
138
|
+
return doc.querySelector(getSlideElementSelector(elementId));
|
|
139
|
+
}
|
|
140
|
+
function getSlideInlineStyleValue(slide, elementId, propertyName) {
|
|
141
|
+
const doc = parseHtmlDocument(slide.htmlSource);
|
|
142
|
+
if (!doc) {
|
|
143
|
+
return "";
|
|
144
|
+
}
|
|
145
|
+
const node = querySlideElement(doc, elementId);
|
|
146
|
+
return node?.style.getPropertyValue(propertyName).trim() || "";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/core/slide-html-document.ts
|
|
150
|
+
function parseHtmlDocument2(html) {
|
|
151
|
+
if (typeof DOMParser === "undefined") {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const parser = new DOMParser();
|
|
155
|
+
return parser.parseFromString(html, "text/html");
|
|
156
|
+
}
|
|
157
|
+
function serializeHtmlDocument2(doc) {
|
|
158
|
+
return `<!DOCTYPE html>
|
|
159
|
+
${doc.documentElement.outerHTML}`;
|
|
160
|
+
}
|
|
161
|
+
function updateHtmlSource(html, updater) {
|
|
162
|
+
const doc = parseHtmlDocument2(html);
|
|
163
|
+
if (!doc) {
|
|
164
|
+
return html;
|
|
165
|
+
}
|
|
166
|
+
updater(doc);
|
|
167
|
+
return serializeHtmlDocument2(doc);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/core/layout.ts
|
|
171
|
+
var ELEMENT_LAYOUT_STYLE_KEYS = [
|
|
172
|
+
"position",
|
|
173
|
+
"left",
|
|
174
|
+
"top",
|
|
175
|
+
"width",
|
|
176
|
+
"height",
|
|
177
|
+
"transform",
|
|
178
|
+
"transformOrigin",
|
|
179
|
+
"margin",
|
|
180
|
+
"zIndex"
|
|
181
|
+
];
|
|
182
|
+
function createEmptyElementLayoutStyleSnapshot() {
|
|
183
|
+
return {
|
|
184
|
+
position: null,
|
|
185
|
+
left: null,
|
|
186
|
+
top: null,
|
|
187
|
+
width: null,
|
|
188
|
+
height: null,
|
|
189
|
+
transform: null,
|
|
190
|
+
transformOrigin: null,
|
|
191
|
+
margin: null,
|
|
192
|
+
zIndex: null
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function captureElementLayoutStyleSnapshot(node) {
|
|
196
|
+
const snapshot = createEmptyElementLayoutStyleSnapshot();
|
|
197
|
+
for (const key of ELEMENT_LAYOUT_STYLE_KEYS) {
|
|
198
|
+
const value = node.style[key];
|
|
199
|
+
snapshot[key] = value ? value : null;
|
|
200
|
+
}
|
|
201
|
+
return snapshot;
|
|
202
|
+
}
|
|
203
|
+
function normalizeElementLayoutStyleSnapshot(snapshot) {
|
|
204
|
+
return {
|
|
205
|
+
...createEmptyElementLayoutStyleSnapshot(),
|
|
206
|
+
...snapshot
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function parseTransformParts(transformValue) {
|
|
210
|
+
const rawValue = transformValue || "";
|
|
211
|
+
const translateMatch = rawValue.match(/translate\(([-\d.]+)px,\s*([-\d.]+)px\)/i);
|
|
212
|
+
const rotateMatch = rawValue.match(/rotate\(([-\d.]+)deg\)/i);
|
|
213
|
+
return {
|
|
214
|
+
translateX: translateMatch ? Number.parseFloat(translateMatch[1] || "0") || 0 : 0,
|
|
215
|
+
translateY: translateMatch ? Number.parseFloat(translateMatch[2] || "0") || 0 : 0,
|
|
216
|
+
rotate: rotateMatch ? Number.parseFloat(rotateMatch[1] || "0") || 0 : 0
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function roundNumber(value) {
|
|
220
|
+
return Math.round(value * 100) / 100;
|
|
221
|
+
}
|
|
222
|
+
function composeTransform(translateX, translateY, rotate) {
|
|
223
|
+
const parts = [];
|
|
224
|
+
if (Math.abs(translateX) > 0.01 || Math.abs(translateY) > 0.01) {
|
|
225
|
+
parts.push(`translate(${roundNumber(translateX)}px, ${roundNumber(translateY)}px)`);
|
|
226
|
+
}
|
|
227
|
+
if (Math.abs(rotate) > 0.01) {
|
|
228
|
+
parts.push(`rotate(${roundNumber(rotate)}deg)`);
|
|
229
|
+
}
|
|
230
|
+
return parts.length ? parts.join(" ") : null;
|
|
231
|
+
}
|
|
232
|
+
function elementRectToStageRect(elementRect, rootRect, geometry) {
|
|
233
|
+
return {
|
|
234
|
+
x: geometry.offsetX + (elementRect.left - rootRect.left) * geometry.scale,
|
|
235
|
+
y: geometry.offsetY + (elementRect.top - rootRect.top) * geometry.scale,
|
|
236
|
+
width: elementRect.width * geometry.scale,
|
|
237
|
+
height: elementRect.height * geometry.scale
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function stageDeltaToSlideDelta(deltaX, deltaY, geometry) {
|
|
241
|
+
return {
|
|
242
|
+
x: deltaX / geometry.scale,
|
|
243
|
+
y: deltaY / geometry.scale
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/core/slide-operations.ts
|
|
248
|
+
function numericStyleValue(value) {
|
|
249
|
+
const parsed = Number.parseFloat(value || "");
|
|
250
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
251
|
+
}
|
|
252
|
+
function getNodeRect(node) {
|
|
253
|
+
const transform = parseTransformParts(node.style.transform);
|
|
254
|
+
return {
|
|
255
|
+
x: numericStyleValue(node.style.left) + transform.translateX,
|
|
256
|
+
y: numericStyleValue(node.style.top) + transform.translateY,
|
|
257
|
+
width: numericStyleValue(node.style.width) || DEFAULT_SLIDE_WIDTH,
|
|
258
|
+
height: numericStyleValue(node.style.height) || DEFAULT_SLIDE_HEIGHT
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function getAbsoluteNodeRect(node, elementRects = {}) {
|
|
262
|
+
let current = node;
|
|
263
|
+
let x = 0;
|
|
264
|
+
let y = 0;
|
|
265
|
+
const ownElementId = node.getAttribute(SELECTOR_ATTR) ?? "";
|
|
266
|
+
const ownRect = elementRects[ownElementId] ?? getNodeRect(node);
|
|
267
|
+
const width = ownRect.width;
|
|
268
|
+
const height = ownRect.height;
|
|
269
|
+
while (current) {
|
|
270
|
+
const elementId = current.getAttribute(SELECTOR_ATTR) ?? "";
|
|
271
|
+
const rect = elementRects[elementId] ?? getNodeRect(current);
|
|
272
|
+
x += rect.x;
|
|
273
|
+
y += rect.y;
|
|
274
|
+
const parent = current.parentElement;
|
|
275
|
+
if (!parent || !parent.hasAttribute("data-editable")) {
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
current = parent;
|
|
279
|
+
}
|
|
280
|
+
return { x, y, width, height };
|
|
281
|
+
}
|
|
282
|
+
function getEditableAncestorRect(node, elementRects = {}) {
|
|
283
|
+
const parent = node.parentElement;
|
|
284
|
+
if (!parent || !parent.hasAttribute("data-editable")) {
|
|
285
|
+
return { x: 0, y: 0 };
|
|
286
|
+
}
|
|
287
|
+
const rect = getAbsoluteNodeRect(parent, elementRects);
|
|
288
|
+
return { x: rect.x, y: rect.y };
|
|
289
|
+
}
|
|
290
|
+
function setNodeRect(node, rect) {
|
|
291
|
+
node.style.position = node.style.position || "absolute";
|
|
292
|
+
node.style.left = `${Math.round(rect.x * 100) / 100}px`;
|
|
293
|
+
node.style.top = `${Math.round(rect.y * 100) / 100}px`;
|
|
294
|
+
node.style.width = `${Math.round(rect.width * 100) / 100}px`;
|
|
295
|
+
node.style.height = `${Math.round(rect.height * 100) / 100}px`;
|
|
296
|
+
const transform = parseTransformParts(node.style.transform);
|
|
297
|
+
const nextTransform = composeTransform(0, 0, transform.rotate);
|
|
298
|
+
if (nextTransform) {
|
|
299
|
+
node.style.transform = nextTransform;
|
|
300
|
+
} else {
|
|
301
|
+
node.style.removeProperty("transform");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function applyPresentationStyleSnapshot(node, snapshot) {
|
|
305
|
+
if (!snapshot) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (snapshot.color) {
|
|
309
|
+
node.style.color = snapshot.color;
|
|
310
|
+
}
|
|
311
|
+
if (snapshot.fontSize) {
|
|
312
|
+
node.style.fontSize = snapshot.fontSize;
|
|
313
|
+
}
|
|
314
|
+
if (snapshot.fontWeight) {
|
|
315
|
+
node.style.fontWeight = snapshot.fontWeight;
|
|
316
|
+
}
|
|
317
|
+
if (snapshot.fontStyle) {
|
|
318
|
+
node.style.fontStyle = snapshot.fontStyle;
|
|
319
|
+
}
|
|
320
|
+
if (snapshot.lineHeight) {
|
|
321
|
+
node.style.lineHeight = snapshot.lineHeight;
|
|
322
|
+
}
|
|
323
|
+
if (snapshot.textAlign) {
|
|
324
|
+
node.style.textAlign = snapshot.textAlign;
|
|
325
|
+
}
|
|
326
|
+
if (snapshot.paddingTop) {
|
|
327
|
+
node.style.paddingTop = snapshot.paddingTop;
|
|
328
|
+
}
|
|
329
|
+
if (snapshot.paddingRight) {
|
|
330
|
+
node.style.paddingRight = snapshot.paddingRight;
|
|
331
|
+
}
|
|
332
|
+
if (snapshot.paddingBottom) {
|
|
333
|
+
node.style.paddingBottom = snapshot.paddingBottom;
|
|
334
|
+
}
|
|
335
|
+
if (snapshot.paddingLeft) {
|
|
336
|
+
node.style.paddingLeft = snapshot.paddingLeft;
|
|
337
|
+
}
|
|
338
|
+
if (snapshot.listStylePosition) {
|
|
339
|
+
node.style.listStylePosition = snapshot.listStylePosition;
|
|
340
|
+
}
|
|
341
|
+
if (snapshot.listStyleType) {
|
|
342
|
+
node.style.listStyleType = snapshot.listStyleType;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function getDirectEditableOwner(node) {
|
|
346
|
+
const parent = node.parentElement;
|
|
347
|
+
if (!parent) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
if (parent.hasAttribute(SLIDE_ROOT_ATTR) || parent.hasAttribute("data-editable")) {
|
|
351
|
+
return parent;
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
function childEditableElements(node) {
|
|
356
|
+
return Array.from(node.children).filter(
|
|
357
|
+
(child) => child instanceof HTMLElement && child.hasAttribute("data-editable")
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
function isListWrapperWithEditableItems(node) {
|
|
361
|
+
const tagName = node.tagName.toLowerCase();
|
|
362
|
+
if (tagName !== "ul" && tagName !== "ol") {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
return Array.from(node.children).some(
|
|
366
|
+
(child) => child.tagName.toLowerCase() === "li" && child.hasAttribute("data-editable")
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
function createUniqueElementIdInDocument(doc, preferredId) {
|
|
370
|
+
const existingIds = new Set(
|
|
371
|
+
Array.from(doc.querySelectorAll(`[${SELECTOR_ATTR}]`)).map((node) => node.getAttribute(SELECTOR_ATTR)).filter((value) => Boolean(value))
|
|
372
|
+
);
|
|
373
|
+
if (!existingIds.has(preferredId)) {
|
|
374
|
+
return preferredId;
|
|
375
|
+
}
|
|
376
|
+
const match = preferredId.match(/^(.*?)(?:-(\d+))?$/);
|
|
377
|
+
const base = match?.[1] || preferredId;
|
|
378
|
+
let index = Number.parseInt(match?.[2] || "1", 10) + 1;
|
|
379
|
+
while (existingIds.has(`${base}-${index}`)) {
|
|
380
|
+
index += 1;
|
|
381
|
+
}
|
|
382
|
+
return `${base}-${index}`;
|
|
383
|
+
}
|
|
384
|
+
function flattenableBlockChildElements(node, doc) {
|
|
385
|
+
return Array.from(node.children).filter((child) => {
|
|
386
|
+
if (!(child instanceof HTMLElement)) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
if (child.hasAttribute("data-editable")) {
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
if (!isListWrapperWithEditableItems(child)) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
child.setAttribute("data-editable", "block");
|
|
396
|
+
if (!child.getAttribute(SELECTOR_ATTR)) {
|
|
397
|
+
child.setAttribute(SELECTOR_ATTR, createUniqueElementIdInDocument(doc, "block-1"));
|
|
398
|
+
}
|
|
399
|
+
return true;
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
function updateSlideText(html, elementId, value) {
|
|
403
|
+
return updateHtmlSource(html, (doc) => {
|
|
404
|
+
const node = querySlideElement(doc, elementId);
|
|
405
|
+
if (node) {
|
|
406
|
+
node.textContent = value;
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
function updateSlideStyle(html, elementId, propertyName, value) {
|
|
411
|
+
return updateHtmlSource(html, (doc) => {
|
|
412
|
+
const node = querySlideElement(doc, elementId);
|
|
413
|
+
if (!node) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (value.trim().length === 0) {
|
|
417
|
+
node.style.removeProperty(propertyName);
|
|
418
|
+
} else {
|
|
419
|
+
node.style.setProperty(propertyName, value);
|
|
420
|
+
}
|
|
421
|
+
if (!node.getAttribute("style")?.trim()) {
|
|
422
|
+
node.removeAttribute("style");
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
function updateSlideAttribute(html, elementId, attributeName, value) {
|
|
427
|
+
return updateHtmlSource(html, (doc) => {
|
|
428
|
+
const node = querySlideElement(doc, elementId);
|
|
429
|
+
if (!node) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (value.trim().length === 0) {
|
|
433
|
+
node.removeAttribute(attributeName);
|
|
434
|
+
} else {
|
|
435
|
+
node.setAttribute(attributeName, value);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function duplicateSlideElement(html, sourceElementId, nextElementId) {
|
|
440
|
+
return updateHtmlSource(html, (doc) => {
|
|
441
|
+
const sourceNode = querySlideElement(doc, sourceElementId);
|
|
442
|
+
if (!sourceNode) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const clonedNode = sourceNode.cloneNode(true);
|
|
446
|
+
if (!(clonedNode instanceof HTMLElement)) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
clonedNode.setAttribute(SELECTOR_ATTR, nextElementId);
|
|
450
|
+
clonedNode.removeAttribute("data-hse-editing");
|
|
451
|
+
sourceNode.insertAdjacentElement("afterend", clonedNode);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
function applyElementLayoutStyleSnapshot(node, snapshot) {
|
|
455
|
+
for (const key of ELEMENT_LAYOUT_STYLE_KEYS) {
|
|
456
|
+
const value = snapshot[key];
|
|
457
|
+
if (value === null) {
|
|
458
|
+
node.style[key] = "";
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
node.style[key] = value;
|
|
462
|
+
}
|
|
463
|
+
if (!node.getAttribute("style")?.trim()) {
|
|
464
|
+
node.removeAttribute("style");
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function updateSlideElementLayout(html, elementId, snapshot) {
|
|
468
|
+
return updateHtmlSource(html, (doc) => {
|
|
469
|
+
const node = querySlideElement(doc, elementId);
|
|
470
|
+
if (!node) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
applyElementLayoutStyleSnapshot(node, snapshot);
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
function createElementPlacement(html, elementId) {
|
|
477
|
+
const doc = parseHtmlDocument2(html);
|
|
478
|
+
if (!doc) {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
const node = querySlideElement(doc, elementId);
|
|
482
|
+
if (!node || !(node.parentElement instanceof HTMLElement)) {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
const previousSiblingElementId = node.previousElementSibling instanceof HTMLElement ? node.previousElementSibling.getAttribute(SELECTOR_ATTR) : null;
|
|
486
|
+
const nextSiblingElementId = node.nextElementSibling instanceof HTMLElement ? node.nextElementSibling.getAttribute(SELECTOR_ATTR) : null;
|
|
487
|
+
return {
|
|
488
|
+
parentElementId: node.parentElement.getAttribute(SELECTOR_ATTR),
|
|
489
|
+
previousSiblingElementId,
|
|
490
|
+
nextSiblingElementId
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function getSlideElementHtml(html, elementId) {
|
|
494
|
+
const doc = parseHtmlDocument2(html);
|
|
495
|
+
if (!doc) {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
return querySlideElement(doc, elementId)?.outerHTML ?? null;
|
|
499
|
+
}
|
|
500
|
+
function updateSlideElementHtmlIds(elementHtml, idMap) {
|
|
501
|
+
const doc = parseHtmlDocument2(`<template>${elementHtml}</template>`);
|
|
502
|
+
const root = doc?.querySelector("template")?.content.firstElementChild;
|
|
503
|
+
if (!(root instanceof HTMLElement)) {
|
|
504
|
+
return elementHtml;
|
|
505
|
+
}
|
|
506
|
+
const nodes = [root, ...Array.from(root.querySelectorAll(`[${SELECTOR_ATTR}]`))];
|
|
507
|
+
for (const node of nodes) {
|
|
508
|
+
const currentId = node.getAttribute(SELECTOR_ATTR);
|
|
509
|
+
if (currentId && idMap[currentId]) {
|
|
510
|
+
node.setAttribute(SELECTOR_ATTR, idMap[currentId]);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return root.outerHTML;
|
|
514
|
+
}
|
|
515
|
+
function createUniqueElementId(html, preferredId) {
|
|
516
|
+
const doc = parseHtmlDocument2(html);
|
|
517
|
+
if (!doc) {
|
|
518
|
+
return preferredId;
|
|
519
|
+
}
|
|
520
|
+
return createUniqueElementIdInDocument(doc, preferredId);
|
|
521
|
+
}
|
|
522
|
+
function insertSlideElement(html, operation) {
|
|
523
|
+
return updateHtmlSource(html, (doc) => {
|
|
524
|
+
if (querySlideElement(doc, operation.elementId)) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const fragmentDoc = parseHtmlDocument2(`<template>${operation.html}</template>`);
|
|
528
|
+
const node = fragmentDoc?.querySelector("template")?.content.firstElementChild;
|
|
529
|
+
if (!(node instanceof HTMLElement)) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const parent = (operation.parentElementId ? querySlideElement(doc, operation.parentElementId) : null) ?? doc.querySelector(`[${SLIDE_ROOT_ATTR}]`) ?? doc.body;
|
|
533
|
+
const nextSibling = operation.nextSiblingElementId ? querySlideElement(doc, operation.nextSiblingElementId) : null;
|
|
534
|
+
const previousSibling = operation.previousSiblingElementId ? querySlideElement(doc, operation.previousSiblingElementId) : null;
|
|
535
|
+
if (nextSibling?.parentElement === parent) {
|
|
536
|
+
parent.insertBefore(doc.importNode(node, true), nextSibling);
|
|
537
|
+
} else if (previousSibling?.parentElement === parent) {
|
|
538
|
+
previousSibling.after(doc.importNode(node, true));
|
|
539
|
+
} else {
|
|
540
|
+
parent.appendChild(doc.importNode(node, true));
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
function removeSlideElement(html, elementId) {
|
|
545
|
+
return updateHtmlSource(html, (doc) => {
|
|
546
|
+
querySlideElement(doc, elementId)?.remove();
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
function updateSlideElementTransform(html, elementId, deltaX, deltaY) {
|
|
550
|
+
return updateHtmlSource(html, (doc) => {
|
|
551
|
+
const node = querySlideElement(doc, elementId);
|
|
552
|
+
if (!node) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const currentTransform = parseTransformParts(node.style.transform);
|
|
556
|
+
const nextTransform = composeTransform(
|
|
557
|
+
currentTransform.translateX + deltaX,
|
|
558
|
+
currentTransform.translateY + deltaY,
|
|
559
|
+
currentTransform.rotate
|
|
560
|
+
);
|
|
561
|
+
if (nextTransform) {
|
|
562
|
+
node.style.transform = nextTransform;
|
|
563
|
+
} else {
|
|
564
|
+
node.style.removeProperty("transform");
|
|
565
|
+
}
|
|
566
|
+
if (!node.style.position) {
|
|
567
|
+
node.style.position = "relative";
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
function createGroupCreateOperation({
|
|
572
|
+
html,
|
|
573
|
+
slideId,
|
|
574
|
+
groupElementId,
|
|
575
|
+
elementIds,
|
|
576
|
+
elementRects = {},
|
|
577
|
+
timestamp = Date.now()
|
|
578
|
+
}) {
|
|
579
|
+
const doc = parseHtmlDocument2(html);
|
|
580
|
+
if (!doc || !elementIds.length) {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
const selectedNodes = elementIds.map((elementId) => querySlideElement(doc, elementId)).filter((node) => Boolean(node));
|
|
584
|
+
if (selectedNodes.length !== elementIds.length) {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
const flattenedNodes = selectedNodes.flatMap(
|
|
588
|
+
(node) => node.getAttribute("data-group") === "true" ? childEditableElements(node) : [node]
|
|
589
|
+
);
|
|
590
|
+
const uniqueNodes = flattenedNodes.filter(
|
|
591
|
+
(node, index, nodes) => nodes.findIndex((candidate) => candidate === node) === index
|
|
592
|
+
);
|
|
593
|
+
if (uniqueNodes.length < 2) {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
const parent = getDirectEditableOwner(selectedNodes[0]);
|
|
597
|
+
if (!parent || selectedNodes.some((node) => getDirectEditableOwner(node) !== parent)) {
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
if (querySlideElement(doc, groupElementId)) {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
const selectedGroups = selectedNodes.filter((node) => node.getAttribute("data-group") === "true");
|
|
604
|
+
const rects = uniqueNodes.map((node) => getAbsoluteNodeRect(node, elementRects));
|
|
605
|
+
const left = Math.min(...rects.map((rect) => rect.x));
|
|
606
|
+
const top = Math.min(...rects.map((rect) => rect.y));
|
|
607
|
+
const right = Math.max(...rects.map((rect) => rect.x + rect.width));
|
|
608
|
+
const bottom = Math.max(...rects.map((rect) => rect.y + rect.height));
|
|
609
|
+
const parentRect = getEditableAncestorRect(selectedNodes[0], elementRects);
|
|
610
|
+
const groupNode = doc.createElement("div");
|
|
611
|
+
groupNode.setAttribute("data-editable", "block");
|
|
612
|
+
groupNode.setAttribute("data-group", "true");
|
|
613
|
+
groupNode.setAttribute(SELECTOR_ATTR, groupElementId);
|
|
614
|
+
setNodeRect(groupNode, {
|
|
615
|
+
x: left - parentRect.x,
|
|
616
|
+
y: top - parentRect.y,
|
|
617
|
+
width: right - left,
|
|
618
|
+
height: bottom - top
|
|
619
|
+
});
|
|
620
|
+
const orderedNodes = Array.from(parent.children).flatMap((child) => {
|
|
621
|
+
if (!(child instanceof HTMLElement)) {
|
|
622
|
+
return [];
|
|
623
|
+
}
|
|
624
|
+
if (selectedGroups.includes(child)) {
|
|
625
|
+
return childEditableElements(child).filter((node) => uniqueNodes.includes(node));
|
|
626
|
+
}
|
|
627
|
+
return uniqueNodes.includes(child) ? [child] : [];
|
|
628
|
+
});
|
|
629
|
+
const insertionAnchor = selectedNodes.find((node) => getDirectEditableOwner(node) === parent) ?? null;
|
|
630
|
+
if (insertionAnchor?.parentElement === parent) {
|
|
631
|
+
parent.insertBefore(groupNode, insertionAnchor);
|
|
632
|
+
} else {
|
|
633
|
+
parent.appendChild(groupNode);
|
|
634
|
+
}
|
|
635
|
+
for (const node of orderedNodes) {
|
|
636
|
+
const rect = getAbsoluteNodeRect(node, elementRects);
|
|
637
|
+
setNodeRect(node, {
|
|
638
|
+
x: rect.x - left,
|
|
639
|
+
y: rect.y - top,
|
|
640
|
+
width: rect.width,
|
|
641
|
+
height: rect.height
|
|
642
|
+
});
|
|
643
|
+
groupNode.appendChild(node);
|
|
644
|
+
}
|
|
645
|
+
for (const group of selectedGroups) {
|
|
646
|
+
if (!group.children.length) {
|
|
647
|
+
group.remove();
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
const nextHtmlSource = serializeHtmlDocument2(doc);
|
|
651
|
+
if (nextHtmlSource === html) {
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
type: "group.create",
|
|
656
|
+
slideId,
|
|
657
|
+
groupElementId,
|
|
658
|
+
elementIds: orderedNodes.map((node) => node.getAttribute(SELECTOR_ATTR)).filter((elementId) => Boolean(elementId)),
|
|
659
|
+
previousHtmlSource: html,
|
|
660
|
+
nextHtmlSource,
|
|
661
|
+
timestamp
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
function createGroupUngroupOperation({
|
|
665
|
+
html,
|
|
666
|
+
slideId,
|
|
667
|
+
groupElementId,
|
|
668
|
+
elementRects = {},
|
|
669
|
+
elementPresentationStyles = {},
|
|
670
|
+
timestamp = Date.now()
|
|
671
|
+
}) {
|
|
672
|
+
const doc = parseHtmlDocument2(html);
|
|
673
|
+
if (!doc) {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
const groupNode = querySlideElement(doc, groupElementId);
|
|
677
|
+
if (!groupNode || !groupNode.parentElement) {
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
const parent = groupNode.parentElement;
|
|
681
|
+
const parentRect = getEditableAncestorRect(groupNode, elementRects);
|
|
682
|
+
const isGroup = groupNode.getAttribute("data-group") === "true";
|
|
683
|
+
const children = isGroup ? childEditableElements(groupNode) : flattenableBlockChildElements(groupNode, doc);
|
|
684
|
+
if (!children.length) {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
const selectedElementId = groupNode.getAttribute(SELECTOR_ATTR);
|
|
688
|
+
const insertionAnchor = isGroup ? groupNode : groupNode.nextSibling;
|
|
689
|
+
const childElementIds = children.map((child) => child.getAttribute(SELECTOR_ATTR)).filter((elementId) => Boolean(elementId));
|
|
690
|
+
for (const child of children) {
|
|
691
|
+
const rect = getAbsoluteNodeRect(child, elementRects);
|
|
692
|
+
setNodeRect(child, {
|
|
693
|
+
x: rect.x - parentRect.x,
|
|
694
|
+
y: rect.y - parentRect.y,
|
|
695
|
+
width: rect.width,
|
|
696
|
+
height: rect.height
|
|
697
|
+
});
|
|
698
|
+
if (!isGroup) {
|
|
699
|
+
const childElementId = child.getAttribute(SELECTOR_ATTR) ?? "";
|
|
700
|
+
applyPresentationStyleSnapshot(child, elementPresentationStyles[childElementId]);
|
|
701
|
+
if (isListWrapperWithEditableItems(child)) {
|
|
702
|
+
child.style.margin = "0px";
|
|
703
|
+
}
|
|
704
|
+
for (const descendant of Array.from(
|
|
705
|
+
child.querySelectorAll(`[data-editable][${SELECTOR_ATTR}]`)
|
|
706
|
+
)) {
|
|
707
|
+
const descendantElementId = descendant.getAttribute(SELECTOR_ATTR) ?? "";
|
|
708
|
+
applyPresentationStyleSnapshot(descendant, elementPresentationStyles[descendantElementId]);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
parent.insertBefore(child, insertionAnchor);
|
|
712
|
+
}
|
|
713
|
+
if (isGroup) {
|
|
714
|
+
groupNode.remove();
|
|
715
|
+
}
|
|
716
|
+
const nextHtmlSource = serializeHtmlDocument2(doc);
|
|
717
|
+
if (nextHtmlSource === html) {
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
return {
|
|
721
|
+
type: "group.ungroup",
|
|
722
|
+
slideId,
|
|
723
|
+
groupElementId,
|
|
724
|
+
childElementIds: isGroup || !selectedElementId ? childElementIds : [selectedElementId, ...childElementIds],
|
|
725
|
+
previousHtmlSource: html,
|
|
726
|
+
nextHtmlSource,
|
|
727
|
+
timestamp
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// src/core/slide-operation-reducer.ts
|
|
732
|
+
function preserveSlideSource(sourceSlide, nextSlide) {
|
|
733
|
+
return {
|
|
734
|
+
...nextSlide,
|
|
735
|
+
hidden: sourceSlide.hidden,
|
|
736
|
+
sourceFile: sourceSlide.sourceFile
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
function applySlideOperation(slide, operation) {
|
|
740
|
+
if (operation.type === "slide.visibility.update") {
|
|
741
|
+
if (slide.id !== operation.slideId) {
|
|
742
|
+
return slide;
|
|
743
|
+
}
|
|
744
|
+
return { ...slide, hidden: operation.nextHidden };
|
|
745
|
+
}
|
|
746
|
+
if (!("slideId" in operation) || operation.type === "slide.reorder") {
|
|
747
|
+
return slide;
|
|
748
|
+
}
|
|
749
|
+
if (slide.id !== operation.slideId) {
|
|
750
|
+
return slide;
|
|
751
|
+
}
|
|
752
|
+
switch (operation.type) {
|
|
753
|
+
case "operation.batch":
|
|
754
|
+
return operation.operations.reduce(
|
|
755
|
+
(currentSlide, childOperation) => applySlideOperation(currentSlide, childOperation),
|
|
756
|
+
slide
|
|
757
|
+
);
|
|
758
|
+
case "text.update":
|
|
759
|
+
return preserveSlideSource(
|
|
760
|
+
slide,
|
|
761
|
+
parseSlide(
|
|
762
|
+
updateSlideText(slide.htmlSource, operation.elementId, operation.nextText),
|
|
763
|
+
slide.id
|
|
764
|
+
)
|
|
765
|
+
);
|
|
766
|
+
case "style.update":
|
|
767
|
+
return preserveSlideSource(
|
|
768
|
+
slide,
|
|
769
|
+
parseSlide(
|
|
770
|
+
updateSlideStyle(
|
|
771
|
+
slide.htmlSource,
|
|
772
|
+
operation.elementId,
|
|
773
|
+
operation.propertyName,
|
|
774
|
+
operation.nextValue
|
|
775
|
+
),
|
|
776
|
+
slide.id
|
|
777
|
+
)
|
|
778
|
+
);
|
|
779
|
+
case "attribute.update":
|
|
780
|
+
return preserveSlideSource(
|
|
781
|
+
slide,
|
|
782
|
+
parseSlide(
|
|
783
|
+
updateSlideAttribute(
|
|
784
|
+
slide.htmlSource,
|
|
785
|
+
operation.elementId,
|
|
786
|
+
operation.attributeName,
|
|
787
|
+
operation.nextValue
|
|
788
|
+
),
|
|
789
|
+
slide.id
|
|
790
|
+
)
|
|
791
|
+
);
|
|
792
|
+
case "element.layout.update":
|
|
793
|
+
return preserveSlideSource(
|
|
794
|
+
slide,
|
|
795
|
+
parseSlide(
|
|
796
|
+
updateSlideElementLayout(slide.htmlSource, operation.elementId, operation.nextStyle),
|
|
797
|
+
slide.id
|
|
798
|
+
)
|
|
799
|
+
);
|
|
800
|
+
case "element.insert":
|
|
801
|
+
return preserveSlideSource(
|
|
802
|
+
slide,
|
|
803
|
+
parseSlide(insertSlideElement(slide.htmlSource, operation), slide.id)
|
|
804
|
+
);
|
|
805
|
+
case "element.remove":
|
|
806
|
+
return preserveSlideSource(
|
|
807
|
+
slide,
|
|
808
|
+
parseSlide(removeSlideElement(slide.htmlSource, operation.elementId), slide.id)
|
|
809
|
+
);
|
|
810
|
+
case "group.create":
|
|
811
|
+
case "group.ungroup":
|
|
812
|
+
return preserveSlideSource(slide, parseSlide(operation.nextHtmlSource, slide.id));
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
function invertSlideOperation(operation) {
|
|
816
|
+
switch (operation.type) {
|
|
817
|
+
case "operation.batch":
|
|
818
|
+
return {
|
|
819
|
+
type: "operation.batch",
|
|
820
|
+
slideId: operation.slideId,
|
|
821
|
+
operations: operation.operations.map((childOperation) => invertSlideOperation(childOperation)).reverse(),
|
|
822
|
+
timestamp: operation.timestamp
|
|
823
|
+
};
|
|
824
|
+
case "text.update":
|
|
825
|
+
return {
|
|
826
|
+
...operation,
|
|
827
|
+
previousText: operation.nextText,
|
|
828
|
+
nextText: operation.previousText
|
|
829
|
+
};
|
|
830
|
+
case "style.update":
|
|
831
|
+
return {
|
|
832
|
+
...operation,
|
|
833
|
+
previousValue: operation.nextValue,
|
|
834
|
+
nextValue: operation.previousValue
|
|
835
|
+
};
|
|
836
|
+
case "attribute.update":
|
|
837
|
+
return {
|
|
838
|
+
...operation,
|
|
839
|
+
previousValue: operation.nextValue,
|
|
840
|
+
nextValue: operation.previousValue
|
|
841
|
+
};
|
|
842
|
+
case "element.layout.update":
|
|
843
|
+
return {
|
|
844
|
+
...operation,
|
|
845
|
+
previousStyle: operation.nextStyle,
|
|
846
|
+
nextStyle: operation.previousStyle
|
|
847
|
+
};
|
|
848
|
+
case "element.insert":
|
|
849
|
+
return {
|
|
850
|
+
type: "element.remove",
|
|
851
|
+
slideId: operation.slideId,
|
|
852
|
+
elementId: operation.elementId,
|
|
853
|
+
parentElementId: operation.parentElementId,
|
|
854
|
+
previousSiblingElementId: operation.previousSiblingElementId,
|
|
855
|
+
nextSiblingElementId: operation.nextSiblingElementId,
|
|
856
|
+
html: operation.html,
|
|
857
|
+
timestamp: operation.timestamp
|
|
858
|
+
};
|
|
859
|
+
case "element.remove":
|
|
860
|
+
return {
|
|
861
|
+
type: "element.insert",
|
|
862
|
+
slideId: operation.slideId,
|
|
863
|
+
elementId: operation.elementId,
|
|
864
|
+
parentElementId: operation.parentElementId,
|
|
865
|
+
previousSiblingElementId: operation.previousSiblingElementId,
|
|
866
|
+
nextSiblingElementId: operation.nextSiblingElementId,
|
|
867
|
+
html: operation.html,
|
|
868
|
+
timestamp: operation.timestamp
|
|
869
|
+
};
|
|
870
|
+
case "group.create":
|
|
871
|
+
return {
|
|
872
|
+
type: "group.ungroup",
|
|
873
|
+
slideId: operation.slideId,
|
|
874
|
+
groupElementId: operation.groupElementId,
|
|
875
|
+
childElementIds: operation.elementIds,
|
|
876
|
+
previousHtmlSource: operation.nextHtmlSource,
|
|
877
|
+
nextHtmlSource: operation.previousHtmlSource,
|
|
878
|
+
timestamp: operation.timestamp
|
|
879
|
+
};
|
|
880
|
+
case "group.ungroup":
|
|
881
|
+
return {
|
|
882
|
+
type: "group.create",
|
|
883
|
+
slideId: operation.slideId,
|
|
884
|
+
groupElementId: operation.groupElementId,
|
|
885
|
+
elementIds: operation.childElementIds,
|
|
886
|
+
previousHtmlSource: operation.nextHtmlSource,
|
|
887
|
+
nextHtmlSource: operation.previousHtmlSource,
|
|
888
|
+
timestamp: operation.timestamp
|
|
889
|
+
};
|
|
890
|
+
case "slide.create":
|
|
891
|
+
return {
|
|
892
|
+
type: "slide.delete",
|
|
893
|
+
slide: operation.slide,
|
|
894
|
+
index: operation.index,
|
|
895
|
+
timestamp: operation.timestamp
|
|
896
|
+
};
|
|
897
|
+
case "slide.delete":
|
|
898
|
+
return {
|
|
899
|
+
type: "slide.create",
|
|
900
|
+
slide: operation.slide,
|
|
901
|
+
index: operation.index,
|
|
902
|
+
timestamp: operation.timestamp
|
|
903
|
+
};
|
|
904
|
+
case "slide.duplicate":
|
|
905
|
+
return {
|
|
906
|
+
type: "slide.delete",
|
|
907
|
+
slide: operation.slide,
|
|
908
|
+
index: operation.index,
|
|
909
|
+
timestamp: operation.timestamp
|
|
910
|
+
};
|
|
911
|
+
case "slide.reorder":
|
|
912
|
+
return {
|
|
913
|
+
...operation,
|
|
914
|
+
fromIndex: operation.toIndex,
|
|
915
|
+
toIndex: operation.fromIndex
|
|
916
|
+
};
|
|
917
|
+
case "slide.visibility.update":
|
|
918
|
+
return {
|
|
919
|
+
...operation,
|
|
920
|
+
previousHidden: operation.nextHidden,
|
|
921
|
+
nextHidden: operation.previousHidden
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// src/core/deck-slide-operations.ts
|
|
927
|
+
var DEFAULT_NEW_SLIDE_HTML = `<!DOCTYPE html>
|
|
928
|
+
<html lang="en">
|
|
929
|
+
<head>
|
|
930
|
+
<meta charset="utf-8" />
|
|
931
|
+
<title>Untitled Slide</title>
|
|
932
|
+
</head>
|
|
933
|
+
<body>
|
|
934
|
+
<main
|
|
935
|
+
data-slide-root="true"
|
|
936
|
+
data-slide-width="1920"
|
|
937
|
+
data-slide-height="1080"
|
|
938
|
+
data-editor-id="slide-root"
|
|
939
|
+
style="position: relative; width: 1920px; height: 1080px; overflow: hidden; background: #ffffff;"
|
|
940
|
+
>
|
|
941
|
+
<h1 data-editable="text" data-editor-id="text-1" style="position: absolute; left: 160px; top: 160px; width: 1200px; margin: 0; font-size: 96px; line-height: 1.05;">
|
|
942
|
+
Untitled Slide
|
|
943
|
+
</h1>
|
|
944
|
+
</main>
|
|
945
|
+
</body>
|
|
946
|
+
</html>`;
|
|
947
|
+
function createUniqueSlideId(slides, preferredId) {
|
|
948
|
+
const existingIds = new Set(slides.map((slide) => slide.id));
|
|
949
|
+
if (!existingIds.has(preferredId)) {
|
|
950
|
+
return preferredId;
|
|
951
|
+
}
|
|
952
|
+
let index = 2;
|
|
953
|
+
while (existingIds.has(`${preferredId}-${index}`)) {
|
|
954
|
+
index += 1;
|
|
955
|
+
}
|
|
956
|
+
return `${preferredId}-${index}`;
|
|
957
|
+
}
|
|
958
|
+
function createUniqueSlideSourceFile(slides, preferredFile) {
|
|
959
|
+
const existingFiles = new Set(slides.map((slide) => slide.sourceFile).filter(Boolean));
|
|
960
|
+
if (!existingFiles.has(preferredFile)) {
|
|
961
|
+
return preferredFile;
|
|
962
|
+
}
|
|
963
|
+
const extensionIndex = preferredFile.lastIndexOf(".");
|
|
964
|
+
const base = extensionIndex >= 0 ? preferredFile.slice(0, extensionIndex) : preferredFile;
|
|
965
|
+
const extension = extensionIndex >= 0 ? preferredFile.slice(extensionIndex) : "";
|
|
966
|
+
let index = 2;
|
|
967
|
+
while (existingFiles.has(`${base}-${index}${extension}`)) {
|
|
968
|
+
index += 1;
|
|
969
|
+
}
|
|
970
|
+
return `${base}-${index}${extension}`;
|
|
971
|
+
}
|
|
972
|
+
function createBlankSlide(slides, insertIndex) {
|
|
973
|
+
const position = Math.max(insertIndex + 1, 1);
|
|
974
|
+
const slideId = createUniqueSlideId(slides, `generated-slide-${position}`);
|
|
975
|
+
const sourceFile = createUniqueSlideSourceFile(
|
|
976
|
+
slides,
|
|
977
|
+
`${String(position).padStart(2, "0")}-untitled.html`
|
|
978
|
+
);
|
|
979
|
+
return {
|
|
980
|
+
...parseSlide(DEFAULT_NEW_SLIDE_HTML, slideId),
|
|
981
|
+
hidden: false,
|
|
982
|
+
sourceFile,
|
|
983
|
+
title: "Untitled Slide"
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
function createDuplicatedSlide(slides, sourceSlide) {
|
|
987
|
+
const slideId = createUniqueSlideId(slides, `${sourceSlide.id}-copy`);
|
|
988
|
+
const sourceFile = createUniqueSlideSourceFile(
|
|
989
|
+
slides,
|
|
990
|
+
sourceSlide.sourceFile ? appendFileSuffix(sourceSlide.sourceFile, "copy") : `${slideId}.html`
|
|
991
|
+
);
|
|
992
|
+
return {
|
|
993
|
+
...parseSlide(sourceSlide.htmlSource, slideId),
|
|
994
|
+
hidden: sourceSlide.hidden === true,
|
|
995
|
+
sourceFile,
|
|
996
|
+
title: sourceSlide.title
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
function appendFileSuffix(file, suffix) {
|
|
1000
|
+
const extensionIndex = file.lastIndexOf(".");
|
|
1001
|
+
if (extensionIndex < 0) {
|
|
1002
|
+
return `${file}-${suffix}`;
|
|
1003
|
+
}
|
|
1004
|
+
return `${file.slice(0, extensionIndex)}-${suffix}${file.slice(extensionIndex)}`;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// src/core/history.ts
|
|
1008
|
+
function clampInsertIndex(index, length) {
|
|
1009
|
+
return Math.min(Math.max(index, 0), length);
|
|
1010
|
+
}
|
|
1011
|
+
function applyOperationToSlides(slides, operation) {
|
|
1012
|
+
switch (operation.type) {
|
|
1013
|
+
case "slide.create":
|
|
1014
|
+
case "slide.duplicate": {
|
|
1015
|
+
const index = clampInsertIndex(operation.index, slides.length);
|
|
1016
|
+
return [...slides.slice(0, index), operation.slide, ...slides.slice(index)];
|
|
1017
|
+
}
|
|
1018
|
+
case "slide.delete":
|
|
1019
|
+
return slides.filter((slide) => slide.id !== operation.slide.id);
|
|
1020
|
+
case "slide.reorder": {
|
|
1021
|
+
const fromIndex = slides.findIndex((slide2) => slide2.id === operation.slideId);
|
|
1022
|
+
if (fromIndex < 0) {
|
|
1023
|
+
return slides;
|
|
1024
|
+
}
|
|
1025
|
+
const nextSlides = [...slides];
|
|
1026
|
+
const [slide] = nextSlides.splice(fromIndex, 1);
|
|
1027
|
+
if (!slide) {
|
|
1028
|
+
return slides;
|
|
1029
|
+
}
|
|
1030
|
+
nextSlides.splice(clampInsertIndex(operation.toIndex, nextSlides.length), 0, slide);
|
|
1031
|
+
return nextSlides;
|
|
1032
|
+
}
|
|
1033
|
+
default:
|
|
1034
|
+
return slides.map((slide) => applySlideOperation(slide, operation));
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
function createHistoryState(slides) {
|
|
1038
|
+
return {
|
|
1039
|
+
slides,
|
|
1040
|
+
undoStack: [],
|
|
1041
|
+
redoStack: []
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
function reduceHistory(state, action) {
|
|
1045
|
+
switch (action.type) {
|
|
1046
|
+
case "history.reset":
|
|
1047
|
+
return createHistoryState(action.slides);
|
|
1048
|
+
case "history.commit":
|
|
1049
|
+
return {
|
|
1050
|
+
slides: applyOperationToSlides(state.slides, action.operation),
|
|
1051
|
+
undoStack: [...state.undoStack, action.operation],
|
|
1052
|
+
redoStack: []
|
|
1053
|
+
};
|
|
1054
|
+
case "history.undo": {
|
|
1055
|
+
const operation = state.undoStack[state.undoStack.length - 1];
|
|
1056
|
+
if (!operation) {
|
|
1057
|
+
return state;
|
|
1058
|
+
}
|
|
1059
|
+
return {
|
|
1060
|
+
slides: applyOperationToSlides(state.slides, invertSlideOperation(operation)),
|
|
1061
|
+
undoStack: state.undoStack.slice(0, -1),
|
|
1062
|
+
redoStack: [...state.redoStack, operation]
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
case "history.redo": {
|
|
1066
|
+
const operation = state.redoStack[state.redoStack.length - 1];
|
|
1067
|
+
if (!operation) {
|
|
1068
|
+
return state;
|
|
1069
|
+
}
|
|
1070
|
+
return {
|
|
1071
|
+
slides: applyOperationToSlides(state.slides, operation),
|
|
1072
|
+
undoStack: [...state.undoStack, operation],
|
|
1073
|
+
redoStack: state.redoStack.slice(0, -1)
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// src/core/generated-deck.ts
|
|
1080
|
+
async function loadSlidesFromManifest({
|
|
1081
|
+
manifestUrl,
|
|
1082
|
+
fetchImpl,
|
|
1083
|
+
requestInit,
|
|
1084
|
+
slideIdPrefix = "generated-slide-"
|
|
1085
|
+
}) {
|
|
1086
|
+
const activeFetch = fetchImpl ?? globalThis.fetch;
|
|
1087
|
+
if (!activeFetch) {
|
|
1088
|
+
throw new Error("loadSlidesFromManifest requires a fetch implementation.");
|
|
1089
|
+
}
|
|
1090
|
+
const effectiveRequestInit = {
|
|
1091
|
+
cache: "no-store",
|
|
1092
|
+
...requestInit
|
|
1093
|
+
};
|
|
1094
|
+
const manifestResponse = await activeFetch(manifestUrl, effectiveRequestInit);
|
|
1095
|
+
if (!manifestResponse.ok) {
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
const manifest = await manifestResponse.json();
|
|
1099
|
+
if (!manifest.slides?.length) {
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
const manifestBaseUrl = manifestResponse.url || manifestUrl;
|
|
1103
|
+
const manifestUrlObject = new URL(manifestBaseUrl);
|
|
1104
|
+
const slides = await Promise.all(
|
|
1105
|
+
manifest.slides.map(async (slide, index) => {
|
|
1106
|
+
const slideUrl = new URL(slide.file, manifestBaseUrl);
|
|
1107
|
+
for (const [key, value] of manifestUrlObject.searchParams) {
|
|
1108
|
+
if (!slideUrl.searchParams.has(key)) {
|
|
1109
|
+
slideUrl.searchParams.set(key, value);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
const slideResponse = await activeFetch(slideUrl.toString(), effectiveRequestInit);
|
|
1113
|
+
if (!slideResponse.ok) {
|
|
1114
|
+
throw new Error(`Failed to load slide HTML: ${slide.file}`);
|
|
1115
|
+
}
|
|
1116
|
+
const html = await slideResponse.text();
|
|
1117
|
+
const parsedSlide = parseSlide(html, `${slideIdPrefix}${index + 1}`);
|
|
1118
|
+
return {
|
|
1119
|
+
...parsedSlide,
|
|
1120
|
+
hidden: slide.hidden === true,
|
|
1121
|
+
sourceFile: slide.file,
|
|
1122
|
+
title: slide.title || parsedSlide.title
|
|
1123
|
+
};
|
|
1124
|
+
})
|
|
1125
|
+
);
|
|
1126
|
+
return {
|
|
1127
|
+
manifest,
|
|
1128
|
+
slides
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// src/core/pdf-export.ts
|
|
1133
|
+
function planPdfExport({
|
|
1134
|
+
slides,
|
|
1135
|
+
selection
|
|
1136
|
+
}) {
|
|
1137
|
+
const resolvedSelection = selection ?? { mode: "all" };
|
|
1138
|
+
const mode = resolvedSelection.mode ?? "all";
|
|
1139
|
+
if (mode === "all") {
|
|
1140
|
+
return { mode: "all", slides: [...slides] };
|
|
1141
|
+
}
|
|
1142
|
+
if (mode === "slide") {
|
|
1143
|
+
const slideFile = "slideFile" in resolvedSelection ? resolvedSelection.slideFile?.trim() : "";
|
|
1144
|
+
if (!slideFile) {
|
|
1145
|
+
throw new Error("--slide requires a manifest slide file value");
|
|
1146
|
+
}
|
|
1147
|
+
const slide = findSlide(slides, slideFile);
|
|
1148
|
+
if (!slide) {
|
|
1149
|
+
throw new Error(`--slide must match a manifest slide file exactly: ${slideFile}`);
|
|
1150
|
+
}
|
|
1151
|
+
return { mode: "single", slides: [slide] };
|
|
1152
|
+
}
|
|
1153
|
+
const slideFiles = "slideFiles" in resolvedSelection ? resolvedSelection.slideFiles ?? [] : [];
|
|
1154
|
+
const requestedFiles = slideFiles.map((file) => file.trim()).filter(Boolean);
|
|
1155
|
+
if (requestedFiles.length === 0) {
|
|
1156
|
+
throw new Error("--slides requires at least one manifest slide file value");
|
|
1157
|
+
}
|
|
1158
|
+
const selectedSlides = [];
|
|
1159
|
+
const missingFiles = [];
|
|
1160
|
+
for (const slideFile of requestedFiles) {
|
|
1161
|
+
const slide = findSlide(slides, slideFile);
|
|
1162
|
+
if (!slide) {
|
|
1163
|
+
missingFiles.push(slideFile);
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
selectedSlides.push(slide);
|
|
1167
|
+
}
|
|
1168
|
+
if (missingFiles.length > 0) {
|
|
1169
|
+
throw new Error(`--slides must match manifest slide files exactly: ${missingFiles.join(", ")}`);
|
|
1170
|
+
}
|
|
1171
|
+
return { mode: "slides", slides: selectedSlides };
|
|
1172
|
+
}
|
|
1173
|
+
function planPdfExportSlides(slides, selection) {
|
|
1174
|
+
return planPdfExport({ slides, selection }).slides;
|
|
1175
|
+
}
|
|
1176
|
+
function findSlide(slides, slideFile) {
|
|
1177
|
+
return slides.find((slide) => slide.file === slideFile);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// src/core/html-export.ts
|
|
1181
|
+
function planHtmlExportSlides(slides) {
|
|
1182
|
+
const visibleSlides = slides.filter((slide) => slide.hidden !== true);
|
|
1183
|
+
return visibleSlides.length > 0 ? visibleSlides : [...slides];
|
|
1184
|
+
}
|
|
1185
|
+
function resolveHtmlExportSlides(slides) {
|
|
1186
|
+
return planHtmlExportSlides(slides).map((slide) => {
|
|
1187
|
+
const size = getSlideSize(slide);
|
|
1188
|
+
return {
|
|
1189
|
+
file: slide.file,
|
|
1190
|
+
...slide.title ? { title: slide.title } : {},
|
|
1191
|
+
htmlSource: slide.htmlSource,
|
|
1192
|
+
width: size.width,
|
|
1193
|
+
height: size.height
|
|
1194
|
+
};
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
function createSingleHtmlExportDocument({
|
|
1198
|
+
title = "Starry Slides",
|
|
1199
|
+
slides
|
|
1200
|
+
}) {
|
|
1201
|
+
const resolvedSlides = resolveHtmlExportSlides(slides);
|
|
1202
|
+
const deckPayload = {
|
|
1203
|
+
title,
|
|
1204
|
+
slides: resolvedSlides.map((slide) => ({
|
|
1205
|
+
file: slide.file,
|
|
1206
|
+
title: slide.title ?? slide.file,
|
|
1207
|
+
htmlSource: slide.htmlSource,
|
|
1208
|
+
width: slide.width,
|
|
1209
|
+
height: slide.height
|
|
1210
|
+
}))
|
|
1211
|
+
};
|
|
1212
|
+
return `<!DOCTYPE html>
|
|
1213
|
+
<html lang="en" data-starry-presenter="true">
|
|
1214
|
+
<head>
|
|
1215
|
+
<meta charset="utf-8" />
|
|
1216
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1217
|
+
<title>${escapeHtml(title)}</title>
|
|
1218
|
+
<style>${STANDALONE_PRESENTER_CSS}</style>
|
|
1219
|
+
</head>
|
|
1220
|
+
<body>
|
|
1221
|
+
<main id="starry-presenter" data-starry-presenter="true">
|
|
1222
|
+
<section class="presenter-stage" data-testid="presenter-view">
|
|
1223
|
+
<div class="presenter-slide-frame" data-testid="presenter-slide-frame">
|
|
1224
|
+
<iframe title="Presented slide" data-testid="presenter-slide-iframe"></iframe>
|
|
1225
|
+
<svg class="presenter-ink-layer" data-testid="presenter-ink-layer"></svg>
|
|
1226
|
+
</div>
|
|
1227
|
+
<div class="presenter-laser-cursor" data-testid="presenter-laser-cursor"></div>
|
|
1228
|
+
<div class="presenter-colors" data-testid="presenter-pen-colors" aria-label="Pen colors"></div>
|
|
1229
|
+
<nav class="presenter-toolbar" data-testid="presenter-toolbar" data-visible="false" aria-label="Presentation controls">
|
|
1230
|
+
<button type="button" data-action="previous" aria-label="Previous slide">Prev</button>
|
|
1231
|
+
<span class="presenter-pagination" data-role="pagination">1 / ${Math.max(resolvedSlides.length, 1)}</span>
|
|
1232
|
+
<button type="button" data-action="next" aria-label="Next slide">Next</button>
|
|
1233
|
+
<span class="presenter-divider"></span>
|
|
1234
|
+
<button type="button" data-tool="laser" aria-label="Laser pointer" aria-pressed="false">Laser</button>
|
|
1235
|
+
<button type="button" data-tool="pen" aria-label="Pen" aria-pressed="false">Pen</button>
|
|
1236
|
+
<button type="button" data-action="fullscreen" aria-label="Enter fullscreen">Fullscreen</button>
|
|
1237
|
+
<span class="presenter-divider"></span>
|
|
1238
|
+
<button type="button" data-action="exit" aria-label="Exit presentation">Exit</button>
|
|
1239
|
+
</nav>
|
|
1240
|
+
</section>
|
|
1241
|
+
</main>
|
|
1242
|
+
<script>
|
|
1243
|
+
window.starryPresenterDeck = ${safeJson(deckPayload)};
|
|
1244
|
+
${STANDALONE_PRESENTER_SCRIPT}
|
|
1245
|
+
</script>
|
|
1246
|
+
</body>
|
|
1247
|
+
</html>`;
|
|
1248
|
+
}
|
|
1249
|
+
function getSlideSize(slide) {
|
|
1250
|
+
if (slide.width && slide.height) {
|
|
1251
|
+
return { width: slide.width, height: slide.height };
|
|
1252
|
+
}
|
|
1253
|
+
return {
|
|
1254
|
+
width: parseDimension(
|
|
1255
|
+
matchAttribute(slide.htmlSource, "data-slide-width"),
|
|
1256
|
+
DEFAULT_SLIDE_WIDTH
|
|
1257
|
+
),
|
|
1258
|
+
height: parseDimension(
|
|
1259
|
+
matchAttribute(slide.htmlSource, "data-slide-height"),
|
|
1260
|
+
DEFAULT_SLIDE_HEIGHT
|
|
1261
|
+
)
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
function matchAttribute(html, attributeName) {
|
|
1265
|
+
const pattern = new RegExp(`${attributeName}\\s*=\\s*["']([^"']+)["']`, "i");
|
|
1266
|
+
return pattern.exec(html)?.[1] ?? null;
|
|
1267
|
+
}
|
|
1268
|
+
function safeJson(value) {
|
|
1269
|
+
return JSON.stringify(value).replace(/</g, "\\u003C").replace(/>/g, "\\u003E");
|
|
1270
|
+
}
|
|
1271
|
+
function escapeHtml(value) {
|
|
1272
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1273
|
+
}
|
|
1274
|
+
var STANDALONE_PRESENTER_CSS = `
|
|
1275
|
+
html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#111;color:#f8fafc;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
|
1276
|
+
*{box-sizing:border-box}
|
|
1277
|
+
#starry-presenter{display:block;width:100vw;height:100vh}
|
|
1278
|
+
.presenter-stage{position:fixed;inset:0;display:grid;place-items:center;overflow:hidden;background:#111;cursor:default}
|
|
1279
|
+
.presenter-stage[data-tool="laser"]{cursor:none}
|
|
1280
|
+
.presenter-slide-frame{position:relative;overflow:hidden;background:#fff;box-shadow:0 18px 70px rgba(0,0,0,.42)}
|
|
1281
|
+
.presenter-slide-frame iframe{display:block;border:0;background:#fff;pointer-events:none;transform-origin:top left}
|
|
1282
|
+
.presenter-ink-layer{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible}
|
|
1283
|
+
.presenter-ink-layer path{fill:none;stroke-linecap:round;stroke-linejoin:round;stroke-width:4}
|
|
1284
|
+
.presenter-laser-cursor{position:fixed;left:0;top:0;width:16px;height:16px;border-radius:999px;background:rgba(247,255,42,.92);box-shadow:0 0 0 1px rgba(17,24,39,.85),0 0 14px rgba(247,255,42,.9),0 0 24px rgba(34,211,238,.65);transform:translate(-50%,-50%);pointer-events:none;opacity:0}
|
|
1285
|
+
.presenter-stage[data-tool="laser"] .presenter-laser-cursor{opacity:1}
|
|
1286
|
+
.presenter-toolbar{position:fixed;left:50%;bottom:22px;z-index:20;display:flex;align-items:center;gap:7px;min-height:44px;padding:6px;border:1px solid rgba(255,255,255,.16);border-radius:10px;background:rgba(17,17,17,.84);box-shadow:0 18px 52px rgba(0,0,0,.38);backdrop-filter:blur(18px);transition:opacity .18s ease,transform .18s ease;transform:translate(-50%,0)}
|
|
1287
|
+
.presenter-toolbar[data-visible="false"]{opacity:0;transform:translate(-50%,16px);pointer-events:none}
|
|
1288
|
+
.presenter-toolbar button{height:32px;border:0;border-radius:6px;background:transparent;color:#f8fafc;padding:0 10px;font:600 12px/1 Inter,ui-sans-serif,system-ui;cursor:pointer}
|
|
1289
|
+
.presenter-toolbar button:hover,.presenter-toolbar button[aria-pressed="true"]{background:rgba(255,255,255,.14)}
|
|
1290
|
+
.presenter-toolbar button:disabled{opacity:.35;cursor:not-allowed}
|
|
1291
|
+
.presenter-pagination{min-width:56px;text-align:center;color:rgba(248,250,252,.78);font:600 12px/1 ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
1292
|
+
.presenter-divider{width:1px;height:20px;background:rgba(255,255,255,.16)}
|
|
1293
|
+
.presenter-colors{position:fixed;left:50%;bottom:78px;z-index:20;display:none;align-items:center;gap:8px;padding:8px;border:1px solid rgba(255,255,255,.16);border-radius:8px;background:rgba(17,17,17,.9);box-shadow:0 14px 44px rgba(0,0,0,.32);backdrop-filter:blur(18px);transform:translateX(-50%)}
|
|
1294
|
+
.presenter-stage[data-tool="pen"] .presenter-colors{display:flex}
|
|
1295
|
+
.presenter-swatch{width:28px;height:28px;border:1px solid rgba(255,255,255,.25);border-radius:999px;box-shadow:0 0 0 1px rgba(0,0,0,.25);padding:0;cursor:pointer}
|
|
1296
|
+
.presenter-swatch[data-active="true"]{outline:2px solid rgba(255,255,255,.9);outline-offset:2px}
|
|
1297
|
+
`;
|
|
1298
|
+
var STANDALONE_PRESENTER_SCRIPT = `
|
|
1299
|
+
(() => {
|
|
1300
|
+
const deck = window.starryPresenterDeck;
|
|
1301
|
+
const stage = document.querySelector(".presenter-stage");
|
|
1302
|
+
const frame = document.querySelector(".presenter-slide-frame");
|
|
1303
|
+
const iframe = document.querySelector("[data-testid='presenter-slide-iframe']");
|
|
1304
|
+
const inkLayer = document.querySelector("[data-testid='presenter-ink-layer']");
|
|
1305
|
+
const toolbar = document.querySelector("[data-testid='presenter-toolbar']");
|
|
1306
|
+
const pagination = document.querySelector("[data-role='pagination']");
|
|
1307
|
+
const laser = document.querySelector("[data-testid='presenter-laser-cursor']");
|
|
1308
|
+
const colorList = document.querySelector(".presenter-colors");
|
|
1309
|
+
const penColors = ["#F59E0B", "#EF4444", "#10B981", "#3B82F6", "#8B5CF6", "#FFFFFF", "#0F172A"];
|
|
1310
|
+
let index = 0;
|
|
1311
|
+
let tool = "none";
|
|
1312
|
+
let hideTimer = 0;
|
|
1313
|
+
let drawing = null;
|
|
1314
|
+
let penColor = penColors[0];
|
|
1315
|
+
let lastWheelAt = 0;
|
|
1316
|
+
|
|
1317
|
+
function activeSlide() {
|
|
1318
|
+
return deck.slides[Math.min(Math.max(index, 0), Math.max(deck.slides.length - 1, 0))];
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
function renderSlide() {
|
|
1322
|
+
const slide = activeSlide();
|
|
1323
|
+
if (!slide) return;
|
|
1324
|
+
const viewportWidth = window.innerWidth;
|
|
1325
|
+
const viewportHeight = window.innerHeight;
|
|
1326
|
+
const scale = Math.min(viewportWidth / slide.width, viewportHeight / slide.height);
|
|
1327
|
+
frame.style.width = Math.floor(slide.width * scale) + "px";
|
|
1328
|
+
frame.style.height = Math.floor(slide.height * scale) + "px";
|
|
1329
|
+
iframe.style.width = slide.width + "px";
|
|
1330
|
+
iframe.style.height = slide.height + "px";
|
|
1331
|
+
iframe.style.transform = "scale(" + scale + ")";
|
|
1332
|
+
iframe.srcdoc = slide.htmlSource;
|
|
1333
|
+
inkLayer.setAttribute("viewBox", "0 0 " + slide.width + " " + slide.height);
|
|
1334
|
+
inkLayer.dataset.scale = String(scale);
|
|
1335
|
+
inkLayer.replaceChildren();
|
|
1336
|
+
pagination.textContent = String(index + 1) + " / " + String(deck.slides.length);
|
|
1337
|
+
toolbar.querySelector("[data-action='previous']").disabled = index === 0;
|
|
1338
|
+
toolbar.querySelector("[data-action='next']").disabled = index >= deck.slides.length - 1;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
function setTool(nextTool) {
|
|
1342
|
+
tool = tool === nextTool ? "none" : nextTool;
|
|
1343
|
+
stage.dataset.tool = tool === "none" ? "" : tool;
|
|
1344
|
+
for (const button of toolbar.querySelectorAll("[data-tool]")) {
|
|
1345
|
+
button.setAttribute("aria-pressed", button.dataset.tool === tool ? "true" : "false");
|
|
1346
|
+
}
|
|
1347
|
+
drawing = null;
|
|
1348
|
+
updateCursor();
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function clearInk() {
|
|
1352
|
+
inkLayer.replaceChildren();
|
|
1353
|
+
drawing = null;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function renderPenColors() {
|
|
1357
|
+
colorList.replaceChildren(...penColors.map((color) => {
|
|
1358
|
+
const button = document.createElement("button");
|
|
1359
|
+
button.type = "button";
|
|
1360
|
+
button.className = "presenter-swatch";
|
|
1361
|
+
button.setAttribute("aria-label", "Use pen color " + color);
|
|
1362
|
+
button.dataset.active = color === penColor ? "true" : "false";
|
|
1363
|
+
button.style.background = color;
|
|
1364
|
+
button.addEventListener("click", () => {
|
|
1365
|
+
penColor = color;
|
|
1366
|
+
updateCursor();
|
|
1367
|
+
renderPenColors();
|
|
1368
|
+
});
|
|
1369
|
+
return button;
|
|
1370
|
+
}));
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
function createPenCursor(color) {
|
|
1374
|
+
const svg = "<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><circle cx='12' cy='12' r='6' fill='" + color + "' stroke='#111827' stroke-width='2'/></svg>";
|
|
1375
|
+
return "url(\\"data:image/svg+xml," + encodeURIComponent(svg) + "\\") 12 12, auto";
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function updateCursor() {
|
|
1379
|
+
if (tool === "laser") {
|
|
1380
|
+
stage.style.cursor = "none";
|
|
1381
|
+
} else if (tool === "pen") {
|
|
1382
|
+
stage.style.cursor = createPenCursor(penColor);
|
|
1383
|
+
} else {
|
|
1384
|
+
stage.style.cursor = "default";
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function showToolbar() {
|
|
1389
|
+
toolbar.dataset.visible = "true";
|
|
1390
|
+
window.clearTimeout(hideTimer);
|
|
1391
|
+
hideTimer = window.setTimeout(() => {
|
|
1392
|
+
toolbar.dataset.visible = "false";
|
|
1393
|
+
}, 1500);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function maybeShowToolbar(event) {
|
|
1397
|
+
if (event.clientY >= window.innerHeight - 96) {
|
|
1398
|
+
showToolbar();
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function pointInFrame(event) {
|
|
1403
|
+
const rect = frame.getBoundingClientRect();
|
|
1404
|
+
const scale = Number(inkLayer.dataset.scale || "1");
|
|
1405
|
+
return {
|
|
1406
|
+
x: Math.min(Math.max(event.clientX - rect.left, 0), rect.width) / scale,
|
|
1407
|
+
y: Math.min(Math.max(event.clientY - rect.top, 0), rect.height) / scale,
|
|
1408
|
+
inside: event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom,
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function beginInk(event) {
|
|
1413
|
+
if (tool !== "pen") return;
|
|
1414
|
+
if (!frame.contains(event.target)) return;
|
|
1415
|
+
const point = pointInFrame(event);
|
|
1416
|
+
if (!point.inside) return;
|
|
1417
|
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1418
|
+
path.setAttribute("stroke", penColor);
|
|
1419
|
+
path.setAttribute("d", "M " + point.x.toFixed(1) + " " + point.y.toFixed(1));
|
|
1420
|
+
inkLayer.append(path);
|
|
1421
|
+
drawing = { path, d: path.getAttribute("d") };
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function extendInk(event) {
|
|
1425
|
+
if (!drawing) return;
|
|
1426
|
+
const point = pointInFrame(event);
|
|
1427
|
+
drawing.d += " L " + point.x.toFixed(1) + " " + point.y.toFixed(1);
|
|
1428
|
+
drawing.path.setAttribute("d", drawing.d);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function goToIndex(nextIndex) {
|
|
1432
|
+
setTool("none");
|
|
1433
|
+
index = Math.min(Math.max(nextIndex, 0), deck.slides.length - 1);
|
|
1434
|
+
renderSlide();
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
function goNext() {
|
|
1438
|
+
goToIndex(index + 1);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
function updateFullscreenButton() {
|
|
1442
|
+
const button = toolbar.querySelector("[data-action='fullscreen']");
|
|
1443
|
+
const isFullscreen = Boolean(document.fullscreenElement);
|
|
1444
|
+
button.setAttribute("aria-label", isFullscreen ? "Exit fullscreen" : "Enter fullscreen");
|
|
1445
|
+
button.textContent = isFullscreen ? "Window" : "Fullscreen";
|
|
1446
|
+
renderSlide();
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
toolbar.addEventListener("click", (event) => {
|
|
1450
|
+
const button = event.target.closest("button");
|
|
1451
|
+
if (!button) return;
|
|
1452
|
+
showToolbar();
|
|
1453
|
+
if (button.dataset.action === "previous") {
|
|
1454
|
+
goToIndex(index - 1);
|
|
1455
|
+
} else if (button.dataset.action === "next") {
|
|
1456
|
+
goNext();
|
|
1457
|
+
} else if (button.dataset.action === "fullscreen") {
|
|
1458
|
+
if (document.fullscreenElement) {
|
|
1459
|
+
document.exitFullscreen?.();
|
|
1460
|
+
} else if (document.documentElement.requestFullscreen) {
|
|
1461
|
+
document.documentElement.requestFullscreen();
|
|
1462
|
+
}
|
|
1463
|
+
} else if (button.dataset.action === "exit") {
|
|
1464
|
+
window.close();
|
|
1465
|
+
} else if (button.dataset.tool) {
|
|
1466
|
+
setTool(button.dataset.tool);
|
|
1467
|
+
}
|
|
1468
|
+
});
|
|
1469
|
+
toolbar.addEventListener("pointerenter", showToolbar);
|
|
1470
|
+
toolbar.addEventListener("pointermove", showToolbar);
|
|
1471
|
+
|
|
1472
|
+
window.addEventListener("resize", renderSlide);
|
|
1473
|
+
document.addEventListener("fullscreenchange", updateFullscreenButton);
|
|
1474
|
+
window.addEventListener("pointermove", (event) => {
|
|
1475
|
+
maybeShowToolbar(event);
|
|
1476
|
+
if (tool === "laser") {
|
|
1477
|
+
laser.style.transform = "translate(" + event.clientX + "px," + event.clientY + "px) translate(-50%,-50%)";
|
|
1478
|
+
}
|
|
1479
|
+
extendInk(event);
|
|
1480
|
+
});
|
|
1481
|
+
window.addEventListener("pointerdown", beginInk);
|
|
1482
|
+
window.addEventListener("click", (event) => {
|
|
1483
|
+
if (tool !== "none") return;
|
|
1484
|
+
if (!frame.contains(event.target)) return;
|
|
1485
|
+
goNext();
|
|
1486
|
+
});
|
|
1487
|
+
window.addEventListener("wheel", (event) => {
|
|
1488
|
+
if (tool !== "none") return;
|
|
1489
|
+
if (Math.abs(event.deltaY) < Math.abs(event.deltaX) || event.deltaY <= 8) return;
|
|
1490
|
+
const now = Date.now();
|
|
1491
|
+
if (now - lastWheelAt < 360) return;
|
|
1492
|
+
lastWheelAt = now;
|
|
1493
|
+
event.preventDefault();
|
|
1494
|
+
goNext();
|
|
1495
|
+
}, { passive: false });
|
|
1496
|
+
window.addEventListener("pointerup", () => {
|
|
1497
|
+
drawing = null;
|
|
1498
|
+
});
|
|
1499
|
+
window.addEventListener("keydown", (event) => {
|
|
1500
|
+
if (event.key === "Escape") {
|
|
1501
|
+
if (tool === "pen") {
|
|
1502
|
+
setTool("none");
|
|
1503
|
+
clearInk();
|
|
1504
|
+
}
|
|
1505
|
+
showToolbar();
|
|
1506
|
+
} else if (event.key === "ArrowRight" || event.key === "ArrowDown" || event.key === "PageDown") {
|
|
1507
|
+
goNext();
|
|
1508
|
+
} else if (event.key === "ArrowLeft" || event.key === "ArrowUp" || event.key === "PageUp") {
|
|
1509
|
+
goToIndex(index - 1);
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1512
|
+
renderPenColors();
|
|
1513
|
+
updateCursor();
|
|
1514
|
+
renderSlide();
|
|
1515
|
+
})();
|
|
1516
|
+
`;
|
|
1517
|
+
|
|
1518
|
+
export {
|
|
1519
|
+
SELECTOR_ATTR,
|
|
1520
|
+
SLIDE_ROOT_ATTR,
|
|
1521
|
+
DEFAULT_SLIDE_WIDTH,
|
|
1522
|
+
DEFAULT_SLIDE_HEIGHT,
|
|
1523
|
+
getSlideElementSelector,
|
|
1524
|
+
getSlideRootSelector,
|
|
1525
|
+
createElementId,
|
|
1526
|
+
parseDimension,
|
|
1527
|
+
normalizeSlideId,
|
|
1528
|
+
ensureEditableSelectors,
|
|
1529
|
+
parseSlide,
|
|
1530
|
+
querySlideElement,
|
|
1531
|
+
getSlideInlineStyleValue,
|
|
1532
|
+
parseHtmlDocument2 as parseHtmlDocument,
|
|
1533
|
+
serializeHtmlDocument2 as serializeHtmlDocument,
|
|
1534
|
+
updateHtmlSource,
|
|
1535
|
+
ELEMENT_LAYOUT_STYLE_KEYS,
|
|
1536
|
+
createEmptyElementLayoutStyleSnapshot,
|
|
1537
|
+
captureElementLayoutStyleSnapshot,
|
|
1538
|
+
normalizeElementLayoutStyleSnapshot,
|
|
1539
|
+
parseTransformParts,
|
|
1540
|
+
composeTransform,
|
|
1541
|
+
elementRectToStageRect,
|
|
1542
|
+
stageDeltaToSlideDelta,
|
|
1543
|
+
applySlideOperation,
|
|
1544
|
+
invertSlideOperation,
|
|
1545
|
+
updateSlideText,
|
|
1546
|
+
updateSlideStyle,
|
|
1547
|
+
updateSlideAttribute,
|
|
1548
|
+
duplicateSlideElement,
|
|
1549
|
+
updateSlideElementLayout,
|
|
1550
|
+
createElementPlacement,
|
|
1551
|
+
getSlideElementHtml,
|
|
1552
|
+
updateSlideElementHtmlIds,
|
|
1553
|
+
createUniqueElementId,
|
|
1554
|
+
insertSlideElement,
|
|
1555
|
+
removeSlideElement,
|
|
1556
|
+
updateSlideElementTransform,
|
|
1557
|
+
createGroupCreateOperation,
|
|
1558
|
+
createGroupUngroupOperation,
|
|
1559
|
+
createUniqueSlideId,
|
|
1560
|
+
createUniqueSlideSourceFile,
|
|
1561
|
+
createBlankSlide,
|
|
1562
|
+
createDuplicatedSlide,
|
|
1563
|
+
createHistoryState,
|
|
1564
|
+
reduceHistory,
|
|
1565
|
+
loadSlidesFromManifest,
|
|
1566
|
+
planPdfExport,
|
|
1567
|
+
planPdfExportSlides,
|
|
1568
|
+
planHtmlExportSlides,
|
|
1569
|
+
resolveHtmlExportSlides,
|
|
1570
|
+
createSingleHtmlExportDocument
|
|
1571
|
+
};
|