@designcrowd/library.brand-page 5.18.1-undo-26 → 5.18.1-undo-28
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/lib/src/{CreatedOnBrandCrowd-de-de-DWzLegt3.js → CreatedOnBrandCrowd-de-de-CmaFdOwA.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-Dr6isTH_.js → CreatedOnBrandCrowd-en-us-DYcx1zJ5.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-hPCTaL-A.js → CreatedOnBrandCrowd-es-es-BW-OyQM7.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-BNDUTNc_.js → CreatedOnBrandCrowd-fr-ca-DDBKibOe.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-BNDUTNc_.js → CreatedOnBrandCrowd-fr-fr-DDBKibOe.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-DKn5U0m5.js → CreatedOnBrandCrowd-pt-br-6OSkxFgf.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-DKn5U0m5.js → CreatedOnBrandCrowd-pt-pt-6OSkxFgf.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-Cxe0RSmZ.js → CreatedOnDcom-de-de-Ds3pSY3b.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-D1D6h8b6.js → CreatedOnDcom-en-us-nu_xhm3W.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-4k5Jz225.js → CreatedOnDcom-es-es-DeWd8xLJ.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-CeB-CL-1.js → CreatedOnDcom-fr-ca-C30UWAc7.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-D7PCXxYg.js → CreatedOnDcom-fr-fr-ZFAi67Ii.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-D89FAdoN.js → CreatedOnDcom-pt-br-Bvtz3TFZ.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-CtJ0lm4O.js → CreatedOnDcom-pt-pt-Dr__mA4b.js} +1 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +13 -1
- package/lib/src/brand-page-maker/stores/history/history-store.d.ts +4 -1
- package/lib/src/brand-page-maker/stores/history/history-store.js +90 -393
- package/lib/src/brand-page-maker/stores/user/user-store.js +1 -1
- package/lib/src/index.mjs +1 -1
- package/lib/src/{lib-D_xnR4VH.js → lib-BBItimSg.js} +18133 -18249
- package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +64 -0
- package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.js +12 -0
- package/package.json +1 -1
- package/src/brand-page-maker/components/quick-link-bar/undo-redo-buttons.test.ts +116 -0
- package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +15 -2
- package/src/brand-page-maker/stores/history/history-store.test.ts +152 -790
- package/src/brand-page-maker/stores/history/history-store.ts +95 -444
- package/src/brand-page-maker/stores/user/user-store.ts +1 -1
- package/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.vue +7 -0
- package/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.ts +13 -0
- package/src/brand-page-maker/components/quick-link-bar/RedoButton.test.ts +0 -166
- package/src/brand-page-maker/components/quick-link-bar/UndoButton.test.ts +0 -166
|
@@ -7,7 +7,6 @@ import { usePageNavigationStore } from '@shared/page-navigation/store/page-navig
|
|
|
7
7
|
import { useUiEditableLogoStore } from '@shared/logo-settings/store/editable-logo-store';
|
|
8
8
|
import { useShopConfigStore } from '@shared/ecommerce-settings/store/shop-settings-store';
|
|
9
9
|
import { useUiPageSettingsStore } from '@shared/page-settings/store';
|
|
10
|
-
import { ESCAPED_NEWLINE_CHARACTER } from '@src/constants/ui';
|
|
11
10
|
/**
|
|
12
11
|
* Single-stack history model with a pointer instead of two stacks.
|
|
13
12
|
*
|
|
@@ -52,52 +51,20 @@ const MAX_ENTRY_BYTES = 25 * 1024 * 1024;
|
|
|
52
51
|
const BASELINE_SETTLE_MS = 2000;
|
|
53
52
|
// Content-level wrapper keys whose `paymentConfig` child is a denormalised
|
|
54
53
|
// cache of `shopConfigStore.paymentConfig` (the user's Stripe Connect
|
|
55
|
-
// linkage).
|
|
56
|
-
// `
|
|
57
|
-
// these sections — that hydration is not user authoring intent, and Stripe
|
|
58
|
-
// Connect linkage state is backend-confirmed (per `project-guardrails.md`).
|
|
54
|
+
// linkage). Stripe Connect linkage state is backend-confirmed (per
|
|
55
|
+
// `project-guardrails.md`) and must NEVER ride along on the undo/redo stack.
|
|
59
56
|
//
|
|
60
|
-
// paymentConfig is kept
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// `shopConfigStore.paymentConfig`, which is itself never touched by
|
|
67
|
-
// undo/redo (see `applyHistorySnapshot` step 6).
|
|
68
|
-
//
|
|
69
|
-
// Together: the live wrapper always equals the live shop store, regardless
|
|
70
|
-
// of what the user does in between captures and restores.
|
|
57
|
+
// paymentConfig is kept invariant under undo/redo by re-stamping every live
|
|
58
|
+
// wrapper from `shopConfigStore.paymentConfig` after every restore — see
|
|
59
|
+
// `restorePaymentConfigsOnLivePages`. The shop store itself is never
|
|
60
|
+
// touched by undo/redo (see `applyHistorySnapshot` step 6), so the live
|
|
61
|
+
// wrapper always equals the live shop store regardless of what's in the
|
|
62
|
+
// snapshot bytes.
|
|
71
63
|
const PAYMENT_CONFIG_WRAPPER_KEYS = [
|
|
72
64
|
'supportMeConfig',
|
|
73
65
|
'requestPaymentConfig',
|
|
74
66
|
'shopPaymentConfig',
|
|
75
67
|
];
|
|
76
|
-
/**
|
|
77
|
-
* Walk a `pages` array and remove the `paymentConfig` key from every
|
|
78
|
-
* payment-config wrapper found inside `pages[*].contents[*]`. Mutates in
|
|
79
|
-
* place — caller is responsible for passing a clone if the source must
|
|
80
|
-
* remain untouched.
|
|
81
|
-
*/
|
|
82
|
-
function stripPaymentConfigsFromPages(pages) {
|
|
83
|
-
if (!Array.isArray(pages))
|
|
84
|
-
return;
|
|
85
|
-
for (const page of pages) {
|
|
86
|
-
const contents = page?.contents;
|
|
87
|
-
if (!Array.isArray(contents))
|
|
88
|
-
continue;
|
|
89
|
-
for (const content of contents) {
|
|
90
|
-
if (!content || typeof content !== 'object')
|
|
91
|
-
continue;
|
|
92
|
-
for (const key of PAYMENT_CONFIG_WRAPPER_KEYS) {
|
|
93
|
-
const wrapper = content[key];
|
|
94
|
-
if (wrapper && typeof wrapper === 'object' && !Array.isArray(wrapper)) {
|
|
95
|
-
delete wrapper.paymentConfig;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
68
|
export const useHistoryStore = defineStore('history', () => {
|
|
102
69
|
const eventTracker = useEventTracker();
|
|
103
70
|
// --- State ---
|
|
@@ -131,15 +98,7 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
131
98
|
if (!cachedMakerStore)
|
|
132
99
|
return null;
|
|
133
100
|
try {
|
|
134
|
-
|
|
135
|
-
// `paymentConfig` from every payment-config wrapper before re-
|
|
136
|
-
// stringifying. paymentConfig is the user's Stripe Connect linkage
|
|
137
|
-
// (backend-confirmed) and must not enter the undo stack — see
|
|
138
|
-
// `PAYMENT_CONFIG_WRAPPER_KEYS` above. The live store is never
|
|
139
|
-
// mutated; the strip operates on the detached clone only.
|
|
140
|
-
const cloned = JSON.parse(JSON.stringify(toRaw(cachedMakerStore.brandPageData)));
|
|
141
|
-
stripPaymentConfigsFromPages(cloned.pages);
|
|
142
|
-
return JSON.stringify(cloned);
|
|
101
|
+
return JSON.stringify(toRaw(cachedMakerStore.brandPageData));
|
|
143
102
|
}
|
|
144
103
|
catch (error) {
|
|
145
104
|
console.error('history-store: failed to serialize snapshot', error);
|
|
@@ -170,334 +129,31 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
170
129
|
}
|
|
171
130
|
}
|
|
172
131
|
/**
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* user-visible edits — without this, opening a content panel produces an
|
|
176
|
-
* undo step that "does nothing" when replayed.
|
|
177
|
-
*
|
|
178
|
-
* Known phantom sources (BP-5506):
|
|
179
|
-
* - TipTap RTE writing back its empty-paragraph default on first mount
|
|
180
|
-
* (e.g. `""` → `<p style="text-align: center;"></p>`).
|
|
181
|
-
* - TipTap RTE writing back its default-style hydration on first mount
|
|
182
|
-
* (e.g. plain `"Hello"` → `<p style="..."><span style="..."><mark ...>Hello</mark></span></p>`).
|
|
183
|
-
* - TipTap re-emitting whole-content `<strong>`/`<em>`/`<u>`/`<s>` tags
|
|
184
|
-
* to mirror the resolved `textStyle.fontWeight / fontStyle /
|
|
185
|
-
* textDecoration` (e.g. plain `"text"` →
|
|
186
|
-
* `<p>...<span>...<strong>text</strong></span></p>`).
|
|
187
|
-
* - TipTap rendering the codebase's URL-encoded newline
|
|
188
|
-
* (`ESCAPED_NEWLINE_CHARACTER` = `%0A`) as `<br>` on first hydration
|
|
189
|
-
* (e.g. plain `"A%0AB"` → `<p><span>A<br>B</span></p>`). Plain-side
|
|
190
|
-
* `%0A` is decoded to `\n` and attributeless `<br>` is collapsed to
|
|
191
|
-
* `\n` in `extractCanonicalText`, so the two sides converge.
|
|
192
|
-
* - Lazy hydration of `*Style` objects when a content settings panel —
|
|
193
|
-
* or section selection on the canvas — resolves `textStyleType` and
|
|
194
|
-
* writes the full resolved style (fontFamily, fontSize, color,
|
|
195
|
-
* fontWeight, fontStyle, letterSpacing, lineHeight, textDecoration)
|
|
196
|
-
* onto the model. The `textStyleType` itself is the authoring
|
|
197
|
-
* intent; the resolved keys are just cached values.
|
|
198
|
-
* - `isModifiedAfterTemplateApplied` flipping on first edit gesture.
|
|
132
|
+
* One-shot "fold next mutation back into the baseline (or current entry)
|
|
133
|
+
* rather than pushing a new entry" flag.
|
|
199
134
|
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* happened; stripping it would dedup real logo edits as no-ops. The
|
|
207
|
-
* 2-second `BASELINE_SETTLE_MS` window already collapses initial-load
|
|
208
|
-
* base64 churn into the baseline.
|
|
135
|
+
* Hydration write sites (TipTap mount, lazy paymentConfig fill,
|
|
136
|
+
* stale-page-ref clear) all run from a lifecycle hook on first sidebar
|
|
137
|
+
* open / section click. Each site mutates `brandPageData` exactly once
|
|
138
|
+
* with a value the user can't see as a change — but the deep watcher
|
|
139
|
+
* fires anyway, and without this gate the next idle flush would land a
|
|
140
|
+
* phantom undo entry.
|
|
209
141
|
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
142
|
+
* Each call site sets the flag immediately before the imperative write
|
|
143
|
+
* it knows is a hydration. The watcher's flush consumes the flag (clears
|
|
144
|
+
* it) and merges the resulting snapshot into `entries[currentIndex]`
|
|
145
|
+
* instead of pushing. No TTL — the flag stays armed until either:
|
|
146
|
+
* - the watcher fires and consumes it, or
|
|
147
|
+
* - a snapshot apply (undo/redo) clears it (we don't want a hydration
|
|
148
|
+
* flag set immediately before an apply to swallow the post-apply
|
|
149
|
+
* watcher tick), or
|
|
150
|
+
* - `reset()` clears it on save.
|
|
217
151
|
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
152
|
+
* Multiple call sites in the same burst all set the same flag — they
|
|
153
|
+
* collapse together into a single merge into the current head, which is
|
|
154
|
+
* the semantics we want.
|
|
220
155
|
*/
|
|
221
|
-
|
|
222
|
-
// hydration. None carry user authoring intent on their own when they are
|
|
223
|
-
// the sole element-child wrapping the entire content:
|
|
224
|
-
// - <p>: only its `text-align` style is meaningful, and that's already
|
|
225
|
-
// captured in `textStyle.textAlign`.
|
|
226
|
-
// - <span>: TipTap wraps content in a span with `font-family`, `font-size`,
|
|
227
|
-
// `color`, `letter-spacing`, `line-height` resolved from the
|
|
228
|
-
// textStyle. The textStyle object on the same content is the
|
|
229
|
-
// authoritative source.
|
|
230
|
-
// - <mark>: TipTap's highlight extension default — collapses regardless
|
|
231
|
-
// of `data-color` / `style` attrs, mirroring the original
|
|
232
|
-
// regex's behavior on a hydrated highlight.
|
|
233
|
-
// - <strong>/<em>/<u>/<s>: attributeless tags that mirror
|
|
234
|
-
// `fontWeight: bold` / `fontStyle: italic` / `textDecoration:
|
|
235
|
-
// underline / line-through`. ONLY recognized as hydration when
|
|
236
|
-
// they are the sole child of their parent — partial-content
|
|
237
|
-
// usage like `Hello <strong>world</strong>` is a real edit.
|
|
238
|
-
// Anything else (`<a>`, `<br>`, `<span style="color: red">`, attributes on
|
|
239
|
-
// <strong>/<em>, etc.) is preserved as a real diff.
|
|
240
|
-
const STRUCTURAL_HYDRATION_TAGS = new Set(['P', 'SPAN', 'MARK']);
|
|
241
|
-
const FORMAT_HYDRATION_TAGS = new Set(['STRONG', 'EM', 'U', 'S']);
|
|
242
|
-
// Default style properties TipTap emits on a hydration <span> /
|
|
243
|
-
// <mark>. The textStyle resolver writes all of these onto the live
|
|
244
|
-
// model, so when TipTap re-emits them as inline style on its
|
|
245
|
-
// hydration span/mark there is no user-visible delta.
|
|
246
|
-
// Anything outside this whitelist (e.g. `text-decoration` mid-content,
|
|
247
|
-
// `text-shadow`, `text-transform`) is treated as a real edit.
|
|
248
|
-
const HYDRATION_INLINE_STYLE_PROPS = new Set([
|
|
249
|
-
'font-family',
|
|
250
|
-
'font-size',
|
|
251
|
-
'letter-spacing',
|
|
252
|
-
'line-height',
|
|
253
|
-
'color',
|
|
254
|
-
'background-color',
|
|
255
|
-
]);
|
|
256
|
-
// Empty paragraph forms TipTap emits when an editor mounts on an empty
|
|
257
|
-
// string. Treated as equivalent to `""` for dedup.
|
|
258
|
-
function isEmptyRteHtml(value) {
|
|
259
|
-
if (typeof value !== 'string')
|
|
260
|
-
return false;
|
|
261
|
-
return /^<p(\s[^>]*)?>(<br\s*\/?>)?<\/p>$/.test(value);
|
|
262
|
-
}
|
|
263
|
-
function isHydrationOnlyAttributes(el) {
|
|
264
|
-
// Format tags (<strong>/<em>/<u>/<s>) must be attributeless. Anything
|
|
265
|
-
// with class/style/data-*/etc. is a real edit.
|
|
266
|
-
if (FORMAT_HYDRATION_TAGS.has(el.tagName))
|
|
267
|
-
return el.attributes.length === 0;
|
|
268
|
-
for (const attr of Array.from(el.attributes)) {
|
|
269
|
-
// <mark> may carry a `data-color` attr (TipTap highlight extension's
|
|
270
|
-
// default-and-resolved value). The accompanying `style` is checked
|
|
271
|
-
// against the same hydration-prop whitelist as <span>.
|
|
272
|
-
if (el.tagName === 'MARK' &&
|
|
273
|
-
(attr.name === 'data-color' || attr.name === 'style')) {
|
|
274
|
-
if (attr.name === 'data-color')
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
277
|
-
else if (attr.name !== 'style') {
|
|
278
|
-
return false;
|
|
279
|
-
}
|
|
280
|
-
const decls = attr.value
|
|
281
|
-
.split(';')
|
|
282
|
-
.map((d) => d.trim())
|
|
283
|
-
.filter(Boolean);
|
|
284
|
-
if (el.tagName === 'P') {
|
|
285
|
-
for (const decl of decls) {
|
|
286
|
-
const [prop] = decl.split(':').map((s) => s.trim());
|
|
287
|
-
if (prop !== 'text-align')
|
|
288
|
-
return false;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
else if (el.tagName === 'SPAN' || el.tagName === 'MARK') {
|
|
292
|
-
for (const decl of decls) {
|
|
293
|
-
const [prop, value] = decl.split(':').map((s) => s.trim());
|
|
294
|
-
// Allow `color: inherit` on <mark> (TipTap stamps it on the
|
|
295
|
-
// default highlight to inherit the span's color). Otherwise
|
|
296
|
-
// require the prop to be in the hydration whitelist.
|
|
297
|
-
if (el.tagName === 'MARK' &&
|
|
298
|
-
prop === 'color' &&
|
|
299
|
-
value === 'inherit') {
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
if (!HYDRATION_INLINE_STYLE_PROPS.has(prop))
|
|
303
|
-
return false;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
else {
|
|
307
|
-
return false;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
return true;
|
|
311
|
-
}
|
|
312
|
-
/**
|
|
313
|
-
* Walk a parsed HTML node tree. Return the joined text content if every
|
|
314
|
-
* descendant is hydration noise; return null if any node carries real
|
|
315
|
-
* authoring intent (links, partial formatting, custom styles, etc.).
|
|
316
|
-
*
|
|
317
|
-
* `isFormatTag` differs in how it's allowed to appear: a structural tag
|
|
318
|
-
* (P/SPAN/MARK) can have siblings; a format tag (STRONG/EM/U/S) is only
|
|
319
|
-
* allowed when it's the SOLE child of its parent. The caller enforces
|
|
320
|
-
* that at the iteration site.
|
|
321
|
-
*/
|
|
322
|
-
function extractCanonicalText(node) {
|
|
323
|
-
if (node.nodeType === 3 /* TEXT_NODE */)
|
|
324
|
-
return node.textContent ?? '';
|
|
325
|
-
if (node.nodeType !== 1 /* ELEMENT_NODE */) {
|
|
326
|
-
// Comments, CDATA, etc. — treat as opaque (preserve original).
|
|
327
|
-
return null;
|
|
328
|
-
}
|
|
329
|
-
const el = node;
|
|
330
|
-
// Attributeless <br> is TipTap's deterministic emission of a hard break.
|
|
331
|
-
// Treated as `\n` so a hydrated `<p><span>A<br>B</span></p>` canonicalizes
|
|
332
|
-
// to `"A\nB"` and matches the plain-side `"A%0AB"` after newline decoding.
|
|
333
|
-
// <br> with any attribute (e.g. `<br data-x>`) bails as before.
|
|
334
|
-
if (el.tagName === 'BR') {
|
|
335
|
-
return el.attributes.length === 0 ? '\n' : null;
|
|
336
|
-
}
|
|
337
|
-
const isStructural = STRUCTURAL_HYDRATION_TAGS.has(el.tagName);
|
|
338
|
-
const isFormat = FORMAT_HYDRATION_TAGS.has(el.tagName);
|
|
339
|
-
if (!isStructural && !isFormat)
|
|
340
|
-
return null;
|
|
341
|
-
if (!isHydrationOnlyAttributes(el))
|
|
342
|
-
return null;
|
|
343
|
-
const children = Array.from(el.childNodes);
|
|
344
|
-
let out = '';
|
|
345
|
-
for (const child of children) {
|
|
346
|
-
// Format tags only count as hydration when they wrap the ENTIRE
|
|
347
|
-
// content of their parent. If a <strong>/<em>/<u>/<s> appears
|
|
348
|
-
// alongside any sibling (text or element), it's partial-content
|
|
349
|
-
// formatting — a real edit that can't be expressed via textStyle —
|
|
350
|
-
// so bail.
|
|
351
|
-
if (child.nodeType === 1 &&
|
|
352
|
-
FORMAT_HYDRATION_TAGS.has(child.tagName) &&
|
|
353
|
-
children.length > 1) {
|
|
354
|
-
return null;
|
|
355
|
-
}
|
|
356
|
-
const part = extractCanonicalText(child);
|
|
357
|
-
if (part === null)
|
|
358
|
-
return null;
|
|
359
|
-
out += part;
|
|
360
|
-
}
|
|
361
|
-
return out;
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* If `value` is TipTap's default-hydration wrapper around plain text
|
|
365
|
-
* (only hydration-noise elements wrapping bare text), return that bare
|
|
366
|
-
* text. Otherwise return the original string unchanged.
|
|
367
|
-
*
|
|
368
|
-
* This collapses the BP-5506 phantom where opening a content sidebar —
|
|
369
|
-
* or selecting a section on the canvas — mounts every RTE editor at
|
|
370
|
-
* once and writes back wrapped HTML with no user-visible delta.
|
|
371
|
-
*/
|
|
372
|
-
function canonicalizeRteString(value) {
|
|
373
|
-
// Decode the codebase's URL-encoded newline (`%0A` — see
|
|
374
|
-
// `ESCAPED_NEWLINE_CHARACTER`) on the plain-text side. The hydrated side
|
|
375
|
-
// emits `<br>` for the same logical newline, and `extractCanonicalText`
|
|
376
|
-
// collapses attributeless `<br>` to `\n`, so the two converge.
|
|
377
|
-
// `split`/`join` rather than `decodeURIComponent` keeps this narrow —
|
|
378
|
-
// we don't want to decode `%20`, `%3C`, etc., which would change real
|
|
379
|
-
// user content.
|
|
380
|
-
const decodeNewlines = (s) => s.split(ESCAPED_NEWLINE_CHARACTER).join('\n');
|
|
381
|
-
// Cheap exit: no tags at all. Still decode `%0A` so a plain-text entry
|
|
382
|
-
// matches a hydrated entry whose only diff is `<br>`-vs-newline.
|
|
383
|
-
if (!value.includes('<'))
|
|
384
|
-
return decodeNewlines(value);
|
|
385
|
-
if (isEmptyRteHtml(value))
|
|
386
|
-
return '';
|
|
387
|
-
// DOMParser is unavailable in some non-browser test environments;
|
|
388
|
-
// fall back to leaving the value as-is so the caller produces a
|
|
389
|
-
// real diff (i.e. fail open — never falsely collapse).
|
|
390
|
-
if (typeof DOMParser === 'undefined')
|
|
391
|
-
return decodeNewlines(value);
|
|
392
|
-
let doc;
|
|
393
|
-
try {
|
|
394
|
-
doc = new DOMParser().parseFromString(`<div id="__rte_root__">${value}</div>`, 'text/html');
|
|
395
|
-
}
|
|
396
|
-
catch {
|
|
397
|
-
return decodeNewlines(value);
|
|
398
|
-
}
|
|
399
|
-
const root = doc.getElementById('__rte_root__');
|
|
400
|
-
if (!root)
|
|
401
|
-
return decodeNewlines(value);
|
|
402
|
-
// Join top-level blocks with `\n`. TipTap renders an Enter keypress as
|
|
403
|
-
// a new top-level `<p>` sibling — without the join, `<p>Hello</p>` and
|
|
404
|
-
// `<p>Hello</p><p></p>` (or `<p>Hello</p><p>World</p>`) collapse to the
|
|
405
|
-
// same canonical text and the Enter is invisible to dedup. The join
|
|
406
|
-
// also matches plain-side `%0A` between paragraphs (e.g. stored
|
|
407
|
-
// `Hello%0AWorld` ↔ hydrated `<p>Hello</p><p>World</p>`).
|
|
408
|
-
const parts = [];
|
|
409
|
-
for (const child of Array.from(root.childNodes)) {
|
|
410
|
-
const part = extractCanonicalText(child);
|
|
411
|
-
if (part === null)
|
|
412
|
-
return decodeNewlines(value);
|
|
413
|
-
parts.push(part);
|
|
414
|
-
}
|
|
415
|
-
return parts.join('\n');
|
|
416
|
-
}
|
|
417
|
-
function normalizeNode(node) {
|
|
418
|
-
if (Array.isArray(node))
|
|
419
|
-
return node.map(normalizeNode);
|
|
420
|
-
if (node === null || typeof node !== 'object')
|
|
421
|
-
return node;
|
|
422
|
-
const out = {};
|
|
423
|
-
for (const key of Object.keys(node)) {
|
|
424
|
-
let v = node[key];
|
|
425
|
-
// Strip the template-modified flag — it's meta-state that flips on
|
|
426
|
-
// first edit gesture (e.g. clicking into a panel) and does not
|
|
427
|
-
// represent a user-visible change.
|
|
428
|
-
if (key === 'isModifiedAfterTemplateApplied')
|
|
429
|
-
continue;
|
|
430
|
-
// Collapse RTE default-hydration wrappers down to the plain text
|
|
431
|
-
// they wrap. Plain "X" and `<p style="..."><span ...><mark ...>X
|
|
432
|
-
// </mark></span></p>` share the same canonical "X" so a sidebar
|
|
433
|
-
// open that mounts every editor at once doesn't generate phantom
|
|
434
|
-
// undo steps.
|
|
435
|
-
if (typeof v === 'string') {
|
|
436
|
-
v = canonicalizeRteString(v);
|
|
437
|
-
}
|
|
438
|
-
// Empty TipTap paragraphs collapse to "" — and an empty string is
|
|
439
|
-
// equivalent to the key being absent (JSON.stringify drops `undefined`),
|
|
440
|
-
// so skip the key entirely. Otherwise the normalizer would emit
|
|
441
|
-
// `"description": ""` here while the baseline snapshot — captured when
|
|
442
|
-
// `description` was `undefined` — has no `description` key at all,
|
|
443
|
-
// and the two normalized strings would differ.
|
|
444
|
-
if (isEmptyRteHtml(v) || v === '')
|
|
445
|
-
continue;
|
|
446
|
-
// textStyle / descriptionStyle / titleStyle / *Style objects.
|
|
447
|
-
//
|
|
448
|
-
// When `textStyleType` is set, the live store lazily fills in the
|
|
449
|
-
// resolved values (fontFamily, fontSize, color, fontWeight,
|
|
450
|
-
// fontStyle, letterSpacing, lineHeight, textDecoration, etc.) the
|
|
451
|
-
// first time a settings panel — or section-selection on the canvas
|
|
452
|
-
// — resolves `textStyleType` against `styles.textStyles.textStyleOptions`.
|
|
453
|
-
// None of those lazy fields represent a real user edit; the
|
|
454
|
-
// `textStyleType` itself is the authoring intent.
|
|
455
|
-
//
|
|
456
|
-
// Real user edits via the inline RTE toolbar set `textStyleType: undefined`
|
|
457
|
-
// (see useRichTextEditor.ts `fontStyleChanges = { ..., textStyleType: undefined }`),
|
|
458
|
-
// so we leave every key intact in that case — those values ARE the
|
|
459
|
-
// real authoring intent.
|
|
460
|
-
//
|
|
461
|
-
// Per-content style edits via the global Style panel land in
|
|
462
|
-
// `snapshot.styles.textStyles` (a separate sub-tree), so they still
|
|
463
|
-
// produce a real diff outside this branch.
|
|
464
|
-
if (v !== null &&
|
|
465
|
-
typeof v === 'object' &&
|
|
466
|
-
!Array.isArray(v) &&
|
|
467
|
-
/Style$/.test(key)) {
|
|
468
|
-
const styleObj = v;
|
|
469
|
-
const hasTextStyleType = styleObj.textStyleType !== undefined &&
|
|
470
|
-
styleObj.textStyleType !== null;
|
|
471
|
-
if (hasTextStyleType) {
|
|
472
|
-
// Drop everything except `textStyleType` itself + `textAlign`,
|
|
473
|
-
// which is the user's per-instance alignment override (preserved
|
|
474
|
-
// on hydration via `globalContentStyle = { ...resolved, textAlign: style?.textAlign }`).
|
|
475
|
-
out[key] = {
|
|
476
|
-
textStyleType: styleObj.textStyleType,
|
|
477
|
-
...(styleObj.textAlign !== undefined
|
|
478
|
-
? { textAlign: styleObj.textAlign }
|
|
479
|
-
: {}),
|
|
480
|
-
};
|
|
481
|
-
continue;
|
|
482
|
-
}
|
|
483
|
-
// No textStyleType — these are real per-content overrides. Recurse
|
|
484
|
-
// normally so nested objects still get the standard treatment.
|
|
485
|
-
out[key] = normalizeNode(styleObj);
|
|
486
|
-
continue;
|
|
487
|
-
}
|
|
488
|
-
out[key] = normalizeNode(v);
|
|
489
|
-
}
|
|
490
|
-
return out;
|
|
491
|
-
}
|
|
492
|
-
function normalizeSnapshotForDedup(snapshotStr) {
|
|
493
|
-
try {
|
|
494
|
-
const parsed = JSON.parse(snapshotStr);
|
|
495
|
-
return JSON.stringify(normalizeNode(parsed));
|
|
496
|
-
}
|
|
497
|
-
catch {
|
|
498
|
-
return snapshotStr;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
156
|
+
const mergeNextFlush = ref(false);
|
|
501
157
|
/**
|
|
502
158
|
* Commit a new entry into the stack at the current pointer position.
|
|
503
159
|
*
|
|
@@ -524,19 +180,8 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
524
180
|
*/
|
|
525
181
|
function commitEntry(args) {
|
|
526
182
|
const current = entries.value[currentIndex.value];
|
|
527
|
-
if (current)
|
|
528
|
-
|
|
529
|
-
return 'noop';
|
|
530
|
-
// Phantom-edit dedup: normalize both snapshots to ignore
|
|
531
|
-
// sidebar-open reactive cascades (RTE empty-paragraph init, lazy
|
|
532
|
-
// textStyle hydration, isModifiedAfterTemplateApplied flips). If
|
|
533
|
-
// they're equal after normalization, this capture has no
|
|
534
|
-
// user-visible diff vs. the entry at the pointer.
|
|
535
|
-
if (normalizeSnapshotForDedup(current.snapshot) ===
|
|
536
|
-
normalizeSnapshotForDedup(args.snapshot)) {
|
|
537
|
-
return 'noop';
|
|
538
|
-
}
|
|
539
|
-
}
|
|
183
|
+
if (current && current.snapshot === args.snapshot)
|
|
184
|
+
return 'noop';
|
|
540
185
|
if (args.snapshot.length > MAX_ENTRY_BYTES) {
|
|
541
186
|
eventTracker.sendHistoryOversizedEvent({
|
|
542
187
|
attempted_bytes: args.snapshot.length,
|
|
@@ -642,6 +287,17 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
642
287
|
// than the pre-router value captured in `captureBaseline`).
|
|
643
288
|
const inSettleWindow = currentIndex.value === 0 &&
|
|
644
289
|
Date.now() - baselineCapturedAt < BASELINE_SETTLE_MS;
|
|
290
|
+
// Merge branch — a hydration write site (TipTap mount, lazy
|
|
291
|
+
// paymentConfig fill, stale-page-ref clear) flagged this flush as
|
|
292
|
+
// "fold into the head, don't push". Replace `entries[currentIndex]`
|
|
293
|
+
// in place so the user-visible undo stack stays exactly as it was
|
|
294
|
+
// before the hydration burst arrived. Consume the flag whether or
|
|
295
|
+
// not we're in the settle window — `inSettleWindow` already merges
|
|
296
|
+
// into the baseline, but past that window the same merge has to
|
|
297
|
+
// happen against the current head for hydrations that fire when the
|
|
298
|
+
// user clicks into a section after the settle elapsed.
|
|
299
|
+
const shouldMerge = mergeNextFlush.value;
|
|
300
|
+
mergeNextFlush.value = false;
|
|
645
301
|
if (inSettleWindow) {
|
|
646
302
|
entries.value = [
|
|
647
303
|
{
|
|
@@ -655,6 +311,19 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
655
311
|
];
|
|
656
312
|
return;
|
|
657
313
|
}
|
|
314
|
+
if (shouldMerge && entries.value[currentIndex.value]) {
|
|
315
|
+
const next = entries.value.slice();
|
|
316
|
+
next[currentIndex.value] = {
|
|
317
|
+
...next[currentIndex.value],
|
|
318
|
+
snapshot,
|
|
319
|
+
timestamp: new Date().toISOString(),
|
|
320
|
+
sizeBytes: snapshot.length,
|
|
321
|
+
activePageId,
|
|
322
|
+
logoEdits,
|
|
323
|
+
};
|
|
324
|
+
entries.value = next;
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
658
327
|
commitEntry({
|
|
659
328
|
snapshot,
|
|
660
329
|
source: 'manual_edit',
|
|
@@ -828,16 +497,25 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
828
497
|
}
|
|
829
498
|
const targetEntry = entries.value[targetIndex];
|
|
830
499
|
isApplyingSnapshot.value = true;
|
|
500
|
+
// Drop any merge flag a hydration site might have armed but the
|
|
501
|
+
// watcher hasn't yet consumed — otherwise the post-apply watcher
|
|
502
|
+
// tick would silently fold the freshly-restored snapshot into the
|
|
503
|
+
// entry at the new pointer position and erase the user's redo.
|
|
504
|
+
mergeNextFlush.value = false;
|
|
505
|
+
cancelPendingAutoCapture();
|
|
831
506
|
try {
|
|
832
507
|
await brandPageMakerStore.applyHistorySnapshot(snapshot, resolvedActivePageId, targetEntry?.logoEdits);
|
|
833
508
|
await nextTick();
|
|
834
509
|
// Re-stamp `wrapper.paymentConfig` on every payment-config wrapper in
|
|
835
|
-
// the freshly-restored content tree from the live shop store.
|
|
836
|
-
//
|
|
837
|
-
//
|
|
838
|
-
//
|
|
839
|
-
//
|
|
840
|
-
//
|
|
510
|
+
// the freshly-restored content tree from the live shop store. Stripe
|
|
511
|
+
// Connect linkage is backend-confirmed (per `project-guardrails.md`)
|
|
512
|
+
// and must NEVER come from the snapshot — the captured value can be
|
|
513
|
+
// arbitrarily stale (an entry captured before
|
|
514
|
+
// `PaymentConfigField.mixin.ts:created()` lazily filled in the
|
|
515
|
+
// wrapper carries `paymentConfig: undefined`) and using it would
|
|
516
|
+
// re-surface the "Please enable a payment provider" warning on
|
|
517
|
+
// undo. The shop store's value is itself never undoable (see
|
|
518
|
+
// `applyHistorySnapshot` step 6).
|
|
841
519
|
restorePaymentConfigsOnLivePages();
|
|
842
520
|
}
|
|
843
521
|
finally {
|
|
@@ -938,12 +616,30 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
938
616
|
if (!baselineCaptured.value)
|
|
939
617
|
return;
|
|
940
618
|
cancelPendingAutoCapture();
|
|
619
|
+
mergeNextFlush.value = false;
|
|
941
620
|
entries.value = [];
|
|
942
621
|
currentIndex.value = -1;
|
|
943
622
|
baselineCaptured.value = false;
|
|
944
623
|
baselineCapturedAt = 0;
|
|
945
624
|
captureBaseline();
|
|
946
625
|
}
|
|
626
|
+
/**
|
|
627
|
+
* Arm the next watcher flush to fold its snapshot into the current head
|
|
628
|
+
* entry instead of pushing a new one. Hydration write sites (TipTap mount,
|
|
629
|
+
* lazy paymentConfig fill, stale-page-ref clear) call this immediately
|
|
630
|
+
* before the imperative `update:modelValue` / `$emit` they know is a
|
|
631
|
+
* hydration mutation. The next flush consumes (clears) the flag.
|
|
632
|
+
*
|
|
633
|
+
* Idempotent — multiple call sites in the same burst all set the same
|
|
634
|
+
* flag and collapse together into a single merge into the current head.
|
|
635
|
+
*
|
|
636
|
+
* No-op when the watcher hasn't yet baselined: hydration writes inside
|
|
637
|
+
* the settle window already merge into the baseline; outside a flagged
|
|
638
|
+
* session the flag has no observable effect.
|
|
639
|
+
*/
|
|
640
|
+
function mergeNextMutation() {
|
|
641
|
+
mergeNextFlush.value = true;
|
|
642
|
+
}
|
|
947
643
|
// --- Instrumentation (NFR-04a) ---
|
|
948
644
|
/** Sum of all entries' serialized sizes in bytes. */
|
|
949
645
|
function getCurrentSizeBytes() {
|
|
@@ -967,6 +663,7 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
967
663
|
capture,
|
|
968
664
|
captureTemplateSwitch,
|
|
969
665
|
captureAiChangeset,
|
|
666
|
+
mergeNextMutation,
|
|
970
667
|
undo,
|
|
971
668
|
redo,
|
|
972
669
|
reset,
|
|
@@ -19,7 +19,7 @@ export const useBrandPageMakerUserStore = defineStore('brandPageMakerUser', {
|
|
|
19
19
|
canUseGettyVideos: false,
|
|
20
20
|
canUseGoogleAdsense: false,
|
|
21
21
|
googleAdsenseEnabled: false,
|
|
22
|
-
canUseUndoRedo:
|
|
22
|
+
canUseUndoRedo: true,
|
|
23
23
|
},
|
|
24
24
|
emailAddress: '',
|
|
25
25
|
hasPremiumSubscription: false,
|
package/lib/src/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as E, a as T, b as s, c as I, d as S, e as N, f as e, B as A, g as O, h as C, i as G, j as D, k as P, l as t, m as R, n as L, o as M, p as o, q as r, r as b, s as n, t as F, u as i, v as l, w as g, x as B, y as c, z as Y, C as U, D as d, E as u, F as H, G as p, H as y, I as K, J as k, K as m, L as V, M as X, N as h, O as v, P as W, Q as f, R as x, S as z, T as w, U as Z, V as j, W as Q, X as q, Y as J, Z as $, $ as aa, a0 as _a, a1 as Ea, a2 as Ta, a3 as sa, a4 as Ia, a5 as Sa, a6 as Na, a7 as ea, a8 as Aa, a9 as Oa, aa as Ca, ab as Ga, ac as Da, ad as Pa, ae as ta, af as Ra, ag as La, ah as Ma, ai as oa, aj as ra, ak as ba, al as na, am as Fa, an as ia, ao as la, ap as ga, aq as Ba, ar as ca, as as Ya, at as Ua, au as da, av as ua, aw as Ha, ax as pa, ay as ya, az as Ka, aA as ka, aB as ma, aC as Va, aD as Xa, aE as ha, aF as va, aG as Wa, aH as fa, aI as xa, aJ as za, aK as wa, aL as Za, aM as ja, aN as Qa, aO as qa, aP as Ja, aQ as $a, aR as a_, aS as __, aT as E_, aU as T_, aV as s_, aW as I_, aX as S_, aY as N_, aZ as e_, a_ as A_, a$ as O_, b0 as C_, b1 as G_, b2 as D_, b3 as P_, b4 as t_, b5 as R_, b6 as L_, b7 as M_, b8 as o_, b9 as r_, ba as b_, bb as n_, bc as F_, bd as i_, be as l_, bf as g_, bg as B_, bh as c_, bi as Y_, bj as U_, bk as d_, bl as u_, bm as H_, bn as p_, bo as y_, bp as K_, bq as k_, br as m_, bs as V_, bt as X_, bu as h_, bv as v_, bw as W_, bx as f_, by as x_, bz as z_, bA as w_, bB as Z_, bC as j_, bD as Q_, bE as q_, bF as J_, bG as $_, bH as aE, bI as _E, bJ as EE, bK as TE, bL as sE, bM as IE, bN as SE, bO as NE, bP as eE, bQ as AE, bR as OE, bS as CE, bT as GE, bU as DE, bV as PE, bW as tE, bX as RE, bY as LE, bZ as ME, b_ as oE, b$ as rE, c0 as bE, c1 as nE, c2 as FE, c3 as iE, c4 as lE, c5 as gE, c6 as BE, c7 as cE, c8 as YE, c9 as UE, ca as dE, cb as uE, cc as HE, cd as pE, ce as yE, cf as KE, cg as kE, ch as mE, ci as VE, cj as XE, ck as hE, cl as vE, cm as WE, cn as fE, co as xE } from "./lib-
|
|
1
|
+
import { A as E, a as T, b as s, c as I, d as S, e as N, f as e, B as A, g as O, h as C, i as G, j as D, k as P, l as t, m as R, n as L, o as M, p as o, q as r, r as b, s as n, t as F, u as i, v as l, w as g, x as B, y as c, z as Y, C as U, D as d, E as u, F as H, G as p, H as y, I as K, J as k, K as m, L as V, M as X, N as h, O as v, P as W, Q as f, R as x, S as z, T as w, U as Z, V as j, W as Q, X as q, Y as J, Z as $, $ as aa, a0 as _a, a1 as Ea, a2 as Ta, a3 as sa, a4 as Ia, a5 as Sa, a6 as Na, a7 as ea, a8 as Aa, a9 as Oa, aa as Ca, ab as Ga, ac as Da, ad as Pa, ae as ta, af as Ra, ag as La, ah as Ma, ai as oa, aj as ra, ak as ba, al as na, am as Fa, an as ia, ao as la, ap as ga, aq as Ba, ar as ca, as as Ya, at as Ua, au as da, av as ua, aw as Ha, ax as pa, ay as ya, az as Ka, aA as ka, aB as ma, aC as Va, aD as Xa, aE as ha, aF as va, aG as Wa, aH as fa, aI as xa, aJ as za, aK as wa, aL as Za, aM as ja, aN as Qa, aO as qa, aP as Ja, aQ as $a, aR as a_, aS as __, aT as E_, aU as T_, aV as s_, aW as I_, aX as S_, aY as N_, aZ as e_, a_ as A_, a$ as O_, b0 as C_, b1 as G_, b2 as D_, b3 as P_, b4 as t_, b5 as R_, b6 as L_, b7 as M_, b8 as o_, b9 as r_, ba as b_, bb as n_, bc as F_, bd as i_, be as l_, bf as g_, bg as B_, bh as c_, bi as Y_, bj as U_, bk as d_, bl as u_, bm as H_, bn as p_, bo as y_, bp as K_, bq as k_, br as m_, bs as V_, bt as X_, bu as h_, bv as v_, bw as W_, bx as f_, by as x_, bz as z_, bA as w_, bB as Z_, bC as j_, bD as Q_, bE as q_, bF as J_, bG as $_, bH as aE, bI as _E, bJ as EE, bK as TE, bL as sE, bM as IE, bN as SE, bO as NE, bP as eE, bQ as AE, bR as OE, bS as CE, bT as GE, bU as DE, bV as PE, bW as tE, bX as RE, bY as LE, bZ as ME, b_ as oE, b$ as rE, c0 as bE, c1 as nE, c2 as FE, c3 as iE, c4 as lE, c5 as gE, c6 as BE, c7 as cE, c8 as YE, c9 as UE, ca as dE, cb as uE, cc as HE, cd as pE, ce as yE, cf as KE, cg as kE, ch as mE, ci as VE, cj as XE, ck as hE, cl as vE, cm as WE, cn as fE, co as xE } from "./lib-BBItimSg.js";
|
|
2
2
|
import { createPinia as wE, setActivePinia as ZE } from "pinia";
|
|
3
3
|
export {
|
|
4
4
|
E as ABOUT_SECTION_SETTINGS_CONFIG,
|