@ohos-ports/slides-grab 1.5.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README-ko.md +292 -0
- package/README.md +301 -0
- package/bin/ohos-postinstall.cjs +118 -0
- package/bin/ppt-agent.js +620 -0
- package/convert.cjs +20 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/glib-2.0/include/glibconfig.h +220 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/index.js +1 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libexpat.so +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libffi.so +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libjpeg.so +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libpcre2-8.so.0 +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libvips-cpp.so.42.20.3 +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libvips.so.42.20.3 +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/package.json +13 -0
- package/native/@img/sharp-openharmony-arm64/lib/sharp-openharmony-arm64.node +0 -0
- package/native/@img/sharp-openharmony-arm64/package.json +16 -0
- package/package.json +95 -0
- package/runtimes/claude-code/agents/design-critic-agent.md +23 -0
- package/runtimes/codex/agents/slides-grab-design-critic.md +22 -0
- package/scripts/build-viewer.js +444 -0
- package/scripts/design-gate.js +258 -0
- package/scripts/download-video.js +213 -0
- package/scripts/editor-server.js +1167 -0
- package/scripts/figma-export.js +169 -0
- package/scripts/generate-image.js +116 -0
- package/scripts/generate-images.js +164 -0
- package/scripts/html2pdf.js +822 -0
- package/scripts/html2png.js +246 -0
- package/scripts/html2pptx.js +162 -0
- package/scripts/import-template.js +86 -0
- package/scripts/install-runtime.js +216 -0
- package/scripts/render-tldraw.js +44 -0
- package/scripts/validate-slides.js +221 -0
- package/skills/slides-grab/SKILL.md +65 -0
- package/skills/slides-grab/references/presentation-workflow-reference.md +61 -0
- package/skills/slides-grab-card-news/SKILL.md +35 -0
- package/skills/slides-grab-design/SKILL.md +88 -0
- package/skills/slides-grab-design/references/beautiful-slide-defaults.md +88 -0
- package/skills/slides-grab-design/references/design-gate.md +349 -0
- package/skills/slides-grab-design/references/design-rules.md +76 -0
- package/skills/slides-grab-design/references/design-system-full.md +568 -0
- package/skills/slides-grab-design/references/detailed-design-rules.md +70 -0
- package/skills/slides-grab-export/SKILL.md +55 -0
- package/skills/slides-grab-export/references/export-rules.md +27 -0
- package/skills/slides-grab-export/references/html2pptx.md +627 -0
- package/skills/slides-grab-export/references/ooxml.md +427 -0
- package/skills/slides-grab-export/references/pptx-skill-reference.md +189 -0
- package/skills/slides-grab-html/SKILL.md +67 -0
- package/skills/slides-grab-image/SKILL.md +82 -0
- package/skills/slides-grab-plan/SKILL.md +68 -0
- package/skills/slides-grab-plan/references/design-md-to-slides-conversion.md +135 -0
- package/skills/slides-grab-plan/references/outline-format.md +47 -0
- package/skills/slides-grab-plan/references/plan-workflow-reference.md +140 -0
- package/src/codex-imagen.js +182 -0
- package/src/design-diversity-data.js +6932 -0
- package/src/design-gate-report.js +244 -0
- package/src/design-gate-state.js +329 -0
- package/src/design-import.js +164 -0
- package/src/design-md-parser.js +415 -0
- package/src/design-styles-data.js +1928 -0
- package/src/design-styles.js +209 -0
- package/src/editor/codex-edit.js +584 -0
- package/src/editor/edit-subprocess.js +170 -0
- package/src/editor/editor-codex-prompt.md +50 -0
- package/src/editor/editor.html +1785 -0
- package/src/editor/js/editor-bbox.js +332 -0
- package/src/editor/js/editor-chat.js +56 -0
- package/src/editor/js/editor-direct-edit.js +238 -0
- package/src/editor/js/editor-dom.js +59 -0
- package/src/editor/js/editor-init.js +405 -0
- package/src/editor/js/editor-navigation.js +54 -0
- package/src/editor/js/editor-select.js +558 -0
- package/src/editor/js/editor-send.js +175 -0
- package/src/editor/js/editor-sse.js +163 -0
- package/src/editor/js/editor-state.js +41 -0
- package/src/editor/js/editor-type.js +71 -0
- package/src/editor/js/editor-utils.js +167 -0
- package/src/editor/js/model-registry.js +37 -0
- package/src/editor/screenshot.js +82 -0
- package/src/export-resolution.cjs +68 -0
- package/src/figma.js +71 -0
- package/src/html2pptx-scale.cjs +120 -0
- package/src/html2pptx.cjs +1262 -0
- package/src/image-contract.js +329 -0
- package/src/image-native.js +468 -0
- package/src/nano-banana.js +841 -0
- package/src/ohos-browser-bridge.cjs +272 -0
- package/src/pptx-raster-export.cjs +299 -0
- package/src/resolve.js +110 -0
- package/src/slide-mode.cjs +72 -0
- package/src/template-fidelity.js +267 -0
- package/src/template-import.js +505 -0
- package/src/template-layout.js +261 -0
- package/src/template-pack.js +255 -0
- package/src/tldraw/render.js +473 -0
- package/src/validation/cli.js +120 -0
- package/src/validation/core.js +1023 -0
- package/templates/chart.html +121 -0
- package/templates/closing.html +54 -0
- package/templates/content.html +50 -0
- package/templates/contents.html +60 -0
- package/templates/cover.html +64 -0
- package/templates/custom/.gitkeep +0 -0
- package/templates/custom/README.md +7 -0
- package/templates/design-styles/README.md +20 -0
- package/templates/design-styles/preview.html +4438 -0
- package/templates/diagram-tldraw.html +56 -0
- package/templates/diagram.html +98 -0
- package/templates/quote.html +31 -0
- package/templates/section-divider.html +43 -0
- package/templates/split-layout.html +43 -0
- package/templates/statistics.html +55 -0
- package/templates/team.html +49 -0
- package/templates/timeline.html +59 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
// editor-select.js — Object selection, hover, tool mode UI
|
|
2
|
+
|
|
3
|
+
import { state, TOOL_MODE_DRAW, TOOL_MODE_SELECT, SLIDE_W, SLIDE_H, NON_SELECTABLE_TAGS, DIRECT_TEXT_TAGS } from './editor-state.js';
|
|
4
|
+
import {
|
|
5
|
+
slideIframe, slidePanel, objectLayer, drawBox, toolModeDrawBtn, toolModeSelectBtn,
|
|
6
|
+
bboxToolbar, selectToolbar, editorHint, objectSelectedBox, objectHoverBox,
|
|
7
|
+
selectedObjectMini, miniTag, miniText, selectEmptyHint,
|
|
8
|
+
toggleBold, toggleItalic, toggleUnderline, toggleStrike,
|
|
9
|
+
alignLeft, alignCenter, alignRight,
|
|
10
|
+
popoverTextInput, popoverApplyText, popoverTextColorInput, popoverBgColorInput,
|
|
11
|
+
popoverSizeInput, popoverApplySize,
|
|
12
|
+
} from './editor-dom.js';
|
|
13
|
+
import {
|
|
14
|
+
currentSlideFile, getSlideState, setStatus, clamp,
|
|
15
|
+
normalizeHexColor, parsePixelValue, isBoldFontWeight,
|
|
16
|
+
} from './editor-utils.js';
|
|
17
|
+
import { renderBboxes, scaleSlide, clientToSlidePoint, getXPath } from './editor-bbox.js';
|
|
18
|
+
|
|
19
|
+
const MIN_OBJECT_SIZE = 16;
|
|
20
|
+
const OBJECT_MOVE_EPSILON = 4;
|
|
21
|
+
const SLIDE_BACKGROUND_AREA_RATIO = 0.85;
|
|
22
|
+
const RESIZE_HANDLE_STYLES = new Set(['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se', 'move']);
|
|
23
|
+
|
|
24
|
+
let objectTransform = null;
|
|
25
|
+
let commitObjectTransform = () => {};
|
|
26
|
+
let suppressNextObjectClick = false;
|
|
27
|
+
|
|
28
|
+
export function setObjectTransformCommitHandler(callback) {
|
|
29
|
+
commitObjectTransform = typeof callback === 'function' ? callback : (() => {});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isObjectTransformActive() {
|
|
33
|
+
return Boolean(objectTransform);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveObjectTransformTarget() {
|
|
37
|
+
const selected = getSelectedObjectElement();
|
|
38
|
+
if (!selected) return null;
|
|
39
|
+
|
|
40
|
+
const rect = elementToSlideRect(selected);
|
|
41
|
+
return rect ? { element: selected, rect } : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function clampObjectGeometry(base, dx, dy, handle = 'move') {
|
|
45
|
+
const next = {
|
|
46
|
+
x: base.x,
|
|
47
|
+
y: base.y,
|
|
48
|
+
width: base.width,
|
|
49
|
+
height: base.height,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (handle === 'move') {
|
|
53
|
+
next.x = base.x + dx;
|
|
54
|
+
next.y = base.y + dy;
|
|
55
|
+
} else {
|
|
56
|
+
if (handle.includes('w')) {
|
|
57
|
+
next.x = base.x + dx;
|
|
58
|
+
next.width = base.width - dx;
|
|
59
|
+
}
|
|
60
|
+
if (handle.includes('e')) {
|
|
61
|
+
next.width = base.width + dx;
|
|
62
|
+
}
|
|
63
|
+
if (handle.includes('n')) {
|
|
64
|
+
next.y = base.y + dy;
|
|
65
|
+
next.height = base.height - dy;
|
|
66
|
+
}
|
|
67
|
+
if (handle.includes('s')) {
|
|
68
|
+
next.height = base.height + dy;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
next.x = clamp(Math.round(next.x), 0, SLIDE_W - MIN_OBJECT_SIZE);
|
|
73
|
+
next.y = Math.round(next.y);
|
|
74
|
+
next.y = clamp(next.y, 0, SLIDE_H - MIN_OBJECT_SIZE);
|
|
75
|
+
|
|
76
|
+
if (next.width < MIN_OBJECT_SIZE) {
|
|
77
|
+
next.width = MIN_OBJECT_SIZE;
|
|
78
|
+
if (handle.includes('w')) {
|
|
79
|
+
next.x = base.x + base.width - MIN_OBJECT_SIZE;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (next.height < MIN_OBJECT_SIZE) {
|
|
84
|
+
next.height = MIN_OBJECT_SIZE;
|
|
85
|
+
if (handle.includes('n')) {
|
|
86
|
+
next.y = base.y + base.height - MIN_OBJECT_SIZE;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
next.x = clamp(Math.round(next.x), 0, SLIDE_W - MIN_OBJECT_SIZE);
|
|
91
|
+
next.y = clamp(Math.round(next.y), 0, SLIDE_H - MIN_OBJECT_SIZE);
|
|
92
|
+
next.width = clamp(Math.round(next.width), MIN_OBJECT_SIZE, SLIDE_W - next.x);
|
|
93
|
+
next.height = clamp(Math.round(next.height), MIN_OBJECT_SIZE, SLIDE_H - next.y);
|
|
94
|
+
|
|
95
|
+
return next;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getObjectHandleAnchor(rect, handleType) {
|
|
99
|
+
const hasWest = handleType.includes('w');
|
|
100
|
+
const hasEast = handleType.includes('e');
|
|
101
|
+
const hasNorth = handleType.includes('n');
|
|
102
|
+
const hasSouth = handleType.includes('s');
|
|
103
|
+
|
|
104
|
+
if (hasWest && !hasEast) {
|
|
105
|
+
return {
|
|
106
|
+
x: rect.x,
|
|
107
|
+
y: rect.y + (hasNorth ? 0 : hasSouth ? rect.height : rect.height / 2),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (hasEast && !hasWest) {
|
|
112
|
+
return {
|
|
113
|
+
x: rect.x + rect.width,
|
|
114
|
+
y: rect.y + (hasNorth ? 0 : hasSouth ? rect.height : rect.height / 2),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (hasNorth) {
|
|
119
|
+
return {
|
|
120
|
+
x: rect.x + rect.width / 2,
|
|
121
|
+
y: rect.y,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (hasSouth) {
|
|
126
|
+
return {
|
|
127
|
+
x: rect.x + rect.width / 2,
|
|
128
|
+
y: rect.y + rect.height,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
x: rect.x,
|
|
134
|
+
y: rect.y,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getOffsetParentOrigin(element) {
|
|
139
|
+
const offsetParent = element.offsetParent;
|
|
140
|
+
if (!isElementNode(offsetParent)) {
|
|
141
|
+
return { x: 0, y: 0 };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const tag = offsetParent.tagName.toLowerCase();
|
|
145
|
+
const frameWindow = slideIframe.contentWindow;
|
|
146
|
+
const styles = frameWindow?.getComputedStyle ? frameWindow.getComputedStyle(offsetParent) : null;
|
|
147
|
+
if ((tag === 'body' || tag === 'html') && styles?.position === 'static') {
|
|
148
|
+
return { x: 0, y: 0 };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const rect = offsetParent.getBoundingClientRect();
|
|
152
|
+
return {
|
|
153
|
+
x: rect.left + parsePixelValue(styles?.borderLeftWidth, 0),
|
|
154
|
+
y: rect.top + parsePixelValue(styles?.borderTopWidth, 0),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function serializeObjectTransformDocument(doc) {
|
|
159
|
+
if (!doc?.documentElement) return '';
|
|
160
|
+
const doctype = doc.doctype ? `<!DOCTYPE ${doc.doctype.name}>` : '<!DOCTYPE html>';
|
|
161
|
+
return `${doctype}\n${doc.documentElement.outerHTML}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function applyObjectGeometryToElement(element, rect) {
|
|
165
|
+
element.style.position = element.style.position || 'absolute';
|
|
166
|
+
element.style.boxSizing = 'border-box';
|
|
167
|
+
|
|
168
|
+
const origin = getOffsetParentOrigin(element);
|
|
169
|
+
const localX = rect.x - origin.x;
|
|
170
|
+
const localY = rect.y - origin.y;
|
|
171
|
+
|
|
172
|
+
element.style.left = `${Math.round(localX)}px`;
|
|
173
|
+
element.style.top = `${Math.round(localY)}px`;
|
|
174
|
+
element.style.width = `${Math.round(rect.width)}px`;
|
|
175
|
+
element.style.height = `${Math.round(rect.height)}px`;
|
|
176
|
+
element.style.maxWidth = 'none';
|
|
177
|
+
element.style.maxHeight = 'none';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isElementNode(node) {
|
|
181
|
+
return Boolean(node) && node.nodeType === Node.ELEMENT_NODE;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isSlideBackgroundElement(el) {
|
|
185
|
+
const rect = el.getBoundingClientRect();
|
|
186
|
+
const areaRatio = (rect.width * rect.height) / (SLIDE_W * SLIDE_H);
|
|
187
|
+
return areaRatio >= SLIDE_BACKGROUND_AREA_RATIO && el.children.length > 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function consumeObjectTransformClickSuppression() {
|
|
191
|
+
if (!suppressNextObjectClick) return false;
|
|
192
|
+
suppressNextObjectClick = false;
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function resolveXPath(doc, xpath) {
|
|
197
|
+
if (!doc || typeof xpath !== 'string' || xpath.trim() === '') return null;
|
|
198
|
+
try {
|
|
199
|
+
return doc.evaluate(xpath, doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function getSelectedObjectElement(slide = currentSlideFile()) {
|
|
206
|
+
if (!slide) return null;
|
|
207
|
+
const ss = getSlideState(slide);
|
|
208
|
+
const xpath = ss.selectedObjectXPath;
|
|
209
|
+
if (!xpath) return null;
|
|
210
|
+
|
|
211
|
+
const doc = slideIframe.contentDocument;
|
|
212
|
+
const el = resolveXPath(doc, xpath);
|
|
213
|
+
return isElementNode(el) ? el : null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function isSelectableElement(el) {
|
|
217
|
+
if (!isElementNode(el)) return false;
|
|
218
|
+
const tag = el.tagName.toLowerCase();
|
|
219
|
+
if (NON_SELECTABLE_TAGS.has(tag)) return false;
|
|
220
|
+
const rect = el.getBoundingClientRect();
|
|
221
|
+
return rect.width > 1 && rect.height > 1;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function isTextEditableElement(el) {
|
|
225
|
+
if (!isElementNode(el)) return false;
|
|
226
|
+
const tag = el.tagName.toLowerCase();
|
|
227
|
+
if (!DIRECT_TEXT_TAGS.has(tag)) return false;
|
|
228
|
+
const INLINE_TAGS = new Set(['BR','B','STRONG','I','EM','U','S','SPAN','A','SMALL','SUB','SUP','MARK','CODE']);
|
|
229
|
+
return Array.from(el.children).every(c => INLINE_TAGS.has(c.tagName));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function getSelectableTargetAt(clientX, clientY) {
|
|
233
|
+
const doc = slideIframe.contentDocument;
|
|
234
|
+
if (!doc) return null;
|
|
235
|
+
|
|
236
|
+
const point = clientToSlidePoint(clientX, clientY);
|
|
237
|
+
let node = doc.elementFromPoint(point.x, point.y);
|
|
238
|
+
while (node && !isSelectableElement(node)) {
|
|
239
|
+
node = node.parentElement;
|
|
240
|
+
}
|
|
241
|
+
if (!isElementNode(node)) return null;
|
|
242
|
+
return isSlideBackgroundElement(node) ? null : node;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function elementToSlideRect(el) {
|
|
246
|
+
if (!isElementNode(el)) return null;
|
|
247
|
+
const rect = el.getBoundingClientRect();
|
|
248
|
+
if (!rect.width || !rect.height) return null;
|
|
249
|
+
return {
|
|
250
|
+
x: clamp(Math.round(rect.left), 0, SLIDE_W),
|
|
251
|
+
y: clamp(Math.round(rect.top), 0, SLIDE_H),
|
|
252
|
+
width: Math.max(1, Math.round(rect.width)),
|
|
253
|
+
height: Math.max(1, Math.round(rect.height)),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function applyOverlayRect(node, rect) {
|
|
258
|
+
if (!node || !rect) {
|
|
259
|
+
if (node) node.style.display = 'none';
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
node.style.display = 'block';
|
|
264
|
+
node.style.left = `${rect.x}px`;
|
|
265
|
+
node.style.top = `${rect.y}px`;
|
|
266
|
+
node.style.width = `${rect.width}px`;
|
|
267
|
+
node.style.height = `${rect.height}px`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function readSelectedObjectStyleState(el) {
|
|
271
|
+
const frameWindow = slideIframe.contentWindow;
|
|
272
|
+
const styles = frameWindow?.getComputedStyle ? frameWindow.getComputedStyle(el) : null;
|
|
273
|
+
const textDecorationLine = styles?.textDecorationLine || '';
|
|
274
|
+
return {
|
|
275
|
+
textEditable: isTextEditableElement(el),
|
|
276
|
+
textValue: (el.innerHTML || '').replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, '').trim(),
|
|
277
|
+
textColor: normalizeHexColor(styles?.color, '#111111'),
|
|
278
|
+
backgroundColor: normalizeHexColor(styles?.backgroundColor, '#ffffff'),
|
|
279
|
+
fontSize: parsePixelValue(styles?.fontSize, 24),
|
|
280
|
+
bold: isBoldFontWeight(styles?.fontWeight),
|
|
281
|
+
italic: styles?.fontStyle === 'italic',
|
|
282
|
+
underline: /\bunderline\b/.test(textDecorationLine),
|
|
283
|
+
strike: /\bline-through\b/.test(textDecorationLine),
|
|
284
|
+
textAlign: styles?.textAlign || 'left',
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function setToggleActive(button, active) {
|
|
289
|
+
if (!button) return;
|
|
290
|
+
button.classList.toggle('active', Boolean(active));
|
|
291
|
+
button.setAttribute('aria-pressed', active ? 'true' : 'false');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function setControlEnabled(button, enabled) {
|
|
295
|
+
if (!button) return;
|
|
296
|
+
button.disabled = !enabled;
|
|
297
|
+
button.setAttribute('aria-disabled', enabled ? 'false' : 'true');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function getSelectedObjectCapabilities(el) {
|
|
301
|
+
const textEditable = isTextEditableElement(el);
|
|
302
|
+
return {
|
|
303
|
+
textEditable,
|
|
304
|
+
textColorEditable: textEditable,
|
|
305
|
+
backgroundEditable: isElementNode(el),
|
|
306
|
+
sizeEditable: textEditable,
|
|
307
|
+
emphasisEditable: textEditable,
|
|
308
|
+
alignEditable: textEditable,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function syncInlineInputs(snapshot) {
|
|
313
|
+
if (!document.activeElement || !document.activeElement.closest?.('#select-toolbar')) {
|
|
314
|
+
popoverTextInput.value = snapshot?.textValue || '';
|
|
315
|
+
popoverSizeInput.value = String(snapshot?.fontSize || 24);
|
|
316
|
+
}
|
|
317
|
+
popoverTextColorInput.value = snapshot?.textColor || '#111111';
|
|
318
|
+
popoverBgColorInput.value = snapshot?.backgroundColor || '#ffffff';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function updateObjectEditorControls() {
|
|
322
|
+
const selected = state.toolMode === TOOL_MODE_SELECT ? getSelectedObjectElement() : null;
|
|
323
|
+
const snapshot = selected ? readSelectedObjectStyleState(selected) : null;
|
|
324
|
+
const capabilities = getSelectedObjectCapabilities(selected);
|
|
325
|
+
|
|
326
|
+
if (bboxToolbar) {
|
|
327
|
+
bboxToolbar.hidden = state.toolMode !== TOOL_MODE_DRAW;
|
|
328
|
+
}
|
|
329
|
+
if (selectToolbar) {
|
|
330
|
+
selectToolbar.hidden = state.toolMode !== TOOL_MODE_SELECT;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (selectedObjectMini && selectEmptyHint) {
|
|
334
|
+
if (selected) {
|
|
335
|
+
const tag = selected.tagName.toLowerCase();
|
|
336
|
+
const fullText = (selected.textContent || '').trim();
|
|
337
|
+
const preview = fullText.slice(0, 24);
|
|
338
|
+
miniTag.textContent = `<${tag}>`;
|
|
339
|
+
miniText.textContent = preview ? ` ${preview}${fullText.length > 24 ? '\u2026' : ''}` : '';
|
|
340
|
+
selectedObjectMini.style.display = '';
|
|
341
|
+
selectEmptyHint.style.display = 'none';
|
|
342
|
+
} else {
|
|
343
|
+
selectedObjectMini.style.display = 'none';
|
|
344
|
+
selectEmptyHint.style.display = state.toolMode === TOOL_MODE_SELECT ? '' : 'none';
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
popoverTextInput.disabled = !capabilities.textEditable;
|
|
349
|
+
popoverApplyText.disabled = !capabilities.textEditable;
|
|
350
|
+
popoverTextColorInput.disabled = !capabilities.textColorEditable;
|
|
351
|
+
popoverBgColorInput.disabled = !capabilities.backgroundEditable;
|
|
352
|
+
popoverSizeInput.disabled = !capabilities.sizeEditable;
|
|
353
|
+
popoverApplySize.disabled = !capabilities.sizeEditable;
|
|
354
|
+
|
|
355
|
+
setControlEnabled(toggleBold, capabilities.emphasisEditable);
|
|
356
|
+
setControlEnabled(toggleItalic, capabilities.emphasisEditable);
|
|
357
|
+
setControlEnabled(toggleUnderline, capabilities.emphasisEditable);
|
|
358
|
+
setControlEnabled(toggleStrike, capabilities.emphasisEditable);
|
|
359
|
+
setControlEnabled(alignLeft, capabilities.alignEditable);
|
|
360
|
+
setControlEnabled(alignCenter, capabilities.alignEditable);
|
|
361
|
+
setControlEnabled(alignRight, capabilities.alignEditable);
|
|
362
|
+
|
|
363
|
+
setToggleActive(toggleBold, capabilities.emphasisEditable && snapshot?.bold);
|
|
364
|
+
setToggleActive(toggleItalic, capabilities.emphasisEditable && snapshot?.italic);
|
|
365
|
+
setToggleActive(toggleUnderline, capabilities.emphasisEditable && snapshot?.underline);
|
|
366
|
+
setToggleActive(toggleStrike, capabilities.emphasisEditable && snapshot?.strike);
|
|
367
|
+
setToggleActive(alignLeft, capabilities.alignEditable && (snapshot?.textAlign === 'left' || snapshot?.textAlign === 'start'));
|
|
368
|
+
setToggleActive(alignCenter, capabilities.alignEditable && snapshot?.textAlign === 'center');
|
|
369
|
+
setToggleActive(alignRight, capabilities.alignEditable && (snapshot?.textAlign === 'right' || snapshot?.textAlign === 'end'));
|
|
370
|
+
|
|
371
|
+
syncInlineInputs(snapshot);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function beginObjectTransform(event) {
|
|
375
|
+
if (!objectSelectedBox || state.toolMode !== TOOL_MODE_SELECT) return;
|
|
376
|
+
const button = event.button;
|
|
377
|
+
if (typeof button === 'number' && button !== 0) return;
|
|
378
|
+
|
|
379
|
+
const target = event.target;
|
|
380
|
+
if (!(target instanceof HTMLElement)) return;
|
|
381
|
+
const handle = target.closest('[data-object-handle]');
|
|
382
|
+
const handleType = handle?.dataset?.objectHandle || 'move';
|
|
383
|
+
if (!RESIZE_HANDLE_STYLES.has(handleType)) return;
|
|
384
|
+
|
|
385
|
+
if (!objectSelectedBox.contains(target) && target !== objectSelectedBox) return;
|
|
386
|
+
|
|
387
|
+
const selected = resolveObjectTransformTarget();
|
|
388
|
+
if (!selected) return;
|
|
389
|
+
|
|
390
|
+
const start = clientToSlidePoint(event.clientX, event.clientY);
|
|
391
|
+
const anchor = getObjectHandleAnchor(selected.rect, handleType);
|
|
392
|
+
objectTransform = {
|
|
393
|
+
element: selected.element,
|
|
394
|
+
startRect: selected.rect,
|
|
395
|
+
nextRect: selected.rect,
|
|
396
|
+
startHtml: serializeObjectTransformDocument(slideIframe.contentDocument),
|
|
397
|
+
hasMoved: false,
|
|
398
|
+
pointerDownX: start.x,
|
|
399
|
+
pointerDownY: start.y,
|
|
400
|
+
handleType,
|
|
401
|
+
anchorX: anchor.x,
|
|
402
|
+
anchorY: anchor.y,
|
|
403
|
+
pointerOffsetX: start.x - anchor.x,
|
|
404
|
+
pointerOffsetY: start.y - anchor.y,
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
event.preventDefault();
|
|
408
|
+
event.stopPropagation();
|
|
409
|
+
objectSelectedBox.style.cursor = handleType === 'move' ? 'move' : 'grabbing';
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function continueObjectTransform(event) {
|
|
413
|
+
if (!isObjectTransformActive()) return;
|
|
414
|
+
const {
|
|
415
|
+
element,
|
|
416
|
+
startRect,
|
|
417
|
+
handleType,
|
|
418
|
+
anchorX,
|
|
419
|
+
anchorY,
|
|
420
|
+
pointerOffsetX,
|
|
421
|
+
pointerOffsetY,
|
|
422
|
+
pointerDownX,
|
|
423
|
+
pointerDownY,
|
|
424
|
+
hasMoved,
|
|
425
|
+
} = objectTransform;
|
|
426
|
+
if (!element || !objectSelectedBox) return;
|
|
427
|
+
|
|
428
|
+
const cursor = clientToSlidePoint(event.clientX, event.clientY);
|
|
429
|
+
const adjustedCursorX = cursor.x - pointerOffsetX;
|
|
430
|
+
const adjustedCursorY = cursor.y - pointerOffsetY;
|
|
431
|
+
const dx = adjustedCursorX - anchorX;
|
|
432
|
+
const dy = adjustedCursorY - anchorY;
|
|
433
|
+
|
|
434
|
+
if (!hasMoved && Math.abs(cursor.x - pointerDownX) <= OBJECT_MOVE_EPSILON && Math.abs(cursor.y - pointerDownY) <= OBJECT_MOVE_EPSILON) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
objectTransform.hasMoved = true;
|
|
439
|
+
|
|
440
|
+
const nextRect = clampObjectGeometry(startRect, dx, dy, handleType);
|
|
441
|
+
|
|
442
|
+
objectTransform.nextRect = nextRect;
|
|
443
|
+
applyObjectGeometryToElement(element, nextRect);
|
|
444
|
+
applyOverlayRect(objectSelectedBox, nextRect);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function endObjectTransform() {
|
|
448
|
+
if (!isObjectTransformActive()) return;
|
|
449
|
+
const stateSnapshot = objectTransform;
|
|
450
|
+
objectTransform = null;
|
|
451
|
+
if (objectSelectedBox) {
|
|
452
|
+
objectSelectedBox.style.cursor = '';
|
|
453
|
+
renderObjectSelection();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const snapshotRect = stateSnapshot?.startRect;
|
|
457
|
+
const element = stateSnapshot?.element;
|
|
458
|
+
const finalRect = stateSnapshot?.nextRect || snapshotRect;
|
|
459
|
+
const hasMoved = Boolean(stateSnapshot?.hasMoved);
|
|
460
|
+
|
|
461
|
+
if (!hasMoved) return;
|
|
462
|
+
|
|
463
|
+
suppressNextObjectClick = true;
|
|
464
|
+
window.setTimeout(() => {
|
|
465
|
+
suppressNextObjectClick = false;
|
|
466
|
+
}, 0);
|
|
467
|
+
|
|
468
|
+
if (element && finalRect && snapshotRect) {
|
|
469
|
+
if (
|
|
470
|
+
finalRect.x !== snapshotRect.x ||
|
|
471
|
+
finalRect.y !== snapshotRect.y ||
|
|
472
|
+
finalRect.width !== snapshotRect.width ||
|
|
473
|
+
finalRect.height !== snapshotRect.height
|
|
474
|
+
) {
|
|
475
|
+
applyObjectGeometryToElement(element, finalRect);
|
|
476
|
+
commitObjectTransform({ beforeHtml: stateSnapshot?.startHtml || '' });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export function initObjectInteractionEvents() {
|
|
482
|
+
if (!objectLayer) return;
|
|
483
|
+
objectSelectedBox.addEventListener('mousedown', beginObjectTransform);
|
|
484
|
+
window.addEventListener('mousemove', continueObjectTransform);
|
|
485
|
+
window.addEventListener('mouseup', endObjectTransform);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export function renderObjectSelection() {
|
|
489
|
+
const selectedEl = state.toolMode === TOOL_MODE_SELECT ? getSelectedObjectElement() : null;
|
|
490
|
+
const hoveredEl = state.toolMode === TOOL_MODE_SELECT
|
|
491
|
+
? resolveXPath(slideIframe.contentDocument, state.hoveredObjectXPath)
|
|
492
|
+
: null;
|
|
493
|
+
|
|
494
|
+
const selectedRect = selectedEl ? elementToSlideRect(selectedEl) : null;
|
|
495
|
+
const hoveredRect = hoveredEl && hoveredEl !== selectedEl ? elementToSlideRect(hoveredEl) : null;
|
|
496
|
+
applyOverlayRect(objectSelectedBox, selectedRect);
|
|
497
|
+
applyOverlayRect(objectHoverBox, hoveredRect);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export function updateToolModeUI() {
|
|
501
|
+
const isDraw = state.toolMode === TOOL_MODE_DRAW;
|
|
502
|
+
slidePanel.classList.toggle('mode-draw', isDraw);
|
|
503
|
+
slidePanel.classList.toggle('mode-select', !isDraw);
|
|
504
|
+
if (objectLayer) {
|
|
505
|
+
objectLayer.style.pointerEvents = isDraw ? 'none' : 'auto';
|
|
506
|
+
}
|
|
507
|
+
toolModeDrawBtn.classList.toggle('active', isDraw);
|
|
508
|
+
toolModeSelectBtn.classList.toggle('active', !isDraw);
|
|
509
|
+
toolModeDrawBtn.setAttribute('aria-pressed', isDraw ? 'true' : 'false');
|
|
510
|
+
toolModeSelectBtn.setAttribute('aria-pressed', !isDraw ? 'true' : 'false');
|
|
511
|
+
editorHint.textContent = isDraw
|
|
512
|
+
? 'Drag on the slide to add red bboxes. Cmd/Ctrl+Enter to run.'
|
|
513
|
+
: 'Click an object to edit. \u2318B bold \u00b7 \u2318I italic \u00b7 \u2318U underline';
|
|
514
|
+
renderBboxes();
|
|
515
|
+
renderObjectSelection();
|
|
516
|
+
updateObjectEditorControls();
|
|
517
|
+
scaleSlide();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export function setToolMode(mode) {
|
|
521
|
+
state.toolMode = mode === TOOL_MODE_SELECT ? TOOL_MODE_SELECT : TOOL_MODE_DRAW;
|
|
522
|
+
state.drawing = false;
|
|
523
|
+
state.drawStart = null;
|
|
524
|
+
drawBox.style.display = 'none';
|
|
525
|
+
if (isObjectTransformActive()) {
|
|
526
|
+
endObjectTransform();
|
|
527
|
+
}
|
|
528
|
+
if (state.toolMode !== TOOL_MODE_SELECT) {
|
|
529
|
+
state.hoveredObjectXPath = '';
|
|
530
|
+
}
|
|
531
|
+
updateToolModeUI();
|
|
532
|
+
setStatus(state.toolMode === TOOL_MODE_SELECT ? 'Select mode enabled.' : 'BBox draw mode enabled.');
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export function setSelectedObjectXPath(xpath, statusMessage = 'Object selected.') {
|
|
536
|
+
const slide = currentSlideFile();
|
|
537
|
+
if (!slide) return;
|
|
538
|
+
const ss = getSlideState(slide);
|
|
539
|
+
ss.selectedObjectXPath = xpath || '';
|
|
540
|
+
state.hoveredObjectXPath = xpath || '';
|
|
541
|
+
renderObjectSelection();
|
|
542
|
+
updateObjectEditorControls();
|
|
543
|
+
if (statusMessage) {
|
|
544
|
+
setStatus(statusMessage);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export function updateHoveredObjectFromPointer(clientX, clientY) {
|
|
549
|
+
if (state.toolMode !== TOOL_MODE_SELECT) return;
|
|
550
|
+
const target = getSelectableTargetAt(clientX, clientY);
|
|
551
|
+
state.hoveredObjectXPath = target ? getXPath(target) : '';
|
|
552
|
+
renderObjectSelection();
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export function clearHoveredObject() {
|
|
556
|
+
state.hoveredObjectXPath = '';
|
|
557
|
+
renderObjectSelection();
|
|
558
|
+
}
|