@hyperframes/studio 0.7.28 → 0.7.30
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/dist/assets/{index-BGUJ2C2G.js → index-BhSyGx7y.js} +1 -1
- package/dist/assets/{index-Bz6Eqd_G.js → index-D0yNztV_.js} +1 -1
- package/dist/assets/{index-CLlPjdPl.js → index-kbACg3_I.js} +139 -139
- package/dist/{chunk-AN2EWWK3.js → chunk-JND3XUJL.js} +38 -10
- package/dist/chunk-JND3XUJL.js.map +1 -0
- package/dist/{domEditingLayers-EK7R7R4G.js → domEditingLayers-UIQZJCOA.js} +4 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.html +1 -1
- package/dist/index.js +1145 -675
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/editor/DomEditOverlay.test.ts +95 -0
- package/src/components/editor/DomEditOverlay.tsx +5 -3
- package/src/components/editor/domEditOverlayGestures.ts +3 -4
- package/src/components/editor/domEditing.ts +1 -0
- package/src/components/editor/domEditingLayers.test.ts +52 -0
- package/src/components/editor/domEditingLayers.ts +55 -19
- package/src/components/editor/domEditingTypes.ts +1 -0
- package/src/components/editor/persistSeam.integration.test.ts +264 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +15 -1
- package/src/components/editor/useDomEditOverlayGestures.ts +1 -0
- package/src/hooks/domEditCommitRunner.ts +46 -0
- package/src/hooks/domEditPersistFailure.test.ts +123 -0
- package/src/hooks/domEditPersistFailure.ts +89 -0
- package/src/hooks/domEditTextFieldCommitOps.test.ts +111 -0
- package/src/hooks/domEditTextFieldCommitOps.ts +63 -0
- package/src/hooks/domSelectionTestHarness.ts +40 -0
- package/src/hooks/useDomEditAttributeCommits.ts +227 -0
- package/src/hooks/useDomEditCommits.test.tsx +775 -0
- package/src/hooks/useDomEditCommits.ts +33 -5
- package/src/hooks/useDomEditTextCommits.ts +243 -220
- package/src/hooks/useDomSelection.test.ts +134 -0
- package/src/hooks/useDomSelection.ts +29 -15
- package/src/hooks/usePreviewInteraction.test.ts +260 -0
- package/src/hooks/usePreviewInteraction.ts +60 -19
- package/src/utils/sdkCutoverEligibility.test.ts +17 -0
- package/src/utils/sdkCutoverEligibility.ts +8 -2
- package/src/utils/sdkResolverShadow.test.ts +180 -1
- package/src/utils/sdkResolverShadow.ts +94 -1
- package/src/utils/sourcePatcher.ts +2 -0
- package/src/utils/studioPreviewHelpers.test.ts +95 -1
- package/src/utils/studioPreviewHelpers.ts +109 -10
- package/src/utils/studioSaveDiagnostics.ts +5 -2
- package/src/utils/studioTelemetry.ts +27 -20
- package/dist/chunk-AN2EWWK3.js.map +0 -1
- /package/dist/{domEditingLayers-EK7R7R4G.js.map → domEditingLayers-UIQZJCOA.js.map} +0 -0
|
@@ -10,6 +10,11 @@ import { fontFamilyFromAssetPath, type ImportedFontAsset } from "../components/e
|
|
|
10
10
|
import type { EditHistoryKind } from "../utils/editHistory";
|
|
11
11
|
import type { PersistDomEditOperations } from "./domEditCommitTypes";
|
|
12
12
|
import type { PatchOperation } from "../utils/sourcePatcher";
|
|
13
|
+
import {
|
|
14
|
+
DomEditPersistUnsafeValueError,
|
|
15
|
+
DomEditPersistUnresolvableError,
|
|
16
|
+
warnDomEditPersistNoOp,
|
|
17
|
+
} from "./domEditPersistFailure";
|
|
13
18
|
import { useDomEditPositionPatchCommit } from "./useDomEditPositionPatchCommit";
|
|
14
19
|
import { useDomEditTextCommits } from "./useDomEditTextCommits";
|
|
15
20
|
import { useDomGeometryCommits } from "./useDomGeometryCommits";
|
|
@@ -22,6 +27,10 @@ function formatUnsafeFieldList(fields: Array<{ path: string }>): string {
|
|
|
22
27
|
return fields.map((field) => field.path).join(", ");
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
function getErrorDetail(error: unknown): string {
|
|
31
|
+
return error instanceof Error ? error.message : String(error);
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
async function readErrorResponseBody(
|
|
26
35
|
response: Response,
|
|
27
36
|
): Promise<{ error?: string; fields?: string[] } | null> {
|
|
@@ -168,7 +177,9 @@ export function useDomEditCommits({
|
|
|
168
177
|
if (unsafeFields.length > 0) {
|
|
169
178
|
const fields = formatUnsafeFieldList(unsafeFields);
|
|
170
179
|
showToast("Couldn't save edit because it contains invalid layout values", "error");
|
|
171
|
-
throw new
|
|
180
|
+
throw new DomEditPersistUnsafeValueError(`DOM patch contains unsafe values: ${fields}`, {
|
|
181
|
+
alreadyToasted: true,
|
|
182
|
+
});
|
|
172
183
|
}
|
|
173
184
|
|
|
174
185
|
// Skip the SDK path when prepareContent is set (e.g. @font-face injection
|
|
@@ -203,7 +214,9 @@ export function useDomEditCommits({
|
|
|
203
214
|
);
|
|
204
215
|
if (!patchResponse.ok) {
|
|
205
216
|
showToast(formatPatchRejectionMessage(await readErrorResponseBody(patchResponse)), "error");
|
|
206
|
-
throw await createStudioSaveHttpError(patchResponse, `Failed to patch ${targetPath}
|
|
217
|
+
throw await createStudioSaveHttpError(patchResponse, `Failed to patch ${targetPath}`, {
|
|
218
|
+
alreadyToasted: true,
|
|
219
|
+
});
|
|
207
220
|
}
|
|
208
221
|
|
|
209
222
|
const patchData = (await patchResponse.json()) as {
|
|
@@ -225,7 +238,9 @@ export function useDomEditCommits({
|
|
|
225
238
|
composition: activeCompPath ?? undefined,
|
|
226
239
|
});
|
|
227
240
|
}
|
|
241
|
+
throw new DomEditPersistUnresolvableError(targetPath);
|
|
228
242
|
}
|
|
243
|
+
warnDomEditPersistNoOp(selection, operations);
|
|
229
244
|
return;
|
|
230
245
|
}
|
|
231
246
|
|
|
@@ -234,9 +249,21 @@ export function useDomEditCommits({
|
|
|
234
249
|
|
|
235
250
|
let finalContent = patchedContent;
|
|
236
251
|
if (options?.prepareContent) {
|
|
237
|
-
|
|
238
|
-
if (
|
|
239
|
-
|
|
252
|
+
const preparedContent = options.prepareContent(patchedContent, targetPath);
|
|
253
|
+
if (preparedContent !== patchedContent) {
|
|
254
|
+
try {
|
|
255
|
+
await writeProjectFile(targetPath, preparedContent);
|
|
256
|
+
finalContent = preparedContent;
|
|
257
|
+
} catch (error) {
|
|
258
|
+
// The patch above already landed on disk — only the prepareContent
|
|
259
|
+
// embellishment (e.g. an injected @font-face) failed to write. Keep
|
|
260
|
+
// the already-persisted patchedContent instead of throwing, which
|
|
261
|
+
// would otherwise revert a change the server already committed.
|
|
262
|
+
showToast(
|
|
263
|
+
`Saved, but couldn't finish updating ${targetPath}: ${getErrorDetail(error)}`,
|
|
264
|
+
"error",
|
|
265
|
+
);
|
|
266
|
+
}
|
|
240
267
|
}
|
|
241
268
|
}
|
|
242
269
|
|
|
@@ -286,6 +313,7 @@ export function useDomEditCommits({
|
|
|
286
313
|
buildDomSelectionFromTarget,
|
|
287
314
|
persistDomEditOperations,
|
|
288
315
|
resolveImportedFontAsset,
|
|
316
|
+
showToast,
|
|
289
317
|
});
|
|
290
318
|
|
|
291
319
|
// ── Position patch helper (shared by geometry + lifecycle hooks) ──
|
|
@@ -23,12 +23,24 @@ import {
|
|
|
23
23
|
} from "../components/editor/domEditing";
|
|
24
24
|
import type { ImportedFontAsset } from "../components/editor/fontAssets";
|
|
25
25
|
import type { PersistDomEditOperations } from "./domEditCommitTypes";
|
|
26
|
+
import { buildTextFieldChildOperations } from "./domEditTextFieldCommitOps";
|
|
27
|
+
import {
|
|
28
|
+
DomEditPersistUnsupportedTextStructureError,
|
|
29
|
+
reportDomEditPersistFailure,
|
|
30
|
+
} from "./domEditPersistFailure";
|
|
31
|
+
import {
|
|
32
|
+
bumpDomEditCommitMapVersion,
|
|
33
|
+
bumpDomEditCommitVersion,
|
|
34
|
+
runDomEditCommit,
|
|
35
|
+
} from "./domEditCommitRunner";
|
|
36
|
+
import { useDomEditAttributeCommits } from "./useDomEditAttributeCommits";
|
|
26
37
|
|
|
27
38
|
// ── Types ──
|
|
28
39
|
|
|
29
40
|
export interface UseDomEditTextCommitsParams {
|
|
30
41
|
activeCompPath: string | null;
|
|
31
42
|
previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
|
|
43
|
+
showToast: (message: string, tone?: "error" | "info") => void;
|
|
32
44
|
domEditSelection: DomEditSelection | null;
|
|
33
45
|
applyDomSelection: (
|
|
34
46
|
selection: DomEditSelection | null,
|
|
@@ -43,31 +55,78 @@ export interface UseDomEditTextCommitsParams {
|
|
|
43
55
|
resolveImportedFontAsset: (fontFamilyValue: string) => ImportedFontAsset | null;
|
|
44
56
|
}
|
|
45
57
|
|
|
46
|
-
|
|
58
|
+
interface DomTextCommitPlan {
|
|
59
|
+
usesSerializedTextFields: boolean;
|
|
60
|
+
nextContent: string;
|
|
61
|
+
childOperations: PatchOperation[] | null;
|
|
62
|
+
operations: PatchOperation[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildDomStyleCommitOperations(
|
|
66
|
+
property: string,
|
|
67
|
+
value: string,
|
|
68
|
+
isImageBackgroundCommit: boolean,
|
|
69
|
+
): PatchOperation[] {
|
|
70
|
+
const operations: PatchOperation[] = [
|
|
71
|
+
buildDomEditStylePatchOperation(property, normalizeDomEditStyleValue(property, value)),
|
|
72
|
+
];
|
|
73
|
+
if (isImageBackgroundCommit) {
|
|
74
|
+
operations.push(
|
|
75
|
+
buildDomEditStylePatchOperation("background-position", "center"),
|
|
76
|
+
buildDomEditStylePatchOperation("background-repeat", "no-repeat"),
|
|
77
|
+
buildDomEditStylePatchOperation("background-size", "contain"),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return operations;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildNextDomTextFields(
|
|
84
|
+
textFields: DomEditTextField[],
|
|
85
|
+
value: string,
|
|
86
|
+
fieldKey?: string,
|
|
87
|
+
): DomEditTextField[] {
|
|
88
|
+
if (textFields.length === 0) return [];
|
|
89
|
+
return textFields.map((field) => (field.key === fieldKey ? { ...field, value } : field));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function planDomTextCommit(
|
|
93
|
+
originalTextFields: DomEditTextField[],
|
|
94
|
+
nextTextFields: DomEditTextField[],
|
|
95
|
+
plainTextContent: string,
|
|
96
|
+
): DomTextCommitPlan {
|
|
97
|
+
const usesSerializedTextFields =
|
|
98
|
+
nextTextFields.length > 1 || nextTextFields.some((field) => field.source === "child");
|
|
99
|
+
const nextContent = usesSerializedTextFields
|
|
100
|
+
? serializeDomEditTextFields(nextTextFields)
|
|
101
|
+
: plainTextContent;
|
|
102
|
+
const childOperations = usesSerializedTextFields
|
|
103
|
+
? buildTextFieldChildOperations(originalTextFields, nextTextFields)
|
|
104
|
+
: null;
|
|
105
|
+
const operations =
|
|
106
|
+
childOperations ??
|
|
107
|
+
(usesSerializedTextFields ? [] : [buildDomEditTextPatchOperation(nextContent)]);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
usesSerializedTextFields,
|
|
111
|
+
nextContent,
|
|
112
|
+
childOperations,
|
|
113
|
+
operations,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function resyncDomTextSelectionFromPreview(
|
|
47
118
|
doc: Document | null | undefined,
|
|
48
119
|
selection: DomEditSelection,
|
|
49
120
|
activeCompPath: string | null,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
): void {
|
|
121
|
+
buildDomSelectionFromTarget: UseDomEditTextCommitsParams["buildDomSelectionFromTarget"],
|
|
122
|
+
applyDomSelection: UseDomEditTextCommitsParams["applyDomSelection"],
|
|
123
|
+
): Promise<void> {
|
|
54
124
|
if (!doc) return;
|
|
55
|
-
const
|
|
56
|
-
if (!
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
} else {
|
|
61
|
-
el.setAttribute(fullAttr, value);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
interface DataAttributeCommitOptions {
|
|
66
|
-
label: string;
|
|
67
|
-
coalescePrefix: string;
|
|
68
|
-
skipRefresh: boolean;
|
|
69
|
-
warningMessage: string;
|
|
70
|
-
refreshAfter?: boolean;
|
|
125
|
+
const refreshed = findElementForSelection(doc, selection, activeCompPath);
|
|
126
|
+
if (!refreshed) return;
|
|
127
|
+
const nextSelection = await buildDomSelectionFromTarget(refreshed);
|
|
128
|
+
if (!nextSelection) return;
|
|
129
|
+
applyDomSelection(nextSelection, { revealPanel: false, preserveGroup: true });
|
|
71
130
|
}
|
|
72
131
|
|
|
73
132
|
// ── Hook ──
|
|
@@ -75,6 +134,7 @@ interface DataAttributeCommitOptions {
|
|
|
75
134
|
export function useDomEditTextCommits({
|
|
76
135
|
activeCompPath,
|
|
77
136
|
previewIframeRef,
|
|
137
|
+
showToast,
|
|
78
138
|
domEditSelection,
|
|
79
139
|
applyDomSelection,
|
|
80
140
|
refreshDomEditSelectionFromPreview,
|
|
@@ -83,151 +143,91 @@ export function useDomEditTextCommits({
|
|
|
83
143
|
resolveImportedFontAsset,
|
|
84
144
|
}: UseDomEditTextCommitsParams) {
|
|
85
145
|
const domTextCommitVersionRef = useRef(0);
|
|
146
|
+
const domStyleCommitVersionRef = useRef(new Map<string, number>());
|
|
147
|
+
|
|
148
|
+
const { handleDomAttributeCommit, handleDomAttributeLiveCommit, handleDomHtmlAttributeCommit } =
|
|
149
|
+
useDomEditAttributeCommits({
|
|
150
|
+
activeCompPath,
|
|
151
|
+
previewIframeRef,
|
|
152
|
+
showToast,
|
|
153
|
+
domEditSelection,
|
|
154
|
+
refreshDomEditSelectionFromPreview,
|
|
155
|
+
persistDomEditOperations,
|
|
156
|
+
});
|
|
86
157
|
|
|
87
158
|
const handleDomStyleCommit = useCallback(
|
|
88
159
|
async (property: string, value: string) => {
|
|
89
160
|
if (!domEditSelection) return;
|
|
90
161
|
if (isManualGeometryStyleProperty(property)) return;
|
|
91
162
|
if (!domEditSelection.capabilities.canEditStyles) return;
|
|
163
|
+
const styleCommitKey = `${getDomEditTargetKey(domEditSelection)}:${property}`;
|
|
164
|
+
const isLatestStyleCommit = bumpDomEditCommitMapVersion(
|
|
165
|
+
domStyleCommitVersionRef.current,
|
|
166
|
+
styleCommitKey,
|
|
167
|
+
);
|
|
92
168
|
const importedFont = property === "font-family" ? resolveImportedFontAsset(value) : null;
|
|
93
169
|
const iframe = previewIframeRef.current;
|
|
94
170
|
const doc = iframe?.contentDocument;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
171
|
+
const normalizedValue = normalizeDomEditStyleValue(property, value);
|
|
172
|
+
const isImageBackgroundCommit =
|
|
173
|
+
property === "background-image" && isImageBackgroundValue(value);
|
|
174
|
+
let editedElement: HTMLElement | null = null;
|
|
175
|
+
let previousInlineValue: string | null = null;
|
|
176
|
+
const operations = buildDomStyleCommitOperations(property, value, isImageBackgroundCommit);
|
|
177
|
+
const skipRefresh = property !== "z-index";
|
|
178
|
+
|
|
179
|
+
await runDomEditCommit({
|
|
180
|
+
capture: () => {
|
|
181
|
+
if (!doc) return;
|
|
182
|
+
const el = findElementForSelection(doc, domEditSelection, activeCompPath);
|
|
183
|
+
if (!el) return;
|
|
184
|
+
editedElement = el;
|
|
185
|
+
previousInlineValue = el.style.getPropertyValue(property);
|
|
186
|
+
},
|
|
187
|
+
apply: () => {
|
|
188
|
+
if (!editedElement) return;
|
|
189
|
+
editedElement.style.setProperty(property, normalizedValue);
|
|
190
|
+
if (property === "font-family" && doc) {
|
|
100
191
|
injectPreviewGoogleFont(doc, value);
|
|
101
192
|
if (importedFont) injectPreviewImportedFont(doc, importedFont);
|
|
102
193
|
}
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
194
|
+
if (isImageBackgroundCommit) {
|
|
195
|
+
editedElement.style.setProperty("background-position", "center");
|
|
196
|
+
editedElement.style.setProperty("background-repeat", "no-repeat");
|
|
197
|
+
editedElement.style.setProperty("background-size", "contain");
|
|
107
198
|
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
const operations: PatchOperation[] = [
|
|
111
|
-
buildDomEditStylePatchOperation(property, normalizeDomEditStyleValue(property, value)),
|
|
112
|
-
];
|
|
113
|
-
if (property === "background-image" && isImageBackgroundValue(value)) {
|
|
114
|
-
operations.push(
|
|
115
|
-
buildDomEditStylePatchOperation("background-position", "center"),
|
|
116
|
-
buildDomEditStylePatchOperation("background-repeat", "no-repeat"),
|
|
117
|
-
buildDomEditStylePatchOperation("background-size", "contain"),
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
const skipRefresh = property !== "z-index";
|
|
121
|
-
try {
|
|
122
|
-
await persistDomEditOperations(domEditSelection, operations, {
|
|
123
|
-
label: "Edit layer style",
|
|
124
|
-
skipRefresh,
|
|
125
|
-
prepareContent: importedFont
|
|
126
|
-
? (html, sourceFile) => ensureImportedFontFace(html, importedFont, sourceFile)
|
|
127
|
-
: undefined,
|
|
128
|
-
});
|
|
129
|
-
} catch {}
|
|
130
|
-
refreshDomEditSelectionFromPreview(domEditSelection);
|
|
131
|
-
},
|
|
132
|
-
[
|
|
133
|
-
activeCompPath,
|
|
134
|
-
domEditSelection,
|
|
135
|
-
persistDomEditOperations,
|
|
136
|
-
refreshDomEditSelectionFromPreview,
|
|
137
|
-
resolveImportedFontAsset,
|
|
138
|
-
previewIframeRef,
|
|
139
|
-
],
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
const commitDataAttribute = useCallback(
|
|
143
|
-
async (attr: string, value: string | null, options: DataAttributeCommitOptions) => {
|
|
144
|
-
if (!domEditSelection) return;
|
|
145
|
-
const iframe = previewIframeRef.current;
|
|
146
|
-
applyPreviewAttribute(
|
|
147
|
-
iframe?.contentDocument,
|
|
148
|
-
domEditSelection,
|
|
149
|
-
activeCompPath,
|
|
150
|
-
attr,
|
|
151
|
-
value,
|
|
152
|
-
{
|
|
153
|
-
prefixData: true,
|
|
154
199
|
},
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
refreshDomEditSelectionFromPreview,
|
|
173
|
-
previewIframeRef,
|
|
174
|
-
],
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
const handleDomAttributeCommit = useCallback(
|
|
178
|
-
async (attr: string, value: string) => {
|
|
179
|
-
await commitDataAttribute(attr, value, {
|
|
180
|
-
label: `Edit ${attr.replace(/-/g, " ")}`,
|
|
181
|
-
coalescePrefix: "attr",
|
|
182
|
-
skipRefresh: false,
|
|
183
|
-
warningMessage: "[Studio] Attribute persist failed:",
|
|
184
|
-
refreshAfter: true,
|
|
185
|
-
});
|
|
186
|
-
},
|
|
187
|
-
[commitDataAttribute],
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
const handleDomAttributeLiveCommit = useCallback(
|
|
191
|
-
async (attr: string, value: string | null) => {
|
|
192
|
-
await commitDataAttribute(attr, value, {
|
|
193
|
-
label: `Edit ${attr.replace(/^(data-)?/, "").replace(/-/g, " ")}`,
|
|
194
|
-
coalescePrefix: "attr-live",
|
|
195
|
-
skipRefresh: true,
|
|
196
|
-
warningMessage: "[Studio] Live attribute persist failed:",
|
|
197
|
-
});
|
|
198
|
-
},
|
|
199
|
-
[commitDataAttribute],
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
const handleDomHtmlAttributeCommit = useCallback(
|
|
203
|
-
async (attr: string, value: string | null) => {
|
|
204
|
-
if (!domEditSelection) return;
|
|
205
|
-
const iframe = previewIframeRef.current;
|
|
206
|
-
applyPreviewAttribute(
|
|
207
|
-
iframe?.contentDocument,
|
|
208
|
-
domEditSelection,
|
|
209
|
-
activeCompPath,
|
|
210
|
-
attr,
|
|
211
|
-
value,
|
|
212
|
-
{
|
|
213
|
-
removeFalse: true,
|
|
200
|
+
persist: () =>
|
|
201
|
+
persistDomEditOperations(domEditSelection, operations, {
|
|
202
|
+
label: "Edit layer style",
|
|
203
|
+
skipRefresh,
|
|
204
|
+
prepareContent: importedFont
|
|
205
|
+
? (html, sourceFile) => ensureImportedFontFace(html, importedFont, sourceFile)
|
|
206
|
+
: undefined,
|
|
207
|
+
}),
|
|
208
|
+
shouldRevert: () => isLatestStyleCommit(),
|
|
209
|
+
revert: () => {
|
|
210
|
+
if (!editedElement || previousInlineValue === null) return;
|
|
211
|
+
// ponytail: background-image side-effect styles are not reverted here.
|
|
212
|
+
if (previousInlineValue === "") {
|
|
213
|
+
editedElement.style.removeProperty(property);
|
|
214
|
+
} else {
|
|
215
|
+
editedElement.style.setProperty(property, previousInlineValue);
|
|
216
|
+
}
|
|
214
217
|
},
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
coalesceKey: `html-attr:${attr}:${getDomEditTargetKey(domEditSelection)}`,
|
|
221
|
-
skipRefresh: false,
|
|
222
|
-
});
|
|
223
|
-
} catch {}
|
|
224
|
-
refreshDomEditSelectionFromPreview(domEditSelection);
|
|
218
|
+
onError: (error) =>
|
|
219
|
+
reportDomEditPersistFailure(domEditSelection, operations, error, showToast),
|
|
220
|
+
shouldResync: isLatestStyleCommit,
|
|
221
|
+
resync: () => refreshDomEditSelectionFromPreview(domEditSelection),
|
|
222
|
+
});
|
|
225
223
|
},
|
|
226
224
|
[
|
|
227
225
|
activeCompPath,
|
|
228
226
|
domEditSelection,
|
|
229
227
|
persistDomEditOperations,
|
|
230
228
|
refreshDomEditSelectionFromPreview,
|
|
229
|
+
resolveImportedFontAsset,
|
|
230
|
+
showToast,
|
|
231
231
|
previewIframeRef,
|
|
232
232
|
],
|
|
233
233
|
);
|
|
@@ -236,53 +236,57 @@ export function useDomEditTextCommits({
|
|
|
236
236
|
async (value: string, fieldKey?: string) => {
|
|
237
237
|
if (!domEditSelection) return;
|
|
238
238
|
if (!isTextEditableSelection(domEditSelection)) return;
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
const nextTextFields
|
|
242
|
-
domEditSelection.textFields.length > 0
|
|
243
|
-
? domEditSelection.textFields.map((field) =>
|
|
244
|
-
field.key === fieldKey ? { ...field, value } : field,
|
|
245
|
-
)
|
|
246
|
-
: [];
|
|
247
|
-
const nextContent =
|
|
248
|
-
nextTextFields.length > 1 || nextTextFields.some((field) => field.source === "child")
|
|
249
|
-
? serializeDomEditTextFields(nextTextFields)
|
|
250
|
-
: value;
|
|
239
|
+
const isLatestTextCommit = bumpDomEditCommitVersion(domTextCommitVersionRef);
|
|
240
|
+
const nextTextFields = buildNextDomTextFields(domEditSelection.textFields, value, fieldKey);
|
|
241
|
+
const textCommit = planDomTextCommit(domEditSelection.textFields, nextTextFields, value);
|
|
251
242
|
const iframe = previewIframeRef.current;
|
|
252
243
|
const doc = iframe?.contentDocument;
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
)
|
|
260
|
-
|
|
244
|
+
let editedElement: HTMLElement | null = null;
|
|
245
|
+
let previousInnerHtml: string | null = null;
|
|
246
|
+
|
|
247
|
+
await runDomEditCommit({
|
|
248
|
+
capture: () => {
|
|
249
|
+
if (!doc) return;
|
|
250
|
+
const el = findElementForSelection(doc, domEditSelection, activeCompPath);
|
|
251
|
+
if (!el) return;
|
|
252
|
+
editedElement = el;
|
|
253
|
+
previousInnerHtml = el.innerHTML;
|
|
254
|
+
},
|
|
255
|
+
apply: () => {
|
|
256
|
+
if (!editedElement) return;
|
|
257
|
+
if (textCommit.usesSerializedTextFields) {
|
|
258
|
+
editedElement.innerHTML = textCommit.nextContent;
|
|
261
259
|
} else {
|
|
262
|
-
|
|
260
|
+
editedElement.textContent = value;
|
|
263
261
|
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
await persistDomEditOperations(
|
|
267
|
-
domEditSelection,
|
|
268
|
-
[buildDomEditTextPatchOperation(nextContent)],
|
|
269
|
-
{
|
|
270
|
-
label: "Edit text",
|
|
271
|
-
skipRefresh: true,
|
|
272
|
-
shouldSave: () => domTextCommitVersionRef.current === commitVersion,
|
|
273
262
|
},
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (doc) {
|
|
278
|
-
const refreshed = findElementForSelection(doc, domEditSelection, activeCompPath);
|
|
279
|
-
if (refreshed) {
|
|
280
|
-
const nextSelection = await buildDomSelectionFromTarget(refreshed);
|
|
281
|
-
if (nextSelection) {
|
|
282
|
-
applyDomSelection(nextSelection, { revealPanel: false, preserveGroup: true });
|
|
263
|
+
persist: async () => {
|
|
264
|
+
if (textCommit.usesSerializedTextFields && textCommit.childOperations === null) {
|
|
265
|
+
throw new DomEditPersistUnsupportedTextStructureError();
|
|
283
266
|
}
|
|
284
|
-
|
|
285
|
-
|
|
267
|
+
await persistDomEditOperations(domEditSelection, textCommit.operations, {
|
|
268
|
+
label: "Edit text",
|
|
269
|
+
skipRefresh: true,
|
|
270
|
+
shouldSave: isLatestTextCommit,
|
|
271
|
+
});
|
|
272
|
+
},
|
|
273
|
+
shouldRevert: () => isLatestTextCommit(),
|
|
274
|
+
revert: () => {
|
|
275
|
+
if (!editedElement || previousInnerHtml === null) return;
|
|
276
|
+
editedElement.innerHTML = previousInnerHtml;
|
|
277
|
+
},
|
|
278
|
+
onError: (error) =>
|
|
279
|
+
reportDomEditPersistFailure(domEditSelection, textCommit.operations, error, showToast),
|
|
280
|
+
shouldResync: isLatestTextCommit,
|
|
281
|
+
resync: () =>
|
|
282
|
+
resyncDomTextSelectionFromPreview(
|
|
283
|
+
doc,
|
|
284
|
+
domEditSelection,
|
|
285
|
+
activeCompPath,
|
|
286
|
+
buildDomSelectionFromTarget,
|
|
287
|
+
applyDomSelection,
|
|
288
|
+
),
|
|
289
|
+
});
|
|
286
290
|
},
|
|
287
291
|
[
|
|
288
292
|
activeCompPath,
|
|
@@ -291,6 +295,7 @@ export function useDomEditTextCommits({
|
|
|
291
295
|
domEditSelection,
|
|
292
296
|
persistDomEditOperations,
|
|
293
297
|
previewIframeRef,
|
|
298
|
+
showToast,
|
|
294
299
|
],
|
|
295
300
|
);
|
|
296
301
|
|
|
@@ -300,45 +305,62 @@ export function useDomEditTextCommits({
|
|
|
300
305
|
nextTextFields: DomEditTextField[],
|
|
301
306
|
options?: { importedFont?: ImportedFontAsset | null },
|
|
302
307
|
) => {
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
+
const textCommit = planDomTextCommit(
|
|
309
|
+
selection.textFields,
|
|
310
|
+
nextTextFields,
|
|
311
|
+
nextTextFields[0]?.value ?? "",
|
|
312
|
+
);
|
|
308
313
|
const iframe = previewIframeRef.current;
|
|
309
314
|
const doc = iframe?.contentDocument;
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (el) {
|
|
313
|
-
if (
|
|
314
|
-
nextTextFields.length > 1 ||
|
|
315
|
-
nextTextFields.some((field) => field.source === "child")
|
|
316
|
-
) {
|
|
317
|
-
el.innerHTML = nextContent;
|
|
318
|
-
} else {
|
|
319
|
-
el.textContent = nextContent;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
315
|
+
let editedElement: HTMLElement | null = null;
|
|
316
|
+
let previousInnerHtml: string | null = null;
|
|
324
317
|
const importedFont = options?.importedFont ?? null;
|
|
325
|
-
await persistDomEditOperations(selection, [buildDomEditTextPatchOperation(nextContent)], {
|
|
326
|
-
label: "Edit text",
|
|
327
|
-
skipRefresh: true,
|
|
328
|
-
prepareContent: importedFont
|
|
329
|
-
? (html, sourceFile) => ensureImportedFontFace(html, importedFont, sourceFile)
|
|
330
|
-
: undefined,
|
|
331
|
-
});
|
|
332
318
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
if (
|
|
338
|
-
|
|
319
|
+
await runDomEditCommit({
|
|
320
|
+
capture: () => {
|
|
321
|
+
if (!doc) return;
|
|
322
|
+
const el = findElementForSelection(doc, selection, activeCompPath);
|
|
323
|
+
if (!el) return;
|
|
324
|
+
editedElement = el;
|
|
325
|
+
previousInnerHtml = el.innerHTML;
|
|
326
|
+
},
|
|
327
|
+
apply: () => {
|
|
328
|
+
if (!editedElement) return;
|
|
329
|
+
if (textCommit.usesSerializedTextFields) {
|
|
330
|
+
editedElement.innerHTML = textCommit.nextContent;
|
|
331
|
+
} else {
|
|
332
|
+
editedElement.textContent = textCommit.nextContent;
|
|
339
333
|
}
|
|
340
|
-
}
|
|
341
|
-
|
|
334
|
+
},
|
|
335
|
+
persist: async () => {
|
|
336
|
+
if (textCommit.usesSerializedTextFields && textCommit.childOperations === null) {
|
|
337
|
+
throw new DomEditPersistUnsupportedTextStructureError();
|
|
338
|
+
}
|
|
339
|
+
await persistDomEditOperations(selection, textCommit.operations, {
|
|
340
|
+
label: "Edit text",
|
|
341
|
+
skipRefresh: true,
|
|
342
|
+
prepareContent: importedFont
|
|
343
|
+
? (html, sourceFile) => ensureImportedFontFace(html, importedFont, sourceFile)
|
|
344
|
+
: undefined,
|
|
345
|
+
});
|
|
346
|
+
},
|
|
347
|
+
shouldRevert: () => true,
|
|
348
|
+
revert: () => {
|
|
349
|
+
if (!editedElement || previousInnerHtml === null) return;
|
|
350
|
+
editedElement.innerHTML = previousInnerHtml;
|
|
351
|
+
},
|
|
352
|
+
onError: (error) =>
|
|
353
|
+
reportDomEditPersistFailure(selection, textCommit.operations, error, showToast),
|
|
354
|
+
shouldResync: () => true,
|
|
355
|
+
resync: () =>
|
|
356
|
+
resyncDomTextSelectionFromPreview(
|
|
357
|
+
doc,
|
|
358
|
+
selection,
|
|
359
|
+
activeCompPath,
|
|
360
|
+
buildDomSelectionFromTarget,
|
|
361
|
+
applyDomSelection,
|
|
362
|
+
),
|
|
363
|
+
});
|
|
342
364
|
},
|
|
343
365
|
[
|
|
344
366
|
activeCompPath,
|
|
@@ -346,6 +368,7 @@ export function useDomEditTextCommits({
|
|
|
346
368
|
buildDomSelectionFromTarget,
|
|
347
369
|
persistDomEditOperations,
|
|
348
370
|
previewIframeRef,
|
|
371
|
+
showToast,
|
|
349
372
|
],
|
|
350
373
|
);
|
|
351
374
|
|