@designcrowd/library.brand-page 6.0.6-ast-bugs-2 → 6.0.6-json-prereq-1
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-kVnd7OFP.js → CreatedOnBrandCrowd-de-de-Dx3e-vQ6.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-DG29QU7L.js → CreatedOnBrandCrowd-en-us-CbchLDa2.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-DuTyBvo7.js → CreatedOnBrandCrowd-es-es-BXbpN7li.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-CLd7H2W4.js → CreatedOnBrandCrowd-fr-ca-hIk7h9bV.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-CLd7H2W4.js → CreatedOnBrandCrowd-fr-fr-hIk7h9bV.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-BwR0sCCg.js → CreatedOnBrandCrowd-pt-br-CqsCRIqc.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-BwR0sCCg.js → CreatedOnBrandCrowd-pt-pt-CqsCRIqc.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-DLp2FCNs.js → CreatedOnDcom-de-de-NEOtgsvz.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-DctDto-b.js → CreatedOnDcom-en-us-CKtEdcoG.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-B2AXjnem.js → CreatedOnDcom-es-es-DZR9o6DK.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-X-iviFNZ.js → CreatedOnDcom-fr-ca-Bo_oTp-T.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-C63sxkIx.js → CreatedOnDcom-fr-fr-DE6Fa3ie.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-DJoDFoQd.js → CreatedOnDcom-pt-br-Cws2QJ7J.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-DrmkZyNm.js → CreatedOnDcom-pt-pt-DYGQo6q3.js} +1 -1
- package/lib/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.js +2 -7
- package/lib/src/brand-page/models/index.d.ts +34 -26
- package/lib/src/brand-page/utils/migration/migration-18.d.ts +2 -0
- package/lib/src/brand-page/utils/migration/migration-18.js +71 -0
- package/lib/src/brand-page/utils/migration.util.js +11 -0
- package/lib/src/brand-page/utils/text.util.d.ts +1 -1
- package/lib/src/brand-page/utils/text.util.js +32 -0
- package/lib/src/brand-page/utils/vcard.util.js +4 -4
- package/lib/src/brand-page/view-models.d.ts +3 -3
- package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.d.ts +22 -0
- package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.js +22 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +28 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.js +66 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +20 -99
- package/lib/src/brand-page-maker/utils/index.d.ts +2 -0
- package/lib/src/brand-page-maker/utils/index.js +2 -0
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +19 -5
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +35 -5
- package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +32 -0
- package/lib/src/brand-page-maker/utils/rte-render.util.js +78 -0
- package/lib/src/brand-page-maker/utils/rte-sanitize.util.d.ts +6 -0
- package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +158 -0
- package/lib/src/css/library.brand-page-brandCrowd.min.css +27 -40
- package/lib/src/css/library.brand-page-brandPage.min.css +27 -40
- package/lib/src/css/library.brand-page-designCom.min.css +27 -40
- package/lib/src/css/library.brand-page-preview.min.css +27 -40
- package/lib/src/index.d.ts +1 -0
- package/lib/src/index.mjs +150 -145
- package/lib/src/{lib-D5-Y-5cE.js → lib-C5H_bvMR.js} +41122 -40972
- package/lib/src/lib.js +2 -0
- package/lib/src/metadata-builder/builders/update-content-text-builder.js +20 -15
- package/lib/src/metadata-builder/builders/update-content-text-style-builder.js +5 -3
- package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +1 -1
- package/lib/src/metadata-builder/helpers/rte.helpers.js +6 -3
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +12 -3
- package/lib/src/utils/minify-brand-page-metadata.js +4 -1
- package/package.json +1 -1
- package/src/brand-page/components/banner-section/BannerSection.vue +1 -13
- package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.ts +3 -11
- package/src/brand-page/components/shop/Shop.vue +1 -0
- package/src/brand-page/components/text-wrapper/TextWrapper.vue +17 -6
- package/src/brand-page/models/index.ts +35 -26
- package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
- package/src/brand-page/utils/migration/migration-18.test.ts +72 -0
- package/src/brand-page/utils/migration/migration-18.ts +83 -0
- package/src/brand-page/utils/migration.util.ts +12 -0
- package/src/brand-page/utils/style.util.test.ts +51 -0
- package/src/brand-page/utils/text.util.test.ts +47 -0
- package/src/brand-page/utils/text.util.ts +43 -4
- package/src/brand-page/utils/vcard.util.ts +6 -4
- package/src/brand-page/view-models.ts +3 -2
- package/src/brand-page-maker/__fixtures__/complex-rte.fixture.ts +23 -0
- package/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.ts +113 -0
- package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +22 -106
- package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +198 -0
- package/src/brand-page-maker/utils/index.ts +2 -0
- package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +4 -3
- package/src/brand-page-maker/utils/rich-text-editor.util.ts +50 -11
- package/src/brand-page-maker/utils/rte-render-parity.test.ts +144 -0
- package/src/brand-page-maker/utils/rte-render.util.ts +112 -0
- package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +158 -0
- package/src/brand-page-maker/utils/rte-sanitize.util.ts +195 -0
- package/src/lib.ts +9 -0
- package/src/metadata-builder/builders/update-content-text-builder.ts +32 -18
- package/src/metadata-builder/builders/update-content-text-style-builder.ts +6 -4
- package/src/metadata-builder/helpers/rte.helpers.ts +11 -7
- package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +16 -3
- package/src/utils/minify-brand-page-metadata.ts +4 -1
package/lib/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { defineComponent } from 'vue';
|
|
2
|
-
import { isUnsetOrTransparentColor } from '../../../models';
|
|
3
2
|
export default defineComponent({
|
|
4
3
|
props: {
|
|
5
4
|
context: {
|
|
@@ -76,16 +75,12 @@ export default defineComponent({
|
|
|
76
75
|
const baseStyle = isActive
|
|
77
76
|
? this.selectedButtonStyle
|
|
78
77
|
: this.defaultButtonStyle;
|
|
79
|
-
const subNavBackgroundColor = this.styles.hamburgerMenuStyle.subNavBackgroundColor ?? '#ffffff';
|
|
80
|
-
// An active sub-page uses the selected button colour, but when that colour
|
|
81
|
-
// is transparent/unset it must keep the sub-nav background colour rather
|
|
82
|
-
// than rendering transparent (BP-5751).
|
|
83
78
|
return {
|
|
84
79
|
...baseStyle,
|
|
85
80
|
paddingLeft: '32px',
|
|
86
|
-
backgroundColor: isActive
|
|
81
|
+
backgroundColor: isActive
|
|
87
82
|
? baseStyle.backgroundColor
|
|
88
|
-
: subNavBackgroundColor,
|
|
83
|
+
: (this.styles.hamburgerMenuStyle.subNavBackgroundColor ?? '#ffffff'),
|
|
89
84
|
};
|
|
90
85
|
},
|
|
91
86
|
closeSideMenu() {
|
|
@@ -7,6 +7,14 @@ import { TextStyles } from './styles.model';
|
|
|
7
7
|
import { LogoStyles } from './logo.model';
|
|
8
8
|
import type { LogoImageModel, LogoTemplateImageModel } from './logo.model';
|
|
9
9
|
import type { BrandPagePaymentConfig } from './payment.model';
|
|
10
|
+
import type { JSONContent } from '@tiptap/vue-3';
|
|
11
|
+
/**
|
|
12
|
+
* BP-5787 — a rich-text field is either a legacy inline-HTML string or a
|
|
13
|
+
* Tiptap JSON document. Typing RTE content fields as RichTextValue (instead of
|
|
14
|
+
* `string`) forces every consumer to handle both forms — surfacing at compile
|
|
15
|
+
* time any code that assumes a plain string (e.g. `.replace`, `.length`).
|
|
16
|
+
*/
|
|
17
|
+
export type RichTextValue = string | JSONContent;
|
|
10
18
|
export * from './brand-page-context.model';
|
|
11
19
|
export * from './payment.model';
|
|
12
20
|
export * from './content.model';
|
|
@@ -141,7 +149,7 @@ export interface TextContent extends Content {
|
|
|
141
149
|
/**
|
|
142
150
|
* For BrandPage/BrandContact only
|
|
143
151
|
*/
|
|
144
|
-
text:
|
|
152
|
+
text: RichTextValue;
|
|
145
153
|
/**
|
|
146
154
|
* For BrandSite
|
|
147
155
|
*/
|
|
@@ -164,9 +172,9 @@ export interface TextContent extends Content {
|
|
|
164
172
|
}
|
|
165
173
|
export interface ProfileContent extends Content {
|
|
166
174
|
avatarImageUrl: string;
|
|
167
|
-
name:
|
|
168
|
-
title:
|
|
169
|
-
description:
|
|
175
|
+
name: RichTextValue;
|
|
176
|
+
title: RichTextValue;
|
|
177
|
+
description: RichTextValue;
|
|
170
178
|
backgroundColor: BrandPageCssStyle;
|
|
171
179
|
colorReplacements: ColorReplacement[];
|
|
172
180
|
mediaType?: string;
|
|
@@ -184,7 +192,7 @@ export interface MapContent extends Content {
|
|
|
184
192
|
}
|
|
185
193
|
export interface MailingListContent extends Content {
|
|
186
194
|
acceptedFields?: 'emailOnly' | 'nameAndEmail';
|
|
187
|
-
title:
|
|
195
|
+
title: RichTextValue;
|
|
188
196
|
buttonText: string;
|
|
189
197
|
titleStyle: TextStyleDeclaration;
|
|
190
198
|
buttonTextStyle: TextStyleDeclaration;
|
|
@@ -235,10 +243,10 @@ export interface FormField {
|
|
|
235
243
|
export interface ContactFormContent extends Content {
|
|
236
244
|
type: 'contact-form';
|
|
237
245
|
displayStyle: string;
|
|
238
|
-
title:
|
|
239
|
-
description:
|
|
246
|
+
title: RichTextValue;
|
|
247
|
+
description: RichTextValue;
|
|
240
248
|
buttonText: string;
|
|
241
|
-
successMessage:
|
|
249
|
+
successMessage: RichTextValue;
|
|
242
250
|
fields: FormField[];
|
|
243
251
|
responseEmail: string;
|
|
244
252
|
imageUrl?: string;
|
|
@@ -257,8 +265,8 @@ export interface ContactFormContent extends Content {
|
|
|
257
265
|
export interface BannerSectionContent extends Content {
|
|
258
266
|
type: 'banner-section';
|
|
259
267
|
imageUrl: string;
|
|
260
|
-
title:
|
|
261
|
-
description:
|
|
268
|
+
title: RichTextValue;
|
|
269
|
+
description: RichTextValue;
|
|
262
270
|
buttonText: string | false;
|
|
263
271
|
url: string;
|
|
264
272
|
email?: string;
|
|
@@ -285,8 +293,8 @@ export declare const MEDIA_TYPES: {
|
|
|
285
293
|
};
|
|
286
294
|
export type MediaType = keyof typeof MEDIA_TYPES;
|
|
287
295
|
export interface TextMediaItem extends ImageItem {
|
|
288
|
-
title:
|
|
289
|
-
paragraph:
|
|
296
|
+
title: RichTextValue;
|
|
297
|
+
paragraph: RichTextValue;
|
|
290
298
|
itemsId: number;
|
|
291
299
|
buttonText?: string | false;
|
|
292
300
|
buttonPageId?: string;
|
|
@@ -305,16 +313,16 @@ export interface TextShapeItem extends TextMediaItem {
|
|
|
305
313
|
export interface ShapeItem extends ImageItem {
|
|
306
314
|
colorReplacements: ColorReplacement[];
|
|
307
315
|
mediaType: MediaType;
|
|
308
|
-
title?:
|
|
309
|
-
paragraph?:
|
|
316
|
+
title?: RichTextValue;
|
|
317
|
+
paragraph?: RichTextValue;
|
|
310
318
|
titleOverride?: string;
|
|
311
319
|
paragraphOverride?: string;
|
|
312
320
|
}
|
|
313
321
|
export interface TestimonialItems {
|
|
314
322
|
imageUrl: string;
|
|
315
|
-
name:
|
|
316
|
-
position:
|
|
317
|
-
testimonials:
|
|
323
|
+
name: RichTextValue;
|
|
324
|
+
position: RichTextValue;
|
|
325
|
+
testimonials: RichTextValue;
|
|
318
326
|
colorReplacements: ColorReplacement[];
|
|
319
327
|
mediaType: string;
|
|
320
328
|
size?: string;
|
|
@@ -325,7 +333,7 @@ export interface TestimonialItems {
|
|
|
325
333
|
positionOverride?: string;
|
|
326
334
|
}
|
|
327
335
|
export interface TextItem {
|
|
328
|
-
text:
|
|
336
|
+
text: RichTextValue;
|
|
329
337
|
textStyle: TextStyleDeclaration;
|
|
330
338
|
textOverride?: string;
|
|
331
339
|
}
|
|
@@ -342,8 +350,8 @@ export interface ImagesSectionContent extends Content, MediaSection {
|
|
|
342
350
|
enablePopup: boolean;
|
|
343
351
|
}
|
|
344
352
|
export interface MediaSection extends Content {
|
|
345
|
-
title:
|
|
346
|
-
description:
|
|
353
|
+
title: RichTextValue;
|
|
354
|
+
description: RichTextValue;
|
|
347
355
|
displayStyle: ImagesSectionContentDisplayStyle;
|
|
348
356
|
items: TextShapeItem[] | TextMediaItem[];
|
|
349
357
|
titleStyle: TextStyleDeclaration;
|
|
@@ -360,7 +368,7 @@ export interface ColorReplacement {
|
|
|
360
368
|
}
|
|
361
369
|
export interface TestimonialSectionContent extends Content {
|
|
362
370
|
type: 'testimonial-section';
|
|
363
|
-
title:
|
|
371
|
+
title: RichTextValue;
|
|
364
372
|
displayStyle: string;
|
|
365
373
|
items: TestimonialItems[];
|
|
366
374
|
titleStyle: TextStyleDeclaration;
|
|
@@ -374,8 +382,8 @@ export interface TestimonialSectionContent extends Content {
|
|
|
374
382
|
export interface AboutSectionContent extends Content {
|
|
375
383
|
type: 'about-section';
|
|
376
384
|
imageUrl: string;
|
|
377
|
-
title:
|
|
378
|
-
paragraph:
|
|
385
|
+
title: RichTextValue;
|
|
386
|
+
paragraph: RichTextValue;
|
|
379
387
|
displayStyle: string;
|
|
380
388
|
titleStyle: TextStyleDeclaration;
|
|
381
389
|
paragraphStyle: TextStyleDeclaration;
|
|
@@ -397,7 +405,7 @@ export declare const SOCIAL_LINKS_ICON_DISPLAY_STYLES: {
|
|
|
397
405
|
export type SocialLinksIconDisplayStyle = keyof typeof SOCIAL_LINKS_ICON_DISPLAY_STYLES;
|
|
398
406
|
export interface SocialLinksContent extends Content {
|
|
399
407
|
type: 'social-links';
|
|
400
|
-
title:
|
|
408
|
+
title: RichTextValue;
|
|
401
409
|
links: LinksItem[];
|
|
402
410
|
iconDisplayStyle: SocialLinksIconDisplayStyle;
|
|
403
411
|
iconFillStyles: BrandPageCssStyle;
|
|
@@ -420,8 +428,8 @@ export interface DividerSectionContent extends Content {
|
|
|
420
428
|
}
|
|
421
429
|
export interface BookingSectionContent extends Content {
|
|
422
430
|
type: 'booking-section';
|
|
423
|
-
title:
|
|
424
|
-
description:
|
|
431
|
+
title: RichTextValue;
|
|
432
|
+
description: RichTextValue;
|
|
425
433
|
buttonText: string;
|
|
426
434
|
url: string;
|
|
427
435
|
email?: string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { hasRTEContent } from '../../../brand-page-maker/utils';
|
|
2
|
+
import { useExternalRichTextEditor } from '../../../brand-page-maker/composables/rich-text-editor/useRichTextEditor';
|
|
3
|
+
/**
|
|
4
|
+
* BP-5787 — migrate existing inline-HTML RTE content to Tiptap JSON.
|
|
5
|
+
*
|
|
6
|
+
* The editor now emits, and the renderer now reads, Tiptap JSON. Existing
|
|
7
|
+
* published/saved sites still hold RTE fields as HTML strings. This converts
|
|
8
|
+
* them on load:
|
|
9
|
+
* - maker: the loaded model is JSON, so the next save persists JSON.
|
|
10
|
+
* - web-renderer: lazy migrate-on-read — old sites render from JSON without
|
|
11
|
+
* needing a re-save.
|
|
12
|
+
*
|
|
13
|
+
* Runs on EVERY load (runAlways) so it also normalises content the AI section
|
|
14
|
+
* builders still emit as HTML — there is no need to change those builders or
|
|
15
|
+
* widen the content model types.
|
|
16
|
+
*
|
|
17
|
+
* Scope: only HTML-RTE strings (hasRTEContent) are converted. Plain strings,
|
|
18
|
+
* numbers, and already-JSON docs are left untouched, so the migration is
|
|
19
|
+
* idempotent (re-running on JSON content is a no-op — non-string values are
|
|
20
|
+
* skipped). Nested RTE inside item arrays (textItems, testimonials, images…)
|
|
21
|
+
* is handled by recursing into arrays; style objects are intentionally not
|
|
22
|
+
* descended into (they never hold RTE content).
|
|
23
|
+
*/
|
|
24
|
+
const convertRteStringsToJson = (value, editor) => {
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
value.forEach((item) => convertRteStringsToJson(item, editor));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!value || typeof value !== 'object')
|
|
30
|
+
return;
|
|
31
|
+
const record = value;
|
|
32
|
+
Object.keys(record).forEach((key) => {
|
|
33
|
+
const field = record[key];
|
|
34
|
+
if (typeof field === 'string' && hasRTEContent(field)) {
|
|
35
|
+
editor.commands.setContent(field);
|
|
36
|
+
record[key] = editor.getJSON();
|
|
37
|
+
}
|
|
38
|
+
else if (Array.isArray(field)) {
|
|
39
|
+
convertRteStringsToJson(field, editor);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
export const migration18 = async (migrationResult) => {
|
|
44
|
+
const { brandPageModel } = migrationResult;
|
|
45
|
+
// HTML→JSON parsing needs a DOM (Tiptap's editor). In a DOM-less context
|
|
46
|
+
// (pure-node SSR or unit tests) we can't parse HTML, so we leave the RTE
|
|
47
|
+
// fields as HTML strings — the renderer and the fraud/translation extractors
|
|
48
|
+
// all dual-read HTML, so this is safe. Conversion happens wherever a DOM is
|
|
49
|
+
// available (the browser editor, jsdom).
|
|
50
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
51
|
+
return migrationResult;
|
|
52
|
+
}
|
|
53
|
+
let externalEditor;
|
|
54
|
+
try {
|
|
55
|
+
externalEditor = useExternalRichTextEditor().externalEditor;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return migrationResult;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
for (const page of brandPageModel.pages) {
|
|
62
|
+
for (const content of page.contents) {
|
|
63
|
+
convertRteStringsToJson(content, externalEditor);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
externalEditor.destroy();
|
|
69
|
+
}
|
|
70
|
+
return migrationResult;
|
|
71
|
+
};
|
|
@@ -20,6 +20,7 @@ import { migration14 } from './migration/migration-14';
|
|
|
20
20
|
import { migration15 } from './migration/migration-15';
|
|
21
21
|
import { migration16 } from './migration/migration-16';
|
|
22
22
|
import { migration17 } from './migration/migration-17';
|
|
23
|
+
import { migration18 } from './migration/migration-18';
|
|
23
24
|
const MIGRATION_TABLE = [
|
|
24
25
|
{
|
|
25
26
|
version: 1,
|
|
@@ -132,6 +133,16 @@ const MIGRATION_TABLE = [
|
|
|
132
133
|
runAlways: true,
|
|
133
134
|
description: 'Padding Control backfill (BP-5699 Tier 2 + BP-5700 Store): backfill existing top/bottom spacing as explicit padding for text (12px), images/social links/shapes/booking (32px), store (16px), contact form (per layout), banner and about (per display style)',
|
|
134
135
|
},
|
|
136
|
+
{
|
|
137
|
+
version: 18,
|
|
138
|
+
migrateFn: migration18,
|
|
139
|
+
// runAlways: normalise RTE to JSON on every load. Idempotent (only HTML
|
|
140
|
+
// strings are converted; JSON is skipped), so it also catches content the
|
|
141
|
+
// AI section builders still emit as HTML — converting it on the next read
|
|
142
|
+
// without a new migration or changes to those builders.
|
|
143
|
+
runAlways: true,
|
|
144
|
+
description: 'BP-5787: convert inline-HTML RTE content to Tiptap JSON on load (lazy migrate-on-read for web-renderer; persisted on next maker save; also normalises AI-built HTML)',
|
|
145
|
+
},
|
|
135
146
|
];
|
|
136
147
|
export const GetLatestModelVersion = () => Math.max(...MIGRATION_TABLE.map((x) => x.version));
|
|
137
148
|
export const migrateBrandPageModel = async (brandPageModel, options) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const getRenderText: (text: string) => string;
|
|
2
|
-
export declare const escapeContentText: (text:
|
|
2
|
+
export declare const escapeContentText: (text: unknown) => string | undefined;
|
|
3
3
|
export declare const decodeHtmlEntities: (text: string) => string;
|
|
4
4
|
export declare const unescapeContentText: (text: string) => string;
|
|
5
5
|
export declare const encodeContentText: (text: string | undefined) => string | undefined;
|
|
@@ -1,11 +1,43 @@
|
|
|
1
1
|
import { ESCAPED_NEWLINE_CHARACTER } from '../../constants/ui';
|
|
2
|
+
/**
|
|
3
|
+
* BP-5787 — a Tiptap JSON document is an object with `type: 'doc'`.
|
|
4
|
+
* Inlined here (rather than importing from brand-page-maker) to avoid a
|
|
5
|
+
* circular import: rich-text-editor.util imports getRenderText from this file.
|
|
6
|
+
*/
|
|
7
|
+
const isTiptapDoc = (value) => typeof value === 'object' &&
|
|
8
|
+
value !== null &&
|
|
9
|
+
value.type === 'doc';
|
|
10
|
+
const jsonDocPlainText = (doc) => {
|
|
11
|
+
const parts = [];
|
|
12
|
+
const walk = (node) => {
|
|
13
|
+
if (typeof node?.text === 'string')
|
|
14
|
+
parts.push(node.text);
|
|
15
|
+
node?.content?.forEach(walk);
|
|
16
|
+
};
|
|
17
|
+
walk(doc);
|
|
18
|
+
return parts.join(' ').replace(/\s+/g, ' ').trim();
|
|
19
|
+
};
|
|
2
20
|
export const getRenderText = (text) => {
|
|
21
|
+
// BP-5787: RTE fields may now be Tiptap JSON objects. Non-strings pass
|
|
22
|
+
// through untouched so the renderer (TextWrapper) handles them via its
|
|
23
|
+
// JSON-aware path, instead of throwing on `.replace`.
|
|
24
|
+
if (typeof text !== 'string') {
|
|
25
|
+
return text;
|
|
26
|
+
}
|
|
3
27
|
return text.replace(new RegExp(`(${ESCAPED_NEWLINE_CHARACTER}|\\n)`, 'g'), '<br/>');
|
|
4
28
|
};
|
|
29
|
+
// Accepts a legacy inline-HTML string or a Tiptap JSON document (BP-5787):
|
|
30
|
+
// for JSON it returns the concatenated visible text; for HTML it strips tags.
|
|
5
31
|
export const escapeContentText = (text) => {
|
|
6
32
|
if (!text) {
|
|
7
33
|
return text;
|
|
8
34
|
}
|
|
35
|
+
if (isTiptapDoc(text)) {
|
|
36
|
+
return jsonDocPlainText(text);
|
|
37
|
+
}
|
|
38
|
+
if (typeof text !== 'string') {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
9
41
|
return text
|
|
10
42
|
.replace(/(<([^>]+)>)/gi, '')
|
|
11
43
|
.replace(/(\r\n|\r|\n)/g, ESCAPED_NEWLINE_CHARACTER);
|
|
@@ -150,9 +150,9 @@ const processSocialLinks = (socialLinksContent, vcardLines, addedEntries) => {
|
|
|
150
150
|
});
|
|
151
151
|
};
|
|
152
152
|
const addProfileInfo = (profile, vcardLines) => {
|
|
153
|
-
const name = sanitizeForVCard(profile.name || '');
|
|
154
|
-
const title = sanitizeForVCard(profile.title || '');
|
|
155
|
-
const description = sanitizeForVCard(profile.description || '');
|
|
153
|
+
const name = sanitizeForVCard(getPlainTextFromRTE(profile.name) || '');
|
|
154
|
+
const title = sanitizeForVCard(getPlainTextFromRTE(profile.title) || '');
|
|
155
|
+
const description = sanitizeForVCard(getPlainTextFromRTE(profile.description) || '');
|
|
156
156
|
if (name) {
|
|
157
157
|
const nameParts = name.trim().split(/\s+/);
|
|
158
158
|
const lastName = nameParts.length > 1 ? nameParts.pop() || '' : '';
|
|
@@ -205,6 +205,6 @@ const downloadVCard = (vcard, filename) => {
|
|
|
205
205
|
export const saveContact = (brandPageData) => {
|
|
206
206
|
const vcard = generateVCard(brandPageData);
|
|
207
207
|
const profile = findProfileContent(brandPageData);
|
|
208
|
-
const filename = generateVCardFilename(profile?.name);
|
|
208
|
+
const filename = generateVCardFilename(getPlainTextFromRTE(profile?.name));
|
|
209
209
|
downloadVCard(vcard, filename);
|
|
210
210
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ButtonWithModalContent, ProfileContent, TextContent, VideoContent, ImageContent, ContentType, ButtonStyles, LogoStyles, MapContent, ImageGalleryContent, GalleryStyles, MailingListContent, RequestPaymentContent, ShopContent, SupportMeContent, ContactFormContent, BannerSectionContent, BrandPageType, SocialLinksContent, TestimonialSectionContent, AboutSectionContent, DividerSectionContent, ShapesSectionContent, ButtonLinkType, PageHeaderMobile, PageHeaderDesktop, PageFooterMobile, PageFooterDesktop, TextMediaItem, TextShapeItem, TextStyleDeclaration, AspectRatio, LogoImageModel, LogoTemplateImageModel, CookieOptions, BookingSectionContent, MediaTypes, TextStyles, BackgroundStyle, BackgroundShape, PersonalisationRole, SaveContactOptions } from './models';
|
|
1
|
+
import type { ButtonWithModalContent, ProfileContent, TextContent, VideoContent, ImageContent, ContentType, ButtonStyles, LogoStyles, MapContent, ImageGalleryContent, GalleryStyles, MailingListContent, RequestPaymentContent, ShopContent, SupportMeContent, ContactFormContent, BannerSectionContent, BrandPageType, SocialLinksContent, TestimonialSectionContent, AboutSectionContent, DividerSectionContent, ShapesSectionContent, ButtonLinkType, PageHeaderMobile, PageHeaderDesktop, PageFooterMobile, PageFooterDesktop, TextMediaItem, TextShapeItem, TextStyleDeclaration, AspectRatio, LogoImageModel, LogoTemplateImageModel, CookieOptions, BookingSectionContent, MediaTypes, TextStyles, BackgroundStyle, BackgroundShape, PersonalisationRole, SaveContactOptions, RichTextValue } from './models';
|
|
2
2
|
import type { ImagesSectionContentDisplayStyle } from './constants';
|
|
3
3
|
export type ViewModelType = 'button' | 'image' | 'image-gallery' | 'logo' | 'map' | 'mailing-list' | 'profile' | 'support-me' | 'request-payment' | 'text' | 'video' | 'contact-form' | 'banner-section' | 'images-section' | 'about-section' | 'divider-section' | 'testimonial-section' | 'social-links' | 'shapes-section' | 'shop-section' | 'booking-section';
|
|
4
4
|
export interface ContentViewModel {
|
|
@@ -94,8 +94,8 @@ export interface TextImageSectionViewModel {
|
|
|
94
94
|
id: string;
|
|
95
95
|
type: 'images-section';
|
|
96
96
|
role: ContentType;
|
|
97
|
-
title:
|
|
98
|
-
description:
|
|
97
|
+
title: RichTextValue;
|
|
98
|
+
description: RichTextValue;
|
|
99
99
|
displayStyle: ImagesSectionContentDisplayStyle;
|
|
100
100
|
aspectRatio: AspectRatio;
|
|
101
101
|
areImagesFullWidth: boolean;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BP-5787 — golden-master fixture for the RTE HTML→JSON migration.
|
|
3
|
+
*
|
|
4
|
+
* A single rich-text value that exercises every mark/node the editor can emit,
|
|
5
|
+
* so one snapshot guards the whole serialisation surface. Captured as a baseline
|
|
6
|
+
* on the current HTML pipeline; after the migration the JSON pipeline must
|
|
7
|
+
* reproduce byte-identical rendered output (see rte-render-parity.test.ts).
|
|
8
|
+
*
|
|
9
|
+
* Coverage:
|
|
10
|
+
* - block nodes: heading (h2), paragraph, text-align (center / right)
|
|
11
|
+
* - inline marks: bold, italic, underline, strike
|
|
12
|
+
* - textStyle attrs: fontFamily, fontSize (em), letterSpacing (em), lineHeight (unitless)
|
|
13
|
+
* - colour: theme TOKEN ref (var(--logo-text-color)) AND a literal hex (#FF0000)
|
|
14
|
+
* - highlight (background colour via <mark>)
|
|
15
|
+
* - links: internal (target=_self) and external (target=_blank rel=noopener)
|
|
16
|
+
*/
|
|
17
|
+
export declare const COMPLEX_RTE_HTML = "<h2 style=\"text-align: center\"><span style=\"color: var(--logo-text-color)\">Themed heading</span></h2><p><strong>bold</strong> <em>italic</em> <u>underline</u> <s>strike</s> <span style=\"font-family: Montserrat; font-size: 1.5em; letter-spacing: 0.05em; line-height: 1.4\">styled span</span> <mark data-color=\"#ffe08a\" style=\"background-color: #ffe08a\">highlight</mark> <span style=\"color: #FF0000\">literal hex</span> <a target=\"_self\" href=\"/about\">internal link</a> <a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://example.com\">external link</a></p><p style=\"text-align: right\">second paragraph, right-aligned</p>";
|
|
18
|
+
/**
|
|
19
|
+
* The theme-token CSS variable that MUST survive the HTML→JSON round-trip
|
|
20
|
+
* (BP-5787 stores token refs instead of flattening them to rgb()).
|
|
21
|
+
*/
|
|
22
|
+
export declare const THEME_TOKEN_REF = "var(--logo-text-color)";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BP-5787 — golden-master fixture for the RTE HTML→JSON migration.
|
|
3
|
+
*
|
|
4
|
+
* A single rich-text value that exercises every mark/node the editor can emit,
|
|
5
|
+
* so one snapshot guards the whole serialisation surface. Captured as a baseline
|
|
6
|
+
* on the current HTML pipeline; after the migration the JSON pipeline must
|
|
7
|
+
* reproduce byte-identical rendered output (see rte-render-parity.test.ts).
|
|
8
|
+
*
|
|
9
|
+
* Coverage:
|
|
10
|
+
* - block nodes: heading (h2), paragraph, text-align (center / right)
|
|
11
|
+
* - inline marks: bold, italic, underline, strike
|
|
12
|
+
* - textStyle attrs: fontFamily, fontSize (em), letterSpacing (em), lineHeight (unitless)
|
|
13
|
+
* - colour: theme TOKEN ref (var(--logo-text-color)) AND a literal hex (#FF0000)
|
|
14
|
+
* - highlight (background colour via <mark>)
|
|
15
|
+
* - links: internal (target=_self) and external (target=_blank rel=noopener)
|
|
16
|
+
*/
|
|
17
|
+
export const COMPLEX_RTE_HTML = `<h2 style="text-align: center"><span style="color: var(--logo-text-color)">Themed heading</span></h2><p><strong>bold</strong> <em>italic</em> <u>underline</u> <s>strike</s> <span style="font-family: Montserrat; font-size: 1.5em; letter-spacing: 0.05em; line-height: 1.4">styled span</span> <mark data-color="#ffe08a" style="background-color: #ffe08a">highlight</mark> <span style="color: #FF0000">literal hex</span> <a target="_self" href="/about">internal link</a> <a target="_blank" rel="noopener noreferrer nofollow" href="https://example.com">external link</a></p><p style="text-align: right">second paragraph, right-aligned</p>`;
|
|
18
|
+
/**
|
|
19
|
+
* The theme-token CSS variable that MUST survive the HTML→JSON round-trip
|
|
20
|
+
* (BP-5787 stores token refs instead of flattening them to rgb()).
|
|
21
|
+
*/
|
|
22
|
+
export const THEME_TOKEN_REF = 'var(--logo-text-color)';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Extensions } from '@tiptap/vue-3';
|
|
2
|
+
/**
|
|
3
|
+
* Single source of truth for the Tiptap extension set (BP-5787).
|
|
4
|
+
*
|
|
5
|
+
* The live editor, the headless serialiser (useExternalRichTextEditor), and
|
|
6
|
+
* the upcoming render-from-JSON path must all use the SAME extensions, or
|
|
7
|
+
* rendered output drifts from what the user authored. This factory removes the
|
|
8
|
+
* previous duplication while preserving each consumer's exact configuration.
|
|
9
|
+
*
|
|
10
|
+
* Known, intentional differences are parameterised:
|
|
11
|
+
* - `restrictStarterKit` — the live editor disables list/hr/code nodes.
|
|
12
|
+
* - `includeLinkRel` — the headless/render path emits rel="noopener noreferrer";
|
|
13
|
+
* the live editing surface does not.
|
|
14
|
+
* - `editing` — only the live editor carries the Placeholder + MaxLength plugins
|
|
15
|
+
* (neither affects serialised output; they are editing-time only).
|
|
16
|
+
*/
|
|
17
|
+
export interface RteExtensionOptions {
|
|
18
|
+
/** Disable StarterKit nodes the editor doesn't use (lists, hr, code). */
|
|
19
|
+
restrictStarterKit?: boolean;
|
|
20
|
+
/** Emit rel="noopener noreferrer" on link HTMLAttributes (render path). */
|
|
21
|
+
includeLinkRel?: boolean;
|
|
22
|
+
/** When present, add the live-editing-only plugins (Placeholder, MaxLength). */
|
|
23
|
+
editing?: {
|
|
24
|
+
placeholder?: string;
|
|
25
|
+
maxLength?: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export declare const getRteExtensions: (options?: RteExtensionOptions) => Extensions;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
2
|
+
import Underline from '@tiptap/extension-underline';
|
|
3
|
+
import TextStyle from '@tiptap/extension-text-style';
|
|
4
|
+
import TextAlign from '@tiptap/extension-text-align';
|
|
5
|
+
import FontFamily from '@tiptap/extension-font-family';
|
|
6
|
+
import ColorExtension from '@tiptap/extension-color';
|
|
7
|
+
import Highlight from '@tiptap/extension-highlight';
|
|
8
|
+
import Placeholder from '@tiptap/extension-placeholder';
|
|
9
|
+
import Link from '@tiptap/extension-link';
|
|
10
|
+
import { FontSize } from './extensions/FontSize';
|
|
11
|
+
import { LetterSpacing } from './extensions/LetterSpacing';
|
|
12
|
+
import { LineHeight } from './extensions/LineHeight';
|
|
13
|
+
import { MaxLength } from './extensions/MaxLength';
|
|
14
|
+
import { AutoLink } from './extensions/AutoLink';
|
|
15
|
+
import { isHrefPageId } from './hyperlink';
|
|
16
|
+
export const getRteExtensions = (options = {}) => {
|
|
17
|
+
const { restrictStarterKit = false, includeLinkRel = false, editing, } = options;
|
|
18
|
+
const extensions = [
|
|
19
|
+
restrictStarterKit
|
|
20
|
+
? StarterKit.configure({
|
|
21
|
+
bulletList: false,
|
|
22
|
+
orderedList: false,
|
|
23
|
+
listItem: false,
|
|
24
|
+
horizontalRule: false,
|
|
25
|
+
codeBlock: false,
|
|
26
|
+
code: false,
|
|
27
|
+
})
|
|
28
|
+
: StarterKit,
|
|
29
|
+
Underline,
|
|
30
|
+
TextStyle.configure({ mergeNestedSpanStyles: true }),
|
|
31
|
+
TextAlign.configure({
|
|
32
|
+
types: ['heading', 'paragraph'],
|
|
33
|
+
}),
|
|
34
|
+
FontFamily.configure({
|
|
35
|
+
types: ['textStyle'],
|
|
36
|
+
}),
|
|
37
|
+
FontSize,
|
|
38
|
+
LetterSpacing,
|
|
39
|
+
LineHeight,
|
|
40
|
+
ColorExtension.configure({
|
|
41
|
+
types: ['textStyle'],
|
|
42
|
+
}),
|
|
43
|
+
Highlight.configure({
|
|
44
|
+
multicolor: true,
|
|
45
|
+
}),
|
|
46
|
+
];
|
|
47
|
+
// Live-editing-only plugins sit between Highlight and Link (original order).
|
|
48
|
+
if (editing) {
|
|
49
|
+
extensions.push(Placeholder.configure({ placeholder: editing.placeholder }), MaxLength.configure({ maxLength: editing.maxLength }));
|
|
50
|
+
}
|
|
51
|
+
extensions.push(Link.configure({
|
|
52
|
+
openOnClick: false,
|
|
53
|
+
defaultProtocol: 'https',
|
|
54
|
+
autolink: true,
|
|
55
|
+
HTMLAttributes: (attrs) => {
|
|
56
|
+
const href = attrs.href;
|
|
57
|
+
const isExternal = href && (href.startsWith('http://') || href.startsWith('https://'));
|
|
58
|
+
return {
|
|
59
|
+
...(includeLinkRel && { rel: 'noopener noreferrer' }),
|
|
60
|
+
target: isExternal ? '_blank' : null,
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
isAllowedUri: (url, ctx) => ctx.defaultValidate(url) || isHrefPageId(url),
|
|
64
|
+
}), AutoLink);
|
|
65
|
+
return extensions;
|
|
66
|
+
};
|