@designcrowd/library.brand-page 6.0.6-json-prereq-7 → 6.0.6-undo-2

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.
Files changed (134) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-SPVLPi8n.js → CreatedOnBrandCrowd-de-de-BSFTp6yl.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-CvtrCPHd.js → CreatedOnBrandCrowd-en-us-rPQrOY1L.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-BSsac7jB.js → CreatedOnBrandCrowd-es-es-awd8-QFa.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-BauENV2a.js → CreatedOnBrandCrowd-fr-ca-CwJ4J7Ow.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-BauENV2a.js → CreatedOnBrandCrowd-fr-fr-CwJ4J7Ow.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-Be0hawmr.js → CreatedOnBrandCrowd-pt-br-BXYfTLlD.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-Be0hawmr.js → CreatedOnBrandCrowd-pt-pt-BXYfTLlD.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-CTjE2Tmn.js → CreatedOnDcom-de-de-kX6u1lNK.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-DnZRp5Pg.js → CreatedOnDcom-en-us-Bkq5sgFs.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-J-9qvjy8.js → CreatedOnDcom-es-es-zCAC9ujj.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-BDNHqm4G.js → CreatedOnDcom-fr-ca-BYz6Wzg5.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-B_2-ceuO.js → CreatedOnDcom-fr-fr-CN-wAaSH.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-76lJVrAY.js → CreatedOnDcom-pt-br-BrK4Ngg-.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-TQp3CNcu.js → CreatedOnDcom-pt-pt-AfI_g7xJ.js} +1 -1
  15. package/lib/src/brand-page/models/index.d.ts +26 -34
  16. package/lib/src/brand-page/utils/logo.util.d.ts +14 -0
  17. package/lib/src/brand-page/utils/logo.util.js +19 -0
  18. package/lib/src/brand-page/utils/migration.util.js +0 -11
  19. package/lib/src/brand-page/utils/text.util.d.ts +1 -1
  20. package/lib/src/brand-page/utils/text.util.js +0 -32
  21. package/lib/src/brand-page/utils/vcard.util.js +4 -4
  22. package/lib/src/brand-page/utils/view-model.util.d.ts +0 -1
  23. package/lib/src/brand-page/utils/view-model.util.js +20 -68
  24. package/lib/src/brand-page/view-models.d.ts +3 -3
  25. package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.d.ts +1 -1
  26. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +1 -1
  27. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.js +1 -1
  28. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMakerTemplateSettings.mixin.d.ts +1 -1
  29. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +1 -1
  30. package/lib/src/brand-page-maker/components/brandpage-settings/ReplaceImageSettingsForBrandPageSettings.mixin.d.ts +1 -1
  31. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +1 -1
  32. package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +1 -1
  33. package/lib/src/brand-page-maker/components/social-share/SocialShare.mixin.d.ts +1 -1
  34. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +1 -1
  35. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +115 -20
  36. package/lib/src/brand-page-maker/composables/useUndoRedoFlag.d.ts +6 -7
  37. package/lib/src/brand-page-maker/composables/useUndoRedoFlag.js +7 -12
  38. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.d.ts +1 -1
  39. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +12 -7
  40. package/lib/src/brand-page-maker/stores/history/history-store.d.ts +6 -0
  41. package/lib/src/brand-page-maker/stores/history/history-store.js +11 -1
  42. package/lib/src/brand-page-maker/utils/index.d.ts +0 -2
  43. package/lib/src/brand-page-maker/utils/index.js +0 -2
  44. package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +5 -19
  45. package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +5 -35
  46. package/lib/src/brand-page-maker/validators/field.validator.js +2 -10
  47. package/lib/src/css/library.brand-page-brandCrowd.min.css +1 -1
  48. package/lib/src/css/library.brand-page-brandPage.min.css +1 -1
  49. package/lib/src/css/library.brand-page-designCom.min.css +1 -1
  50. package/lib/src/css/library.brand-page-preview.min.css +1 -1
  51. package/lib/src/index.d.ts +0 -1
  52. package/lib/src/index.mjs +145 -150
  53. package/lib/src/{lib-Bh9Y5_-x.js → lib-BYFWAvIq.js} +55864 -56119
  54. package/lib/src/lib.js +0 -2
  55. package/lib/src/metadata-builder/builders/reorder-pages-builder.d.ts +13 -0
  56. package/lib/src/metadata-builder/builders/reorder-pages-builder.js +49 -0
  57. package/lib/src/metadata-builder/builders/update-content-text-builder.js +15 -20
  58. package/lib/src/metadata-builder/builders/update-content-text-style-builder.js +3 -5
  59. package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +1 -1
  60. package/lib/src/metadata-builder/helpers/rte.helpers.js +3 -6
  61. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +1 -14
  62. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +1 -1
  63. package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +1 -1
  64. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +1 -1
  65. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +3 -12
  66. package/lib/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.d.ts +1 -0
  67. package/lib/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.js +18 -7
  68. package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +1 -1
  69. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +1 -1
  70. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +1 -1
  71. package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +1 -1
  72. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +1 -1
  73. package/lib/src/utils/minify-brand-page-metadata.js +1 -4
  74. package/package.json +1 -1
  75. package/src/brand-page/components/text-wrapper/TextWrapper.vue +6 -17
  76. package/src/brand-page/models/index.ts +26 -35
  77. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
  78. package/src/brand-page/utils/logo.util.test.ts +54 -1
  79. package/src/brand-page/utils/logo.util.ts +23 -0
  80. package/src/brand-page/utils/migration.util.ts +0 -12
  81. package/src/brand-page/utils/style.util.test.ts +0 -51
  82. package/src/brand-page/utils/text.util.ts +4 -43
  83. package/src/brand-page/utils/vcard.util.ts +4 -6
  84. package/src/brand-page/utils/view-model.util.ts +26 -94
  85. package/src/brand-page/view-models.ts +2 -3
  86. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.ts +1 -1
  87. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +123 -22
  88. package/src/brand-page-maker/composables/useUndoRedoFlag.ts +7 -13
  89. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.test.ts +53 -0
  90. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +13 -16
  91. package/src/brand-page-maker/stores/history/history-store.test.ts +51 -0
  92. package/src/brand-page-maker/stores/history/history-store.ts +19 -0
  93. package/src/brand-page-maker/utils/index.ts +0 -2
  94. package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +3 -4
  95. package/src/brand-page-maker/utils/rich-text-editor.util.ts +11 -50
  96. package/src/brand-page-maker/validators/field.validator.ts +2 -10
  97. package/src/lib.ts +0 -9
  98. package/src/metadata-builder/builders/reorder-pages-builder.ts +80 -0
  99. package/src/metadata-builder/builders/update-content-text-builder.ts +18 -32
  100. package/src/metadata-builder/builders/update-content-text-style-builder.ts +4 -6
  101. package/src/metadata-builder/helpers/rte.helpers.ts +7 -11
  102. package/src/shared/content-settings/components/GenericTextInput.mixin.ts +1 -17
  103. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +3 -16
  104. package/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.ts +19 -11
  105. package/src/utils/minify-brand-page-metadata.ts +1 -4
  106. package/lib/src/brand-page/utils/migration/migration-18.d.ts +0 -2
  107. package/lib/src/brand-page/utils/migration/migration-18.js +0 -71
  108. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.d.ts +0 -22
  109. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.js +0 -22
  110. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.d.ts +0 -14
  111. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.js +0 -39
  112. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +0 -28
  113. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.js +0 -66
  114. package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +0 -33
  115. package/lib/src/brand-page-maker/utils/rte-render.util.js +0 -50
  116. package/lib/src/brand-page-maker/utils/rte-sanitize.util.d.ts +0 -6
  117. package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +0 -158
  118. package/lib/src/brand-page-maker/utils/rte-serializer.d.ts +0 -5
  119. package/lib/src/brand-page-maker/utils/rte-serializer.js +0 -112
  120. package/src/brand-page/utils/migration/migration-18.test.ts +0 -72
  121. package/src/brand-page/utils/migration/migration-18.ts +0 -83
  122. package/src/brand-page/utils/text.util.test.ts +0 -47
  123. package/src/brand-page/utils/view-model.pageid-rte.test.ts +0 -104
  124. package/src/brand-page-maker/__fixtures__/complex-rte.fixture.ts +0 -23
  125. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.test.ts +0 -52
  126. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.ts +0 -42
  127. package/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.ts +0 -113
  128. package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +0 -200
  129. package/src/brand-page-maker/utils/rte-render-parity.test.ts +0 -156
  130. package/src/brand-page-maker/utils/rte-render.util.ts +0 -77
  131. package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +0 -158
  132. package/src/brand-page-maker/utils/rte-sanitize.util.ts +0 -195
  133. package/src/brand-page-maker/utils/rte-serializer.ts +0 -172
  134. package/src/brand-page-maker/validators/field.validator.test.ts +0 -55
@@ -12,7 +12,12 @@ import {
12
12
  PageHeaderMobile,
13
13
  PageStyles,
14
14
  } from '../models';
15
- import type { BrandPageModel, LogoContent, PageModel } from '../models';
15
+ import type {
16
+ BrandPageModel,
17
+ BrandPageType,
18
+ LogoContent,
19
+ PageModel,
20
+ } from '../models';
16
21
  import * as logoUtil from './logo.util';
17
22
 
18
23
  interface GetLogoVariationIndexFromLogoImageUrlTestCase {
@@ -212,4 +217,52 @@ describe('logo-util', () => {
212
217
  logoImage.logoDraftToken,
213
218
  );
214
219
  });
220
+
221
+ describe('getLogoImageFromModel', () => {
222
+ // Distinct tokens in the content block vs the header so each assertion
223
+ // proves which location the helper read from.
224
+ const buildModel = (brandPageType: BrandPageType): BrandPageModel => {
225
+ const model = createTestBrandPageModel();
226
+
227
+ model.brandPageType = brandPageType;
228
+ (model.pages[0].contents[0] as LogoContent).logoImage = {
229
+ logoDraftToken: 'content-token',
230
+ logoVariation: 0,
231
+ logoLastUpdate: undefined,
232
+ };
233
+ model.pageHeaderDesktop.logoImage = {
234
+ logoDraftToken: 'header-token',
235
+ logoVariation: 0,
236
+ logoLastUpdate: undefined,
237
+ };
238
+
239
+ return model;
240
+ };
241
+
242
+ test('reads the header logo for brand-site', () => {
243
+ const model = buildModel(BRAND_PAGE_TYPES.brandSite);
244
+
245
+ expect(logoUtil.getLogoImageFromModel(model)?.logoDraftToken).toBe(
246
+ 'header-token',
247
+ );
248
+ });
249
+
250
+ test('reads the content-block logo for brand-page', () => {
251
+ const model = buildModel(BRAND_PAGE_TYPES.default);
252
+
253
+ expect(logoUtil.getLogoImageFromModel(model)?.logoDraftToken).toBe(
254
+ 'content-token',
255
+ );
256
+ });
257
+
258
+ // Regression guard: brand-contact previously fell through to the header
259
+ // branch (the `=== brandPage` check) and read a stale header logo on undo.
260
+ test('reads the content-block logo for brand-contact', () => {
261
+ const model = buildModel(BRAND_PAGE_TYPES.brandContact);
262
+
263
+ expect(logoUtil.getLogoImageFromModel(model)?.logoDraftToken).toBe(
264
+ 'content-token',
265
+ );
266
+ });
267
+ });
215
268
  });
@@ -394,6 +394,29 @@ export const calculateOptimizedLogoDimensions = (
394
394
  return { width: currentWidth, height: currentHeight };
395
395
  };
396
396
 
397
+ /**
398
+ * Resolves the "current" logo image from a brand-page MODEL (the raw,
399
+ * deserialized shape — not the live stores, whose contents are wrapped in
400
+ * `settings.content`).
401
+ *
402
+ * Brand-site keeps its logo in the page header; brand-page and brand-contact
403
+ * keep it as a logo CONTENT block on the home page — the same `isBrandSite ?
404
+ * header : content` split the logo-settings store getters/setter use. This is
405
+ * the single source of truth for model-level logo reads: initial load and
406
+ * history-restore both call it so the two paths can't drift. They previously
407
+ * hard-coded `=== brandPage`, which silently excluded brand-contact and read a
408
+ * stale header logo on undo.
409
+ */
410
+ export const getLogoImageFromModel = (
411
+ model: BrandPageModel,
412
+ ): LogoImageModel | undefined => {
413
+ if (model.brandPageType === BRAND_PAGE_TYPES.brandSite) {
414
+ return model.pageHeaderDesktop?.logoImage;
415
+ }
416
+
417
+ return model.pages?.[0]?.contents?.find(isLogoContent)?.logoImage;
418
+ };
419
+
397
420
  export const recalculateLogoContentDimensions = async (
398
421
  pageData: any,
399
422
  editableLogoVariations: {
@@ -20,7 +20,6 @@ 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';
24
23
 
25
24
  export interface MigrationData {
26
25
  brandPageModel: BrandPageModel;
@@ -161,17 +160,6 @@ const MIGRATION_TABLE = [
161
160
  description:
162
161
  '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)',
163
162
  },
164
- {
165
- version: 18,
166
- migrateFn: migration18,
167
- // runAlways: normalise RTE to JSON on every load. Idempotent (only HTML
168
- // strings are converted; JSON is skipped), so it also catches content the
169
- // AI section builders still emit as HTML — converting it on the next read
170
- // without a new migration or changes to those builders.
171
- runAlways: true,
172
- description:
173
- '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)',
174
- },
175
163
  ] as MigrationTableItem[];
176
164
 
177
165
  export const GetLatestModelVersion = () =>
@@ -87,57 +87,6 @@ describe('style.util', () => {
87
87
  );
88
88
  });
89
89
 
90
- test('applyLogoThemeToBrandPageModel preserves var() token refs in JSON RTE content (BP-5787)', () => {
91
- // Arrange — title stored as Tiptap JSON with a theme-token colour mark.
92
- const logoTheme = logoWithFlatColorsTheme;
93
- const titleDoc = {
94
- type: 'doc',
95
- content: [
96
- {
97
- type: 'heading',
98
- attrs: { level: 2, textAlign: 'center' },
99
- content: [
100
- {
101
- type: 'text',
102
- marks: [
103
- {
104
- type: 'textStyle',
105
- attrs: { color: `var(--${STYLE_THEME_KEYS.LogoTextColor})` },
106
- },
107
- ],
108
- text: 'Themed',
109
- },
110
- ],
111
- },
112
- ],
113
- };
114
- const brandPageModel = new BrandPageModel({
115
- ...BRAND_SITE,
116
- pages: [
117
- {
118
- title: '1',
119
- slug: '1',
120
- contents: [
121
- {
122
- ...createInitialBannerSectionContent(),
123
- title: titleDoc,
124
- } as any,
125
- ],
126
- },
127
- ],
128
- });
129
-
130
- // Act
131
- const { model } = applyLogoThemeToBrandPageModel(logoTheme, brandPageModel);
132
-
133
- // Assert — JSON RTE content is an object, so the var() token is NOT
134
- // flattened to rgb(); it survives so a new theme can still propagate.
135
- const title = JSON.stringify((model.pages[0].contents[0] as any).title);
136
-
137
- expect(title).toContain(`var(--${STYLE_THEME_KEYS.LogoTextColor})`);
138
- expect(title).not.toContain(logoTheme.logoTextColor.rgb);
139
- });
140
-
141
90
  test('applyLogoThemeToBrandPageModel should apply logo theme color to content color when content color has a color matched theme key', () => {
142
91
  // Arrange
143
92
  const logoTheme = logoWithFlatColorsTheme;
@@ -1,56 +1,17 @@
1
1
  import { ESCAPED_NEWLINE_CHARACTER } from '../../constants/ui';
2
2
 
3
- /**
4
- * BP-5787 — a Tiptap JSON document is an object with `type: 'doc'`.
5
- * Inlined here (rather than importing from brand-page-maker) to avoid a
6
- * circular import: rich-text-editor.util imports getRenderText from this file.
7
- */
8
- const isTiptapDoc = (value: unknown): value is { type: 'doc' } =>
9
- typeof value === 'object' &&
10
- value !== null &&
11
- (value as { type?: string }).type === 'doc';
12
-
13
- const jsonDocPlainText = (doc: unknown): string => {
14
- const parts: string[] = [];
15
-
16
- const walk = (node: any): void => {
17
- if (typeof node?.text === 'string') parts.push(node.text);
18
-
19
- node?.content?.forEach(walk);
20
- };
21
-
22
- walk(doc);
23
-
24
- return parts.join(' ').replace(/\s+/g, ' ').trim();
25
- };
26
-
27
3
  export const getRenderText = (text: string): string => {
28
- // BP-5787: RTE fields may now be Tiptap JSON objects. Non-strings pass
29
- // through untouched so the renderer (TextWrapper) handles them via its
30
- // JSON-aware path, instead of throwing on `.replace`.
31
- if (typeof text !== 'string') {
32
- return text as unknown as string;
33
- }
34
-
35
4
  return text.replace(
36
5
  new RegExp(`(${ESCAPED_NEWLINE_CHARACTER}|\\n)`, 'g'),
37
6
  '<br/>',
38
7
  );
39
8
  };
40
9
 
41
- // Accepts a legacy inline-HTML string or a Tiptap JSON document (BP-5787):
42
- // for JSON it returns the concatenated visible text; for HTML it strips tags.
43
- export const escapeContentText = (text: unknown): string | undefined => {
10
+ export const escapeContentText = (
11
+ text: string | undefined,
12
+ ): string | undefined => {
44
13
  if (!text) {
45
- return text as undefined;
46
- }
47
-
48
- if (isTiptapDoc(text)) {
49
- return jsonDocPlainText(text);
50
- }
51
-
52
- if (typeof text !== 'string') {
53
- return undefined;
14
+ return text;
54
15
  }
55
16
 
56
17
  return text
@@ -235,11 +235,9 @@ const addProfileInfo = (
235
235
  profile: ProfileViewModel,
236
236
  vcardLines: string[],
237
237
  ): void => {
238
- const name = sanitizeForVCard(getPlainTextFromRTE(profile.name) || '');
239
- const title = sanitizeForVCard(getPlainTextFromRTE(profile.title) || '');
240
- const description = sanitizeForVCard(
241
- getPlainTextFromRTE(profile.description) || '',
242
- );
238
+ const name = sanitizeForVCard(profile.name || '');
239
+ const title = sanitizeForVCard(profile.title || '');
240
+ const description = sanitizeForVCard(profile.description || '');
243
241
 
244
242
  if (name) {
245
243
  const nameParts = name.trim().split(/\s+/);
@@ -318,7 +316,7 @@ export const saveContact = (brandPageData: {
318
316
  }): void => {
319
317
  const vcard = generateVCard(brandPageData);
320
318
  const profile = findProfileContent(brandPageData);
321
- const filename = generateVCardFilename(getPlainTextFromRTE(profile?.name));
319
+ const filename = generateVCardFilename(profile?.name);
322
320
 
323
321
  downloadVCard(vcard, filename);
324
322
  };
@@ -119,7 +119,7 @@ import {
119
119
  getRouteNameByPageIndex,
120
120
  getRouteParamsByPage,
121
121
  } from './route.util';
122
- import { hasRTEContent, isTiptapDocument } from '../../brand-page-maker/utils';
122
+ import { hasRTEContent } from '../../brand-page-maker/utils';
123
123
  import {
124
124
  getPageIdFromHref,
125
125
  isHrefPageId,
@@ -1518,86 +1518,6 @@ const mapPageIdReferencesToRelativePaths = (
1518
1518
  });
1519
1519
  };
1520
1520
 
1521
- // Resolve a `pageid:<id>` internal-link href to the published relative path.
1522
- // Returns the resolved href, or null when `href` is not an internal pageid link
1523
- // (external/absolute links are left untouched). Falls back to the site root when
1524
- // the pageid can't be matched to a nav route.
1525
- const resolvePageIdHref = (
1526
- href: string,
1527
- nav: NavViewModel,
1528
- options: ViewModelMappingOptions,
1529
- ): string | null => {
1530
- // Only process internal links (no protocol, doesn't start with /)
1531
- if (!href || href.match(/^(https?:\/\/|\/)/) || !isHrefPageId(href)) {
1532
- return null;
1533
- }
1534
-
1535
- const pageId = getPageIdFromHref(href);
1536
- const route = findNavItemByPageId(nav.navItems, pageId);
1537
- const brandPageSlugRoute = options.brandPageSlug
1538
- ? `/${options.brandPageSlug}`
1539
- : '/';
1540
-
1541
- if (route) {
1542
- if (route.isRoot) {
1543
- return brandPageSlugRoute;
1544
- }
1545
-
1546
- return `${brandPageSlugRoute}${brandPageSlugRoute === '/' ? '' : '/'}${route.relativePath}`;
1547
- }
1548
-
1549
- // fallback to root if pageid cannot be resolved.
1550
- return brandPageSlugRoute;
1551
- };
1552
-
1553
- // BP-5847: walk a Tiptap JSON doc and rewrite every link mark whose href is a
1554
- // `pageid:` reference into its published relative path. Returns a NEW doc when
1555
- // something changed (so the shared model doc is never mutated), else the same
1556
- // reference.
1557
- export const resolvePageIdsInRteJson = (
1558
- doc: unknown,
1559
- nav: NavViewModel,
1560
- options: ViewModelMappingOptions,
1561
- ): unknown => {
1562
- let changed = false;
1563
-
1564
- const walk = (node: any): any => {
1565
- if (!node || typeof node !== 'object') {
1566
- return node;
1567
- }
1568
-
1569
- let next = node;
1570
-
1571
- if (Array.isArray(node.marks)) {
1572
- const marks = node.marks.map((mark: any) => {
1573
- if (mark?.type === 'link' && typeof mark.attrs?.href === 'string') {
1574
- const resolved = resolvePageIdHref(mark.attrs.href, nav, options);
1575
-
1576
- if (resolved !== null) {
1577
- changed = true;
1578
-
1579
- return { ...mark, attrs: { ...mark.attrs, href: resolved } };
1580
- }
1581
- }
1582
-
1583
- return mark;
1584
- });
1585
-
1586
- next = { ...next, marks };
1587
- }
1588
-
1589
- if (Array.isArray(node.content)) {
1590
- next = { ...next, content: node.content.map(walk) };
1591
- }
1592
-
1593
- return next;
1594
- };
1595
-
1596
- const result = walk(doc);
1597
-
1598
- return changed ? result : doc;
1599
- };
1600
-
1601
1521
  const dynamicallyMapPageIdInHyperlinksToRelativePaths = (
1602
1522
  content: any,
1603
1523
  nav: NavViewModel,
@@ -1610,15 +1530,7 @@ const dynamicallyMapPageIdInHyperlinksToRelativePaths = (
1610
1530
  return;
1611
1531
  }
1612
1532
 
1613
- if (isTiptapDocument(value)) {
1614
- // BP-5847: RTE fields may now be Tiptap JSON. Resolve pageid link marks
1615
- // here too, or internal links render as raw `pageid:` and don't navigate.
1616
- (content as unknown as Record<string, unknown>)[key] =
1617
- resolvePageIdsInRteJson(value, nav, options);
1618
- } else if (
1619
- typeof value === 'object' &&
1620
- (key === 'items' || key === 'textItems')
1621
- ) {
1533
+ if (typeof value === 'object' && (key === 'items' || key === 'textItems')) {
1622
1534
  if (Array.isArray(value)) {
1623
1535
  value.forEach((item) => {
1624
1536
  dynamicallyMapPageIdInHyperlinksToRelativePaths(item, nav, options);
@@ -1630,11 +1542,31 @@ const dynamicallyMapPageIdInHyperlinksToRelativePaths = (
1630
1542
  (content as unknown as Record<string, unknown>)[key] = value.replace(
1631
1543
  /<a\s+([^>]*?)href="([^"]*?)"([^>]*?)>/gi,
1632
1544
  (match, existingAttrsStart, href, existingAttrsEnd) => {
1633
- const resolved = resolvePageIdHref(href, nav, options);
1545
+ // Only process internal links (no protocol, doesn't start with /)
1546
+ if (href && !href.match(/^(https?:\/\/|\/)/) && isHrefPageId(href)) {
1547
+ const pageId = getPageIdFromHref(href);
1548
+
1549
+ const route = findNavItemByPageId(nav.navItems, pageId);
1550
+
1551
+ const brandPageSlugRoute = options.brandPageSlug
1552
+ ? `/${options.brandPageSlug}`
1553
+ : '/';
1554
+
1555
+ if (route) {
1556
+ let href = `${brandPageSlugRoute}`;
1557
+
1558
+ if (!route.isRoot) {
1559
+ href = `${brandPageSlugRoute}${brandPageSlugRoute === '/' ? '' : '/'}${route.relativePath}`;
1560
+ }
1561
+
1562
+ return `<a ${existingAttrsStart}href="${href}"${existingAttrsEnd}>`;
1563
+ }
1564
+
1565
+ // fallback to root if pageid cannot be resolved.
1566
+ return `<a ${existingAttrsStart}href="${brandPageSlugRoute}"${existingAttrsEnd}>`;
1567
+ }
1634
1568
 
1635
- return resolved === null
1636
- ? match
1637
- : `<a ${existingAttrsStart}href="${resolved}"${existingAttrsEnd}>`;
1569
+ return match;
1638
1570
  },
1639
1571
  );
1640
1572
  }
@@ -41,7 +41,6 @@ import type {
41
41
  BackgroundShape,
42
42
  PersonalisationRole,
43
43
  SaveContactOptions,
44
- RichTextValue,
45
44
  } from './models';
46
45
  import type { ImagesSectionContentDisplayStyle } from './constants';
47
46
 
@@ -197,8 +196,8 @@ export interface TextImageSectionViewModel {
197
196
  id: string;
198
197
  type: 'images-section';
199
198
  role: ContentType;
200
- title: RichTextValue;
201
- description: RichTextValue;
199
+ title: string;
200
+ description: string;
202
201
  displayStyle: ImagesSectionContentDisplayStyle;
203
202
  aspectRatio: AspectRatio;
204
203
  areImagesFullWidth: boolean;
@@ -291,7 +291,7 @@ export default defineComponent({
291
291
 
292
292
  // Capture baseline for undo/redo when the feature is active for this
293
293
  // session — see useUndoRedoFlag.ts for the gate (LaunchDarkly flag +
294
- // brand-site + non-admin maker).
294
+ // non-admin maker; all three brands).
295
295
  if (useUndoRedoFlag().value) {
296
296
  useHistoryStore().captureBaseline();
297
297
  }
@@ -8,7 +8,18 @@ import {
8
8
  watch,
9
9
  } from 'vue';
10
10
  import { Editor } from '@tiptap/vue-3';
11
- import { getRteExtensions } from './getRteExtensions';
11
+ import StarterKit from '@tiptap/starter-kit';
12
+ import Underline from '@tiptap/extension-underline';
13
+ import TextStyle from '@tiptap/extension-text-style';
14
+ import TextAlign from '@tiptap/extension-text-align';
15
+ import FontFamily from '@tiptap/extension-font-family';
16
+ import ColorExtension from '@tiptap/extension-color';
17
+ import { FontSize } from './extensions/FontSize';
18
+ import { LetterSpacing } from './extensions/LetterSpacing';
19
+ import { LineHeight } from './extensions/LineHeight';
20
+ import Highlight from '@tiptap/extension-highlight';
21
+ import Placeholder from '@tiptap/extension-placeholder';
22
+ import Link from '@tiptap/extension-link';
12
23
  import type { StyleTextFontFamilyOption } from '@shared/style-settings/models';
13
24
  import { createFormattingActions } from './formatting';
14
25
  import { useTextAlign } from './textAlign';
@@ -20,7 +31,7 @@ import {
20
31
  emToPx,
21
32
  isStyleChanged,
22
33
  pxToEm,
23
- sanitizeRteJson,
34
+ sanitiseClientSideRTE,
24
35
  skipStyleDetection,
25
36
  } from '@brand-page-maker/utils';
26
37
  import type { HyperlinkType, TextStyles } from '@brand-page';
@@ -38,7 +49,9 @@ import {
38
49
  import { useUiContentSettingsStore } from '@shared/content-settings/store';
39
50
  import { useHistoryStore } from '@brand-page-maker/stores/history/history-store';
40
51
  import { AllSelection } from 'prosemirror-state';
41
- import { useHyperlink } from './hyperlink';
52
+ import { MaxLength } from './extensions/MaxLength';
53
+ import { AutoLink } from './extensions/AutoLink';
54
+ import { isHrefPageId, useHyperlink } from './hyperlink';
42
55
  import { useBubbleMenu } from './bubbleMenu';
43
56
  import {
44
57
  hasLogoColorVar,
@@ -116,7 +129,44 @@ export const useExternalRichTextEditor = (): {
116
129
  } => {
117
130
  const externalEditor = new Editor({
118
131
  editable: false,
119
- extensions: getRteExtensions({ includeLinkRel: true }),
132
+ extensions: [
133
+ StarterKit,
134
+ Underline,
135
+ TextStyle.configure({ mergeNestedSpanStyles: true }),
136
+ TextAlign.configure({
137
+ types: ['heading', 'paragraph'],
138
+ }),
139
+ FontFamily.configure({
140
+ types: ['textStyle'],
141
+ }),
142
+ FontSize,
143
+ LetterSpacing,
144
+ LineHeight,
145
+ ColorExtension.configure({
146
+ types: ['textStyle'],
147
+ }),
148
+ Highlight.configure({
149
+ multicolor: true,
150
+ }),
151
+ Link.configure({
152
+ openOnClick: false,
153
+ defaultProtocol: 'https',
154
+ autolink: true,
155
+ HTMLAttributes: (attrs) => {
156
+ const href = attrs.href;
157
+ const isExternal =
158
+ href && (href.startsWith('http://') || href.startsWith('https://'));
159
+
160
+ return {
161
+ rel: 'noopener noreferrer',
162
+ target: isExternal ? '_blank' : null,
163
+ };
164
+ },
165
+ isAllowedUri: (url, ctx) =>
166
+ ctx.defaultValidate(url) || isHrefPageId(url),
167
+ }),
168
+ AutoLink,
169
+ ],
120
170
  });
121
171
 
122
172
  return {
@@ -168,13 +218,54 @@ export const useRichTextEditor = (
168
218
  const editor = new Editor({
169
219
  content: model.value ?? '',
170
220
  editable: !props.readOnly,
171
- extensions: getRteExtensions({
172
- restrictStarterKit: true,
173
- editing: {
221
+ extensions: [
222
+ StarterKit.configure({
223
+ bulletList: false,
224
+ orderedList: false,
225
+ listItem: false,
226
+ horizontalRule: false,
227
+ codeBlock: false,
228
+ code: false,
229
+ }),
230
+ Underline,
231
+ TextStyle.configure({ mergeNestedSpanStyles: true }),
232
+ TextAlign.configure({
233
+ types: ['heading', 'paragraph'],
234
+ }),
235
+ FontFamily.configure({
236
+ types: ['textStyle'],
237
+ }),
238
+ FontSize,
239
+ LetterSpacing,
240
+ LineHeight,
241
+ ColorExtension.configure({
242
+ types: ['textStyle'],
243
+ }),
244
+ Highlight.configure({
245
+ multicolor: true,
246
+ }),
247
+ Placeholder.configure({
174
248
  placeholder: props.placeholder,
175
- maxLength: props.maxLength,
176
- },
177
- }),
249
+ }),
250
+ MaxLength.configure({ maxLength: props.maxLength }),
251
+ Link.configure({
252
+ openOnClick: false,
253
+ defaultProtocol: 'https',
254
+ autolink: true,
255
+ HTMLAttributes: (attrs) => {
256
+ const href = attrs.href;
257
+ const isExternal =
258
+ href && (href.startsWith('http://') || href.startsWith('https://'));
259
+
260
+ return {
261
+ target: isExternal ? '_blank' : null,
262
+ };
263
+ },
264
+ isAllowedUri: (url, ctx) =>
265
+ ctx.defaultValidate(url) || isHrefPageId(url),
266
+ }),
267
+ AutoLink,
268
+ ],
178
269
  editorProps: {
179
270
  handleDOMEvents: {
180
271
  drop(view, event: DragEvent) {
@@ -385,13 +476,7 @@ export const useRichTextEditor = (
385
476
 
386
477
  const isExternalMutation = updateRichTextEditor.value || appliedSinceLast;
387
478
 
388
- // Compare in the stored (JSON) format so a JSON model value isn't always
389
- // seen as "changed" against the editor's HTML (which would re-run
390
- // setContent and reset the user's just-applied styles).
391
- const hasChanged =
392
- JSON.stringify(editor.getJSON()) !== JSON.stringify(newValue);
393
-
394
- if (isExternalMutation && hasChanged) {
479
+ if (isExternalMutation && editor.getHTML() !== newValue) {
395
480
  editor.commands.setContent(newValue, false);
396
481
  updateRichTextEditor.value = false;
397
482
  //text style type is not switching when reordering
@@ -414,12 +499,28 @@ export const useRichTextEditor = (
414
499
  }
415
500
  });
416
501
 
417
- // Update model when editor content changes. RTE content is stored as Tiptap
418
- // JSON (BP-5787). getJSON() is already in hand (the editor holds the doc) and
419
- // is sanitised natively — no getHTML(), no DOMPurify, no extra editor — so
420
- // the per-keystroke path allocates nothing heavy.
502
+ // Update model when editor content changes
421
503
  editor.on('update', () => {
422
- emit('update:modelValue', sanitizeRteJson(editor.getJSON()));
504
+ const sanitizedData = sanitiseClientSideRTE(editor.getHTML());
505
+
506
+ // An update fired while the editor is unfocused is TipTap rewriting the
507
+ // model back into its canonical HTML during mount rehydration or a
508
+ // programmatic setContent — not a user edit. Fold it into the current
509
+ // head so merely selecting a section doesn't push a phantom undo entry
510
+ // (BP-5810). The construction-time arm above lapses before this async
511
+ // write-back fires, so re-arm here. Genuine edits happen while focused and
512
+ // still push a new entry.
513
+ //
514
+ // Known limitation: a programmatic toolbar command run on an editor the
515
+ // user hasn't focused yet would also merge (losing that undo step). Today
516
+ // the RTE only mounts inside an already-open settings drawer and the user
517
+ // focuses it before formatting, so this doesn't occur — revisit if a
518
+ // consumer ever drives toolbar actions on an unfocused editor.
519
+ if (!editor.isFocused && !historyStore.isApplyingSnapshot) {
520
+ historyStore.mergeNextMutation();
521
+ }
522
+
523
+ emit('update:modelValue', sanitizedData.sanitizedHtml);
423
524
  });
424
525
 
425
526
  editor.on('selectionUpdate', () => {
@@ -1,30 +1,24 @@
1
1
  import { computed, type ComputedRef } from 'vue';
2
2
  import { useBrandPageMakerUserStore } from '@brand-page-maker/stores/user';
3
- import { useBrandPageMakerStore } from '@brand-page-maker/stores/brandpage-maker/brandpage-maker-store';
4
3
 
5
4
  /**
6
- * Returns whether undo/redo is active for the current maker session. All
7
- * three conditions must hold:
5
+ * Returns whether undo/redo is active for the current maker session. Both
6
+ * conditions must hold:
8
7
  *
9
8
  * 1. `userStore.features.canUseUndoRedo` is true. The flag is populated by
10
9
  * the consumer (BrandCrowd.Net / web-renderer) via the
11
10
  * `WebsiteDesignUserFeatures.CanUseUndoRedo` boolean on the ViewModel —
12
- * same precedent as RichTextEditor / GettyVideos / AITextGenerator.
13
- * 2. The maker is a brand-site (Website Maker). V1 scope is brand-site
14
- * only; brand-page (single-page profile) and brand-contact (vCard) are
15
- * out of scope.
16
- * 3. The session is NOT in admin-maker mode (`userStore.isAdmin`). The
11
+ * same precedent as RichTextEditor / GettyVideos / AITextGenerator. The
12
+ * one flag governs all three brands (brand-site, brand-page,
13
+ * brand-contact) — there is no per-brand gating.
14
+ * 2. The session is NOT in admin-maker mode (`userStore.isAdmin`). The
17
15
  * admin maker uses the same components but a different editing flow
18
16
  * (template authoring) where undo/redo is out of scope.
19
17
  */
20
18
  export function useUndoRedoFlag(): ComputedRef<boolean> {
21
19
  const userStore = useBrandPageMakerUserStore();
22
- const brandPageMakerStore = useBrandPageMakerStore();
23
20
 
24
21
  return computed(
25
- () =>
26
- userStore.features.canUseUndoRedo &&
27
- brandPageMakerStore.isBrandSite &&
28
- !userStore.isAdmin,
22
+ () => userStore.features.canUseUndoRedo && !userStore.isAdmin,
29
23
  );
30
24
  }