@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
@@ -391,7 +391,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
391
391
  content: TContent;
392
392
  }>(payload: P): Promise<void>;
393
393
  _captureTemplateSwitchIfEnabled(): void;
394
- applyHistorySnapshot(snapshot: import("../../../brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string): Promise<void>;
394
+ applyHistorySnapshot(snapshot: import("../../../brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string, selectedTemplateId?: string | false): Promise<void>;
395
395
  _syncLogoSettingsFromSnapshot(snapshot: import("../../../brand-page").BrandPageModel, logoEditsStr?: string): Promise<void>;
396
396
  _navigateToActivePage(activePageId?: string): void;
397
397
  _fallbackSidebarIfSelectedContentMissing(snapshot: import("../../../brand-page").BrandPageModel): void;
@@ -392,7 +392,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
392
392
  content: TContent;
393
393
  }>(payload: P): Promise<void>;
394
394
  _captureTemplateSwitchIfEnabled(): void;
395
- applyHistorySnapshot(snapshot: import("../../../brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string): Promise<void>;
395
+ applyHistorySnapshot(snapshot: import("../../../brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string, selectedTemplateId?: string | false): Promise<void>;
396
396
  _syncLogoSettingsFromSnapshot(snapshot: import("../../../brand-page").BrandPageModel, logoEditsStr?: string): Promise<void>;
397
397
  _navigateToActivePage(activePageId?: string): void;
398
398
  _fallbackSidebarIfSelectedContentMissing(snapshot: import("../../../brand-page").BrandPageModel): void;
@@ -123,7 +123,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
123
123
  content: TContent;
124
124
  }>(payload: P): Promise<void>;
125
125
  _captureTemplateSwitchIfEnabled(): void;
126
- applyHistorySnapshot(snapshot: import("../../../brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string): Promise<void>;
126
+ applyHistorySnapshot(snapshot: import("../../../brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string, selectedTemplateId?: string | false): Promise<void>;
127
127
  _syncLogoSettingsFromSnapshot(snapshot: import("../../../brand-page").BrandPageModel, logoEditsStr?: string): Promise<void>;
128
128
  _navigateToActivePage(activePageId?: string): void;
129
129
  _fallbackSidebarIfSelectedContentMissing(snapshot: import("../../../brand-page").BrandPageModel): void;
@@ -441,7 +441,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
441
441
  content: TContent;
442
442
  }>(payload: P): Promise<void>;
443
443
  _captureTemplateSwitchIfEnabled(): void;
444
- applyHistorySnapshot(snapshot: import("@brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string): Promise<void>;
444
+ applyHistorySnapshot(snapshot: import("@brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string, selectedTemplateId?: string | false): Promise<void>;
445
445
  _syncLogoSettingsFromSnapshot(snapshot: import("@brand-page").BrandPageModel, logoEditsStr?: string): Promise<void>;
446
446
  _navigateToActivePage(activePageId?: string): void;
447
447
  _fallbackSidebarIfSelectedContentMissing(snapshot: import("@brand-page").BrandPageModel): void;
@@ -1,25 +1,72 @@
1
1
  import { onBeforeUnmount, computed, watch, } from 'vue';
2
2
  import { Editor } from '@tiptap/vue-3';
3
- import { getRteExtensions } from './getRteExtensions';
3
+ import StarterKit from '@tiptap/starter-kit';
4
+ import Underline from '@tiptap/extension-underline';
5
+ import TextStyle from '@tiptap/extension-text-style';
6
+ import TextAlign from '@tiptap/extension-text-align';
7
+ import FontFamily from '@tiptap/extension-font-family';
8
+ import ColorExtension from '@tiptap/extension-color';
9
+ import { FontSize } from './extensions/FontSize';
10
+ import { LetterSpacing } from './extensions/LetterSpacing';
11
+ import { LineHeight } from './extensions/LineHeight';
12
+ import Highlight from '@tiptap/extension-highlight';
13
+ import Placeholder from '@tiptap/extension-placeholder';
14
+ import Link from '@tiptap/extension-link';
4
15
  import { createFormattingActions } from './formatting';
5
16
  import { useTextAlign } from './textAlign';
6
17
  import { useFont } from './font';
7
18
  import { useSpacing } from './spacing';
8
19
  import Color from 'color';
9
- import { buildInitialMarks, emToPx, isStyleChanged, pxToEm, sanitizeRteJson, skipStyleDetection, } from '@brand-page-maker/utils';
20
+ import { buildInitialMarks, emToPx, isStyleChanged, pxToEm, sanitiseClientSideRTE, skipStyleDetection, } from '@brand-page-maker/utils';
10
21
  import { HYPERLINK_TYPE, } from '@brand-page';
11
22
  import { letterSpacingValueConvertor, colorConvertor, backgroundColorConvertor, } from '@brand-page-maker/convertors';
12
23
  import { useUiContentSettingsStore } from '@shared/content-settings/store';
13
24
  import { useHistoryStore } from '@brand-page-maker/stores/history/history-store';
14
25
  import { AllSelection } from 'prosemirror-state';
15
- import { useHyperlink } from './hyperlink';
26
+ import { MaxLength } from './extensions/MaxLength';
27
+ import { AutoLink } from './extensions/AutoLink';
28
+ import { isHrefPageId, useHyperlink } from './hyperlink';
16
29
  import { useBubbleMenu } from './bubbleMenu';
17
30
  import { hasLogoColorVar, isLogoColor, getRTEColor, } from '@brand-page-maker/utils/rich-text-editor.util';
18
31
  import { useEventTracker, EVENTS } from '../useEventTracker';
19
32
  export const useExternalRichTextEditor = () => {
20
33
  const externalEditor = new Editor({
21
34
  editable: false,
22
- extensions: getRteExtensions({ includeLinkRel: true }),
35
+ extensions: [
36
+ StarterKit,
37
+ Underline,
38
+ TextStyle.configure({ mergeNestedSpanStyles: true }),
39
+ TextAlign.configure({
40
+ types: ['heading', 'paragraph'],
41
+ }),
42
+ FontFamily.configure({
43
+ types: ['textStyle'],
44
+ }),
45
+ FontSize,
46
+ LetterSpacing,
47
+ LineHeight,
48
+ ColorExtension.configure({
49
+ types: ['textStyle'],
50
+ }),
51
+ Highlight.configure({
52
+ multicolor: true,
53
+ }),
54
+ Link.configure({
55
+ openOnClick: false,
56
+ defaultProtocol: 'https',
57
+ autolink: true,
58
+ HTMLAttributes: (attrs) => {
59
+ const href = attrs.href;
60
+ const isExternal = href && (href.startsWith('http://') || href.startsWith('https://'));
61
+ return {
62
+ rel: 'noopener noreferrer',
63
+ target: isExternal ? '_blank' : null,
64
+ };
65
+ },
66
+ isAllowedUri: (url, ctx) => ctx.defaultValidate(url) || isHrefPageId(url),
67
+ }),
68
+ AutoLink,
69
+ ],
23
70
  });
24
71
  return {
25
72
  externalEditor,
@@ -33,13 +80,51 @@ export const useRichTextEditor = (model, props, emit, fontSettings, fontStyleDro
33
80
  const editor = new Editor({
34
81
  content: model.value ?? '',
35
82
  editable: !props.readOnly,
36
- extensions: getRteExtensions({
37
- restrictStarterKit: true,
38
- editing: {
83
+ extensions: [
84
+ StarterKit.configure({
85
+ bulletList: false,
86
+ orderedList: false,
87
+ listItem: false,
88
+ horizontalRule: false,
89
+ codeBlock: false,
90
+ code: false,
91
+ }),
92
+ Underline,
93
+ TextStyle.configure({ mergeNestedSpanStyles: true }),
94
+ TextAlign.configure({
95
+ types: ['heading', 'paragraph'],
96
+ }),
97
+ FontFamily.configure({
98
+ types: ['textStyle'],
99
+ }),
100
+ FontSize,
101
+ LetterSpacing,
102
+ LineHeight,
103
+ ColorExtension.configure({
104
+ types: ['textStyle'],
105
+ }),
106
+ Highlight.configure({
107
+ multicolor: true,
108
+ }),
109
+ Placeholder.configure({
39
110
  placeholder: props.placeholder,
40
- maxLength: props.maxLength,
41
- },
42
- }),
111
+ }),
112
+ MaxLength.configure({ maxLength: props.maxLength }),
113
+ Link.configure({
114
+ openOnClick: false,
115
+ defaultProtocol: 'https',
116
+ autolink: true,
117
+ HTMLAttributes: (attrs) => {
118
+ const href = attrs.href;
119
+ const isExternal = href && (href.startsWith('http://') || href.startsWith('https://'));
120
+ return {
121
+ target: isExternal ? '_blank' : null,
122
+ };
123
+ },
124
+ isAllowedUri: (url, ctx) => ctx.defaultValidate(url) || isHrefPageId(url),
125
+ }),
126
+ AutoLink,
127
+ ],
43
128
  editorProps: {
44
129
  handleDOMEvents: {
45
130
  drop(view, event) {
@@ -157,11 +242,7 @@ export const useRichTextEditor = (model, props, emit, fontSettings, fontStyleDro
157
242
  const appliedSinceLast = historyStore.snapshotAppliedToken !== lastAppliedToken;
158
243
  lastAppliedToken = historyStore.snapshotAppliedToken;
159
244
  const isExternalMutation = updateRichTextEditor.value || appliedSinceLast;
160
- // Compare in the stored (JSON) format so a JSON model value isn't always
161
- // seen as "changed" against the editor's HTML (which would re-run
162
- // setContent and reset the user's just-applied styles).
163
- const hasChanged = JSON.stringify(editor.getJSON()) !== JSON.stringify(newValue);
164
- if (isExternalMutation && hasChanged) {
245
+ if (isExternalMutation && editor.getHTML() !== newValue) {
165
246
  editor.commands.setContent(newValue, false);
166
247
  updateRichTextEditor.value = false;
167
248
  //text style type is not switching when reordering
@@ -182,12 +263,26 @@ export const useRichTextEditor = (model, props, emit, fontSettings, fontStyleDro
182
263
  });
183
264
  }
184
265
  });
185
- // Update model when editor content changes. RTE content is stored as Tiptap
186
- // JSON (BP-5787). getJSON() is already in hand (the editor holds the doc) and
187
- // is sanitised natively — no getHTML(), no DOMPurify, no extra editor — so
188
- // the per-keystroke path allocates nothing heavy.
266
+ // Update model when editor content changes
189
267
  editor.on('update', () => {
190
- emit('update:modelValue', sanitizeRteJson(editor.getJSON()));
268
+ const sanitizedData = sanitiseClientSideRTE(editor.getHTML());
269
+ // An update fired while the editor is unfocused is TipTap rewriting the
270
+ // model back into its canonical HTML during mount rehydration or a
271
+ // programmatic setContent — not a user edit. Fold it into the current
272
+ // head so merely selecting a section doesn't push a phantom undo entry
273
+ // (BP-5810). The construction-time arm above lapses before this async
274
+ // write-back fires, so re-arm here. Genuine edits happen while focused and
275
+ // still push a new entry.
276
+ //
277
+ // Known limitation: a programmatic toolbar command run on an editor the
278
+ // user hasn't focused yet would also merge (losing that undo step). Today
279
+ // the RTE only mounts inside an already-open settings drawer and the user
280
+ // focuses it before formatting, so this doesn't occur — revisit if a
281
+ // consumer ever drives toolbar actions on an unfocused editor.
282
+ if (!editor.isFocused && !historyStore.isApplyingSnapshot) {
283
+ historyStore.mergeNextMutation();
284
+ }
285
+ emit('update:modelValue', sanitizedData.sanitizedHtml);
191
286
  });
192
287
  editor.on('selectionUpdate', () => {
193
288
  // Skip while a history snapshot is being applied — `setContent` fires
@@ -1,16 +1,15 @@
1
1
  import { type ComputedRef } from 'vue';
2
2
  /**
3
- * Returns whether undo/redo is active for the current maker session. All
4
- * three conditions must hold:
3
+ * Returns whether undo/redo is active for the current maker session. Both
4
+ * conditions must hold:
5
5
  *
6
6
  * 1. `userStore.features.canUseUndoRedo` is true. The flag is populated by
7
7
  * the consumer (BrandCrowd.Net / web-renderer) via the
8
8
  * `WebsiteDesignUserFeatures.CanUseUndoRedo` boolean on the ViewModel —
9
- * same precedent as RichTextEditor / GettyVideos / AITextGenerator.
10
- * 2. The maker is a brand-site (Website Maker). V1 scope is brand-site
11
- * only; brand-page (single-page profile) and brand-contact (vCard) are
12
- * out of scope.
13
- * 3. The session is NOT in admin-maker mode (`userStore.isAdmin`). The
9
+ * same precedent as RichTextEditor / GettyVideos / AITextGenerator. The
10
+ * one flag governs all three brands (brand-site, brand-page,
11
+ * brand-contact) — there is no per-brand gating.
12
+ * 2. The session is NOT in admin-maker mode (`userStore.isAdmin`). The
14
13
  * admin maker uses the same components but a different editing flow
15
14
  * (template authoring) where undo/redo is out of scope.
16
15
  */
@@ -1,25 +1,20 @@
1
1
  import { computed } 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
- * Returns whether undo/redo is active for the current maker session. All
6
- * three conditions must hold:
4
+ * Returns whether undo/redo is active for the current maker session. Both
5
+ * conditions must hold:
7
6
  *
8
7
  * 1. `userStore.features.canUseUndoRedo` is true. The flag is populated by
9
8
  * the consumer (BrandCrowd.Net / web-renderer) via the
10
9
  * `WebsiteDesignUserFeatures.CanUseUndoRedo` boolean on the ViewModel —
11
- * same precedent as RichTextEditor / GettyVideos / AITextGenerator.
12
- * 2. The maker is a brand-site (Website Maker). V1 scope is brand-site
13
- * only; brand-page (single-page profile) and brand-contact (vCard) are
14
- * out of scope.
15
- * 3. The session is NOT in admin-maker mode (`userStore.isAdmin`). The
10
+ * same precedent as RichTextEditor / GettyVideos / AITextGenerator. The
11
+ * one flag governs all three brands (brand-site, brand-page,
12
+ * brand-contact) — there is no per-brand gating.
13
+ * 2. The session is NOT in admin-maker mode (`userStore.isAdmin`). The
16
14
  * admin maker uses the same components but a different editing flow
17
15
  * (template authoring) where undo/redo is out of scope.
18
16
  */
19
17
  export function useUndoRedoFlag() {
20
18
  const userStore = useBrandPageMakerUserStore();
21
- const brandPageMakerStore = useBrandPageMakerStore();
22
- return computed(() => userStore.features.canUseUndoRedo &&
23
- brandPageMakerStore.isBrandSite &&
24
- !userStore.isAdmin);
19
+ return computed(() => userStore.features.canUseUndoRedo && !userStore.isAdmin);
25
20
  }
@@ -211,7 +211,7 @@ export declare const useBrandPageMakerStore: import("pinia").StoreDefinition<"br
211
211
  * feature flag is on — see `BrandPageMaker.mixin.ts`).
212
212
  */
213
213
  _captureTemplateSwitchIfEnabled(): void;
214
- applyHistorySnapshot(snapshot: BrandPageModel, activePageId?: string, logoEditsStr?: string): Promise<void>;
214
+ applyHistorySnapshot(snapshot: BrandPageModel, activePageId?: string, logoEditsStr?: string, selectedTemplateId?: string | false): Promise<void>;
215
215
  _syncLogoSettingsFromSnapshot(snapshot: BrandPageModel, logoEditsStr?: string): Promise<void>;
216
216
  /**
217
217
  * Restore the active page after a history apply. The id passed in is
@@ -494,7 +494,7 @@ export const useBrandPageMakerStore = defineStore('brandPageMaker', {
494
494
  return;
495
495
  historyStore.captureTemplateSwitch();
496
496
  },
497
- async applyHistorySnapshot(snapshot, activePageId, logoEditsStr) {
497
+ async applyHistorySnapshot(snapshot, activePageId, logoEditsStr, selectedTemplateId) {
498
498
  const styleSettingsStore = useUiStyleSettingsStore();
499
499
  const navigationStyleSettingsStore = useUiNavigationStyleSettingsStore();
500
500
  const backgroundSettingsStore = useUiBackgroundSettingsStore();
@@ -574,6 +574,15 @@ export const useBrandPageMakerStore = defineStore('brandPageMaker', {
574
574
  this.ogDescription = snapshot.ogDescription;
575
575
  this.cookieOptions = snapshot.cookieOptions;
576
576
  this.saveContactSettings = snapshot.saveContactSettings;
577
+ // 7b. Restore the selected-template pointer that drives the templates
578
+ // sidebar highlight. It's a UI pointer, not part of the persisted
579
+ // model, so it rides along as a history-entry sidecar (like
580
+ // `activePageId`). Its watchers only reset scroll position (no
581
+ // template re-application), so setting it here is safe during apply.
582
+ // (BP-5813)
583
+ if (selectedTemplateId !== undefined) {
584
+ this.setSelectedTemplateId({ templateId: selectedTemplateId });
585
+ }
577
586
  // 8. Re-sync the logo-settings sidebar's `selectedLogoImage` from the
578
587
  // restored header logo. The preview rehydrates via the page contents,
579
588
  // but `uiLogoSettings` holds its own copy that drives the sidebar
@@ -622,9 +631,7 @@ export const useBrandPageMakerStore = defineStore('brandPageMaker', {
622
631
  console.error('brandpage-maker-store: failed to restore logoEdits', error);
623
632
  }
624
633
  }
625
- const logoImage = snapshot.brandPageType === BRAND_PAGE_TYPES.default
626
- ? snapshot.pages?.[0]?.contents?.find((c) => c.type === 'logo')?.logoImage
627
- : snapshot.pageHeaderDesktop?.logoImage;
634
+ const logoImage = logoUtil.getLogoImageFromModel(snapshot);
628
635
  if (!logoImage)
629
636
  return;
630
637
  logoSettingsStore.setState({
@@ -848,9 +855,7 @@ export const useBrandPageMakerStore = defineStore('brandPageMaker', {
848
855
  this.setBrandPageType({
849
856
  brandPageType: _brandPageType,
850
857
  });
851
- let logoImage = brandPageModel.brandPageType === BRAND_PAGE_TYPES.default
852
- ? brandPageModel.pages?.[0].contents.find((c) => c.type === 'logo')?.logoImage
853
- : brandPageModel.pageHeaderDesktop?.logoImage;
858
+ let logoImage = logoUtil.getLogoImageFromModel(brandPageModel);
854
859
  if (!logoImage?.logoDraftToken) {
855
860
  logoImage = logoUtil.safeGetLogoImageFromLogoImageUrl(logoVariationImageUrls[0]);
856
861
  }
@@ -16,6 +16,12 @@ export interface HistoryEntry {
16
16
  activePageId: string;
17
17
  /** JSON-stringified `editableLogoStore.logoEdits`. Undefined if logo tab never opened. */
18
18
  logoEdits?: string;
19
+ /**
20
+ * Selected template id at capture time — drives the templates sidebar
21
+ * highlight. `false` when no template is selected. Sidecar to
22
+ * `brandPageData` (not part of the persisted model), like `activePageId`.
23
+ */
24
+ selectedTemplateId?: string | false;
19
25
  }
20
26
  export declare const useHistoryStore: import("pinia").StoreDefinition<"history", Pick<{
21
27
  entries: import("vue").ShallowRef<HistoryEntry[], HistoryEntry[]>;
@@ -97,6 +97,9 @@ export const useHistoryStore = defineStore('history', () => {
97
97
  function getCurrentActivePageId() {
98
98
  return usePageNavigationStore().activePageId ?? '';
99
99
  }
100
+ function getCurrentSelectedTemplateId() {
101
+ return useBrandPageMakerStore().selectedTemplateId;
102
+ }
100
103
  function serializeCurrentLogoEdits() {
101
104
  try {
102
105
  const editableLogoStore = useUiEditableLogoStore();
@@ -150,6 +153,7 @@ export const useHistoryStore = defineStore('history', () => {
150
153
  sizeBytes: args.snapshot.length,
151
154
  activePageId: args.activePageId,
152
155
  logoEdits: args.logoEdits,
156
+ selectedTemplateId: args.selectedTemplateId,
153
157
  };
154
158
  // FR-11: truncate redo tail, then append.
155
159
  const next = entries.value.slice(0, currentIndex.value + 1);
@@ -209,6 +213,7 @@ export const useHistoryStore = defineStore('history', () => {
209
213
  return;
210
214
  const activePageId = getCurrentActivePageId();
211
215
  const logoEdits = serializeCurrentLogoEdits();
216
+ const selectedTemplateId = getCurrentSelectedTemplateId();
212
217
  // Settle window: fold post-init reactive cascades back into the
213
218
  // baseline so undo doesn't light up before the user has done
214
219
  // anything.
@@ -228,6 +233,7 @@ export const useHistoryStore = defineStore('history', () => {
228
233
  sizeBytes: snapshot.length,
229
234
  activePageId,
230
235
  logoEdits,
236
+ selectedTemplateId,
231
237
  },
232
238
  ];
233
239
  return;
@@ -241,6 +247,7 @@ export const useHistoryStore = defineStore('history', () => {
241
247
  sizeBytes: snapshot.length,
242
248
  activePageId,
243
249
  logoEdits,
250
+ selectedTemplateId,
244
251
  };
245
252
  entries.value = next;
246
253
  return;
@@ -251,6 +258,7 @@ export const useHistoryStore = defineStore('history', () => {
251
258
  actor: 'user',
252
259
  activePageId,
253
260
  logoEdits,
261
+ selectedTemplateId,
254
262
  });
255
263
  };
256
264
  // Monotonic — first mutation arms; subsequent ones inside the window
@@ -299,6 +307,7 @@ export const useHistoryStore = defineStore('history', () => {
299
307
  sizeBytes: snapshot.length,
300
308
  activePageId: getCurrentActivePageId(),
301
309
  logoEdits: serializeCurrentLogoEdits(),
310
+ selectedTemplateId: getCurrentSelectedTemplateId(),
302
311
  },
303
312
  ];
304
313
  currentIndex.value = 0;
@@ -318,6 +327,7 @@ export const useHistoryStore = defineStore('history', () => {
318
327
  snapshot,
319
328
  activePageId: getCurrentActivePageId(),
320
329
  logoEdits: serializeCurrentLogoEdits(),
330
+ selectedTemplateId: getCurrentSelectedTemplateId(),
321
331
  ...args,
322
332
  });
323
333
  }
@@ -393,7 +403,7 @@ export const useHistoryStore = defineStore('history', () => {
393
403
  pendingTimerIsMerge = false;
394
404
  cancelPendingAutoCapture();
395
405
  try {
396
- await brandPageMakerStore.applyHistorySnapshot(snapshot, resolvedActivePageId, targetEntry?.logoEdits);
406
+ await brandPageMakerStore.applyHistorySnapshot(snapshot, resolvedActivePageId, targetEntry?.logoEdits, targetEntry?.selectedTemplateId);
397
407
  }
398
408
  finally {
399
409
  isApplyingSnapshot.value = false;
@@ -5,6 +5,4 @@ export * from './url.util';
5
5
  export * from './slug.util';
6
6
  export * from './text.util';
7
7
  export * from './rich-text-editor.util';
8
- export * from './rte-render.util';
9
- export * from './rte-sanitize.util';
10
8
  export declare const isColorMatched: (color: string | null | undefined) => boolean;
@@ -6,8 +6,6 @@ export * from './url.util';
6
6
  export * from './slug.util';
7
7
  export * from './text.util';
8
8
  export * from './rich-text-editor.util';
9
- export * from './rte-render.util';
10
- export * from './rte-sanitize.util';
11
9
  export const isColorMatched = (color) => {
12
10
  if (!color)
13
11
  return false;
@@ -1,6 +1,6 @@
1
- import type { ChainedCommands, Editor, JSONContent } from '@tiptap/vue-3';
1
+ import type { ChainedCommands, Editor } from '@tiptap/vue-3';
2
2
  import type { EditorState, Transaction, Selection } from 'prosemirror-state';
3
- import { type Content, type TextStyleDeclaration, type TextStyleType, type RichTextValue } from '@brand-page';
3
+ import { type Content, type TextStyleDeclaration, type TextStyleType } from '@brand-page';
4
4
  import type { PageSettingsState } from '@shared/page-settings/store';
5
5
  import type { StyleSettingsState } from '@shared/style-settings/store';
6
6
  import type { Mark } from 'prosemirror-model';
@@ -9,22 +9,12 @@ export declare const RTE_ALLOWED_SECTION_TYPES: string[];
9
9
  export declare const isRTEManagedStyleField: (sectionType: string, fieldName: string) => boolean;
10
10
  export declare const isStyleChanged: (tr: Transaction) => boolean;
11
11
  export declare const skipStyleDetection: (chain: ChainedCommands, marks?: Mark[]) => ChainedCommands;
12
- /**
13
- * BP-5787 — canonical discriminator for the JSON RTE model: a ProseMirror
14
- * document is an object with `type: 'doc'`.
15
- */
16
- export declare const isTiptapDocument: (value: unknown) => value is JSONContent;
17
- /**
18
- * Extract visible plain text from a Tiptap JSON document (concatenated text
19
- * nodes). Dependency-free — no editor instantiation.
20
- */
21
- export declare const getRteJsonPlainText: (doc: JSONContent) => string;
22
- export declare const hasRTEContent: (content: unknown) => boolean;
23
- export declare const getPlainTextFromRTE: (content: unknown) => string;
12
+ export declare const hasRTEContent: (content: string) => boolean;
13
+ export declare const getPlainTextFromRTE: (content: string) => string;
24
14
  export declare const replaceTextInRTE: (existingHtml: string, newText: string) => string;
25
15
  export declare const transformToRTE: (uiPageSettingStore: PageSettingsState, externalEditor: Editor, uiStyleSettingsStore?: StyleSettingsState, newContent?: Content, type?: TextStyleType, newStyle?: TextStyleDeclaration) => void;
26
16
  export declare const switchAllContentToRTE: (uiPageSettingStore: PageSettingsState, externalEditor: Editor, uiStyleSettingsStore?: StyleSettingsState) => void;
27
- export declare const getUpdatedHTMLFromEditor: (externalEditor: Editor, contentText: RichTextValue, newContentStyle: TextStyleDeclaration) => string | JSONContent;
17
+ export declare const getUpdatedHTMLFromEditor: (externalEditor: Editor, contentText: string, newContentStyle: TextStyleDeclaration) => string;
28
18
  export declare const buildInitialMarks: (state: EditorState, style: TextStyleDeclaration) => any[];
29
19
  export declare const retainStyleOnEmptyLine: (editor: Editor, selection: Selection) => void;
30
20
  export interface SanitizationResult {
@@ -32,10 +22,6 @@ export interface SanitizationResult {
32
22
  removedElements: string[];
33
23
  }
34
24
  export declare const sanitiseClientSideRTE: (html: string) => SanitizationResult;
35
- /**
36
- * Validates if an href value is safe and returns a sanitized version
37
- */
38
- export declare const validateAndSanitizeHref: (href: string) => string | null;
39
25
  /**
40
26
  * Sanitize HTML string using DOMPurify.
41
27
  * Only allow tags and attributes that correspond to the enabled Tiptap extensions.
@@ -175,41 +175,14 @@ export const skipStyleDetection = (chain, marks) => chain.command(({ tr }) => {
175
175
  }
176
176
  return true;
177
177
  });
178
- /**
179
- * BP-5787 — canonical discriminator for the JSON RTE model: a ProseMirror
180
- * document is an object with `type: 'doc'`.
181
- */
182
- export const isTiptapDocument = (value) => typeof value === 'object' &&
183
- value !== null &&
184
- value.type === 'doc';
185
- /**
186
- * Extract visible plain text from a Tiptap JSON document (concatenated text
187
- * nodes). Dependency-free — no editor instantiation.
188
- */
189
- export const getRteJsonPlainText = (doc) => {
190
- const parts = [];
191
- const walk = (node) => {
192
- if (typeof node.text === 'string')
193
- parts.push(node.text);
194
- node.content?.forEach(walk);
195
- };
196
- walk(doc);
197
- return parts.join(' ').replace(/\s+/g, ' ').trim();
198
- };
199
- // Accepts a legacy inline-HTML string or a Tiptap JSON document (BP-5787).
200
178
  export const hasRTEContent = (content) => {
201
- if (isTiptapDocument(content))
202
- return true;
203
- if (typeof content !== 'string' || content.length === 0)
179
+ if (!content || content.length === 0)
204
180
  return false;
205
181
  const trimmed = content.trim();
206
182
  const tagWrapRegex = /^<([A-Za-z][A-Za-z0-9-]*)\b[^>]*>[\s\S]*<\/\1>$/;
207
183
  return tagWrapRegex.test(trimmed);
208
184
  };
209
185
  export const getPlainTextFromRTE = (content) => {
210
- if (isTiptapDocument(content)) {
211
- return getRteJsonPlainText(content);
212
- }
213
186
  if (typeof content !== 'string') {
214
187
  return '';
215
188
  }
@@ -501,10 +474,7 @@ export const getUpdatedHTMLFromEditor = (externalEditor, contentText, newContent
501
474
  if (!contentText) {
502
475
  return contentText;
503
476
  }
504
- // BP-5787: accept a string (legacy HTML) or a Tiptap JSON doc. setContent
505
- // handles both; only the newline-escaping shim applies to strings.
506
- const content = typeof contentText === 'string' &&
507
- contentText.includes(ESCAPED_NEWLINE_CHARACTER)
477
+ const content = contentText.includes(ESCAPED_NEWLINE_CHARACTER)
508
478
  ? getRenderText(contentText)
509
479
  : contentText;
510
480
  externalEditor.commands.setContent(content);
@@ -548,8 +518,8 @@ export const getUpdatedHTMLFromEditor = (externalEditor, contentText, newContent
548
518
  : chain.unsetBold();
549
519
  chain = newContentStyle?.fontStyle ? chain.setItalic() : chain.unsetItalic();
550
520
  chain.run();
551
- // BP-5787: return the JSON form, consistent with how RTE is now stored.
552
- return externalEditor.getJSON();
521
+ const html = externalEditor.getHTML();
522
+ return html;
553
523
  };
554
524
  // Helper to build stored marks from a style declaration
555
525
  export const buildInitialMarks = (state, style) => {
@@ -628,7 +598,7 @@ export const sanitiseClientSideRTE = (html) => {
628
598
  /**
629
599
  * Validates if an href value is safe and returns a sanitized version
630
600
  */
631
- export const validateAndSanitizeHref = (href) => {
601
+ const validateAndSanitizeHref = (href) => {
632
602
  if (!href || typeof href !== 'string')
633
603
  return null;
634
604
  // Trim whitespace and decode entities
@@ -1,5 +1,5 @@
1
1
  import { isURL, isEmail, isTel } from '@utils/validator.util';
2
- import { YOUTUBE_URL_RE, isTiptapDocument } from '@brand-page-maker/utils';
2
+ import { YOUTUBE_URL_RE } from '@brand-page-maker/utils';
3
3
  import { validatorTr } from '@src/composables/useTranslate';
4
4
  const addressValueValidator = {
5
5
  validate: (name, value) => {
@@ -62,15 +62,7 @@ export const dummyValidator = {
62
62
  };
63
63
  const nonEmptyStringValidator = {
64
64
  validate: (name, value) => {
65
- // BP-5787/BP-5857: match legacy (pre-JSON) validation semantics.
66
- // An RTE field's stored value used to be inline HTML — an empty editor was
67
- // "<p></p>", a NON-empty string, so the required check always passed for RTE
68
- // fields (it only ever failed for genuinely empty PLAIN inputs). Now RTE
69
- // fields are Tiptap JSON docs; treat any doc as "present" so behaviour
70
- // stays identical. A plain string is still required to be non-empty.
71
- const isValid = isTiptapDocument(value)
72
- ? true
73
- : typeof value === 'string' && value.length > 0;
65
+ const isValid = typeof value === 'string' && value.length > 0;
74
66
  return {
75
67
  isValid,
76
68
  message: isValid ? '' : validatorTr(`emptyStringMessage`),
@@ -5703,7 +5703,7 @@ input[data-v-4cd691db]::-webkit-inner-spin-button {
5703
5703
  }
5704
5704
  }
5705
5705
 
5706
- .empty-para[data-v-6ed2f53e] p:empty::after {
5706
+ .empty-para[data-v-c90cc6bc] p:empty::after {
5707
5707
  content: '\00A0';
5708
5708
  }
5709
5709
 
@@ -6314,7 +6314,7 @@ input[data-v-4cd691db]::-webkit-inner-spin-button {
6314
6314
  }
6315
6315
  }
6316
6316
 
6317
- .empty-para[data-v-6ed2f53e] p:empty::after {
6317
+ .empty-para[data-v-c90cc6bc] p:empty::after {
6318
6318
  content: '\00A0';
6319
6319
  }
6320
6320
 
@@ -5733,7 +5733,7 @@ input[data-v-4cd691db]::-webkit-inner-spin-button {
5733
5733
  }
5734
5734
  }
5735
5735
 
5736
- .empty-para[data-v-6ed2f53e] p:empty::after {
5736
+ .empty-para[data-v-c90cc6bc] p:empty::after {
5737
5737
  content: '\00A0';
5738
5738
  }
5739
5739
 
@@ -5703,7 +5703,7 @@ input[data-v-4cd691db]::-webkit-inner-spin-button {
5703
5703
  }
5704
5704
  }
5705
5705
 
5706
- .bp .empty-para[data-v-6ed2f53e] p:empty::after {
5706
+ .bp .empty-para[data-v-c90cc6bc] p:empty::after {
5707
5707
  content: '\00A0';
5708
5708
  }
5709
5709
 
@@ -28,7 +28,6 @@ export * as brandPageUtils from './brand-page/utils';
28
28
  export type { CriticalResourceURLs } from './brand-page/utils/view-model.util';
29
29
  export { getOptimalLogoSize, calculateOptimizedLogoDimensions, applyLogoImageBase64UrlToBrandPageTemplateModel, recalculateLogoContentDimensions, recalculateHeaderLogoSizes, } from './brand-page/utils/logo.util';
30
30
  export { sanitizeRTEHtml } from './brand-page-maker/utils';
31
- export { renderRteToHtml, toRteJson, isTiptapDocument, sanitizeRteJson, getRteJsonPlainText, type RichTextValue, } from './brand-page-maker/utils';
32
31
  export { getPageSlugFromPageTitle } from './brand-page-maker/utils/slug.util';
33
32
  export { executeToolCallsAsync } from './metadata-builder/ai-metadata-executor';
34
33
  export type { ParsedToolCall } from './metadata-builder/parsers/ai-toolcall-parser';