@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.
- package/lib/src/{CreatedOnBrandCrowd-de-de-SPVLPi8n.js → CreatedOnBrandCrowd-de-de-BSFTp6yl.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-CvtrCPHd.js → CreatedOnBrandCrowd-en-us-rPQrOY1L.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-BSsac7jB.js → CreatedOnBrandCrowd-es-es-awd8-QFa.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-BauENV2a.js → CreatedOnBrandCrowd-fr-ca-CwJ4J7Ow.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-BauENV2a.js → CreatedOnBrandCrowd-fr-fr-CwJ4J7Ow.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-Be0hawmr.js → CreatedOnBrandCrowd-pt-br-BXYfTLlD.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-Be0hawmr.js → CreatedOnBrandCrowd-pt-pt-BXYfTLlD.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-CTjE2Tmn.js → CreatedOnDcom-de-de-kX6u1lNK.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-DnZRp5Pg.js → CreatedOnDcom-en-us-Bkq5sgFs.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-J-9qvjy8.js → CreatedOnDcom-es-es-zCAC9ujj.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-BDNHqm4G.js → CreatedOnDcom-fr-ca-BYz6Wzg5.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-B_2-ceuO.js → CreatedOnDcom-fr-fr-CN-wAaSH.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-76lJVrAY.js → CreatedOnDcom-pt-br-BrK4Ngg-.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-TQp3CNcu.js → CreatedOnDcom-pt-pt-AfI_g7xJ.js} +1 -1
- package/lib/src/brand-page/models/index.d.ts +26 -34
- package/lib/src/brand-page/utils/logo.util.d.ts +14 -0
- package/lib/src/brand-page/utils/logo.util.js +19 -0
- package/lib/src/brand-page/utils/migration.util.js +0 -11
- package/lib/src/brand-page/utils/text.util.d.ts +1 -1
- package/lib/src/brand-page/utils/text.util.js +0 -32
- package/lib/src/brand-page/utils/vcard.util.js +4 -4
- package/lib/src/brand-page/utils/view-model.util.d.ts +0 -1
- package/lib/src/brand-page/utils/view-model.util.js +20 -68
- package/lib/src/brand-page/view-models.d.ts +3 -3
- package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.js +1 -1
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMakerTemplateSettings.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/brandpage-settings/ReplaceImageSettingsForBrandPageSettings.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/social-share/SocialShare.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +115 -20
- package/lib/src/brand-page-maker/composables/useUndoRedoFlag.d.ts +6 -7
- package/lib/src/brand-page-maker/composables/useUndoRedoFlag.js +7 -12
- package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.d.ts +1 -1
- package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +12 -7
- package/lib/src/brand-page-maker/stores/history/history-store.d.ts +6 -0
- package/lib/src/brand-page-maker/stores/history/history-store.js +11 -1
- package/lib/src/brand-page-maker/utils/index.d.ts +0 -2
- package/lib/src/brand-page-maker/utils/index.js +0 -2
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +5 -19
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +5 -35
- package/lib/src/brand-page-maker/validators/field.validator.js +2 -10
- package/lib/src/css/library.brand-page-brandCrowd.min.css +1 -1
- package/lib/src/css/library.brand-page-brandPage.min.css +1 -1
- package/lib/src/css/library.brand-page-designCom.min.css +1 -1
- package/lib/src/css/library.brand-page-preview.min.css +1 -1
- package/lib/src/index.d.ts +0 -1
- package/lib/src/index.mjs +145 -150
- package/lib/src/{lib-Bh9Y5_-x.js → lib-BYFWAvIq.js} +55864 -56119
- package/lib/src/lib.js +0 -2
- package/lib/src/metadata-builder/builders/reorder-pages-builder.d.ts +13 -0
- package/lib/src/metadata-builder/builders/reorder-pages-builder.js +49 -0
- package/lib/src/metadata-builder/builders/update-content-text-builder.js +15 -20
- package/lib/src/metadata-builder/builders/update-content-text-style-builder.js +3 -5
- package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +1 -1
- package/lib/src/metadata-builder/helpers/rte.helpers.js +3 -6
- package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +1 -14
- package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +3 -12
- package/lib/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.d.ts +1 -0
- package/lib/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.js +18 -7
- package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +1 -1
- package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +1 -1
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +1 -1
- package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +1 -1
- package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +1 -1
- package/lib/src/utils/minify-brand-page-metadata.js +1 -4
- package/package.json +1 -1
- package/src/brand-page/components/text-wrapper/TextWrapper.vue +6 -17
- package/src/brand-page/models/index.ts +26 -35
- package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
- package/src/brand-page/utils/logo.util.test.ts +54 -1
- package/src/brand-page/utils/logo.util.ts +23 -0
- package/src/brand-page/utils/migration.util.ts +0 -12
- package/src/brand-page/utils/style.util.test.ts +0 -51
- package/src/brand-page/utils/text.util.ts +4 -43
- package/src/brand-page/utils/vcard.util.ts +4 -6
- package/src/brand-page/utils/view-model.util.ts +26 -94
- package/src/brand-page/view-models.ts +2 -3
- package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.ts +1 -1
- package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +123 -22
- package/src/brand-page-maker/composables/useUndoRedoFlag.ts +7 -13
- package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.test.ts +53 -0
- package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +13 -16
- package/src/brand-page-maker/stores/history/history-store.test.ts +51 -0
- package/src/brand-page-maker/stores/history/history-store.ts +19 -0
- package/src/brand-page-maker/utils/index.ts +0 -2
- package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +3 -4
- package/src/brand-page-maker/utils/rich-text-editor.util.ts +11 -50
- package/src/brand-page-maker/validators/field.validator.ts +2 -10
- package/src/lib.ts +0 -9
- package/src/metadata-builder/builders/reorder-pages-builder.ts +80 -0
- package/src/metadata-builder/builders/update-content-text-builder.ts +18 -32
- package/src/metadata-builder/builders/update-content-text-style-builder.ts +4 -6
- package/src/metadata-builder/helpers/rte.helpers.ts +7 -11
- package/src/shared/content-settings/components/GenericTextInput.mixin.ts +1 -17
- package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +3 -16
- package/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.ts +19 -11
- package/src/utils/minify-brand-page-metadata.ts +1 -4
- package/lib/src/brand-page/utils/migration/migration-18.d.ts +0 -2
- package/lib/src/brand-page/utils/migration/migration-18.js +0 -71
- package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.d.ts +0 -22
- package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.js +0 -22
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.d.ts +0 -14
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.js +0 -39
- package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +0 -28
- package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.js +0 -66
- package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +0 -33
- package/lib/src/brand-page-maker/utils/rte-render.util.js +0 -50
- package/lib/src/brand-page-maker/utils/rte-sanitize.util.d.ts +0 -6
- package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +0 -158
- package/lib/src/brand-page-maker/utils/rte-serializer.d.ts +0 -5
- package/lib/src/brand-page-maker/utils/rte-serializer.js +0 -112
- package/src/brand-page/utils/migration/migration-18.test.ts +0 -72
- package/src/brand-page/utils/migration/migration-18.ts +0 -83
- package/src/brand-page/utils/text.util.test.ts +0 -47
- package/src/brand-page/utils/view-model.pageid-rte.test.ts +0 -104
- package/src/brand-page-maker/__fixtures__/complex-rte.fixture.ts +0 -23
- package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.test.ts +0 -52
- package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.ts +0 -42
- package/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.ts +0 -113
- package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +0 -200
- package/src/brand-page-maker/utils/rte-render-parity.test.ts +0 -156
- package/src/brand-page-maker/utils/rte-render.util.ts +0 -77
- package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +0 -158
- package/src/brand-page-maker/utils/rte-sanitize.util.ts +0 -195
- package/src/brand-page-maker/utils/rte-serializer.ts +0 -172
- package/src/brand-page-maker/validators/field.validator.test.ts +0 -55
|
@@ -68,19 +68,17 @@ const mergeStyles = (
|
|
|
68
68
|
return merged;
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const updateRTEText = (text: unknown, style: TextStyleDeclaration): unknown => {
|
|
74
|
-
if (!hasRTEContent(text)) {
|
|
71
|
+
const updateRTEText = (text: string, style: TextStyleDeclaration): string => {
|
|
72
|
+
if (!text || typeof text !== 'string' || !hasRTEContent(text)) {
|
|
75
73
|
return text;
|
|
76
74
|
}
|
|
77
75
|
|
|
78
76
|
const { externalEditor } = useExternalRichTextEditor();
|
|
79
|
-
const
|
|
77
|
+
const updatedText = applyStyleToRTEText(externalEditor, text, style);
|
|
80
78
|
|
|
81
79
|
externalEditor.destroy();
|
|
82
80
|
|
|
83
|
-
return
|
|
81
|
+
return updatedText;
|
|
84
82
|
};
|
|
85
83
|
|
|
86
84
|
const findStyleKey = (target: Content, property: string): string | null => {
|
|
@@ -11,23 +11,18 @@ import {
|
|
|
11
11
|
|
|
12
12
|
export const applyStyleToRTEText = (
|
|
13
13
|
externalEditor: Editor,
|
|
14
|
-
contentText:
|
|
14
|
+
contentText: string,
|
|
15
15
|
newContentStyle: TextStyleDeclaration,
|
|
16
16
|
) => {
|
|
17
17
|
if (!contentText) {
|
|
18
18
|
return contentText;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const content =
|
|
25
|
-
typeof contentText === 'string' &&
|
|
26
|
-
contentText.includes(ESCAPED_NEWLINE_CHARACTER)
|
|
27
|
-
? getRenderText(contentText)
|
|
28
|
-
: contentText;
|
|
21
|
+
const content = contentText.includes(ESCAPED_NEWLINE_CHARACTER)
|
|
22
|
+
? getRenderText(contentText)
|
|
23
|
+
: contentText;
|
|
29
24
|
|
|
30
|
-
externalEditor.commands.setContent(content
|
|
25
|
+
externalEditor.commands.setContent(content);
|
|
31
26
|
|
|
32
27
|
let chain = externalEditor.chain().selectAll();
|
|
33
28
|
|
|
@@ -82,8 +77,9 @@ export const applyStyleToRTEText = (
|
|
|
82
77
|
: chain.unsetItalic();
|
|
83
78
|
|
|
84
79
|
chain.run();
|
|
80
|
+
const html = externalEditor.getHTML();
|
|
85
81
|
|
|
86
|
-
return
|
|
82
|
+
return html;
|
|
87
83
|
};
|
|
88
84
|
|
|
89
85
|
/**
|
|
@@ -159,23 +159,7 @@ export default defineComponent({
|
|
|
159
159
|
},
|
|
160
160
|
},
|
|
161
161
|
watch: {
|
|
162
|
-
value(newVal: string | undefined
|
|
163
|
-
// BP-5787: RTE values are Tiptap JSON objects. The parent (TextItemsField)
|
|
164
|
-
// re-clones its items array (cloneDeep) on ANY change — including a sibling
|
|
165
|
-
// field's style sync — yielding a NEW object reference with identical text
|
|
166
|
-
// content. For objects Vue fires this watcher on reference change, so that
|
|
167
|
-
// spurious re-clone would overwrite `_value` and clobber an in-flight edit
|
|
168
|
-
// (a mark applied but not yet debounced to the store), silently dropping
|
|
169
|
-
// the user's formatting. Strings never hit this (same content = same
|
|
170
|
-
// reference = no fire), so mirror that: ignore re-clones whose content is
|
|
171
|
-
// unchanged from the previous prop value.
|
|
172
|
-
if (
|
|
173
|
-
this.showRichTextEditor &&
|
|
174
|
-
JSON.stringify(newVal) === JSON.stringify(oldVal)
|
|
175
|
-
) {
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
|
|
162
|
+
value(newVal: string | undefined) {
|
|
179
163
|
this._value = newVal;
|
|
180
164
|
|
|
181
165
|
if (this.showRichTextEditor) {
|
|
@@ -21,9 +21,6 @@ import {
|
|
|
21
21
|
replaceTextInRTE,
|
|
22
22
|
RTE_ALLOWED_SECTION_TYPES,
|
|
23
23
|
isRTEManagedStyleField,
|
|
24
|
-
isTiptapDocument,
|
|
25
|
-
renderRteToHtml,
|
|
26
|
-
toRteJson,
|
|
27
24
|
} from '@brand-page-maker/utils';
|
|
28
25
|
import { isDefaultContentText } from '@shared/content-settings/utils';
|
|
29
26
|
import { useUiLogoSettingsStore } from '../../../logo-settings/store';
|
|
@@ -324,20 +321,10 @@ export default defineComponent({
|
|
|
324
321
|
this.isToggled = isToggled;
|
|
325
322
|
},
|
|
326
323
|
onUseGeneratedText(generatedText: string) {
|
|
327
|
-
let formattedText
|
|
324
|
+
let formattedText = generatedText;
|
|
328
325
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
// (cold AI path, shared editor); string fields use it directly.
|
|
332
|
-
if (this.showRichTextEditor) {
|
|
333
|
-
if (isTiptapDocument(this.value)) {
|
|
334
|
-
formattedText =
|
|
335
|
-
toRteJson(
|
|
336
|
-
replaceTextInRTE(renderRteToHtml(this.value), generatedText),
|
|
337
|
-
) ?? generatedText;
|
|
338
|
-
} else if (typeof this.value === 'string') {
|
|
339
|
-
formattedText = replaceTextInRTE(this.value, generatedText);
|
|
340
|
-
}
|
|
326
|
+
if (this.showRichTextEditor && typeof this.value === 'string') {
|
|
327
|
+
formattedText = replaceTextInRTE(this.value, generatedText);
|
|
341
328
|
}
|
|
342
329
|
|
|
343
330
|
this.contentSettingsStore.updateItemValue({
|
|
@@ -176,17 +176,7 @@ export default defineComponent({
|
|
|
176
176
|
TEXT_SIZE_CHANGE_DEBOUNCE_WAIT_IN_MS,
|
|
177
177
|
);
|
|
178
178
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (value.textStyleType) {
|
|
182
|
-
const textStyleTypeOption = this.textStyleTypeOptions.find(
|
|
183
|
-
(x) => x.value === value.textStyleType,
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
if (textStyleTypeOption) {
|
|
187
|
-
textStyleTypeOption.isSelected = true;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
179
|
+
this.syncTextStyleTypeSelection();
|
|
190
180
|
},
|
|
191
181
|
computed: {
|
|
192
182
|
selectedTextStyle(): TextStyleDeclaration {
|
|
@@ -423,9 +413,27 @@ export default defineComponent({
|
|
|
423
413
|
generateFontFamilySelectorKey() {
|
|
424
414
|
return `fontFamilySelector-${this.id}-${Date.now()}`;
|
|
425
415
|
},
|
|
416
|
+
// Re-derive the text-style-type selection from the current `value`.
|
|
417
|
+
// Seeded in created() and re-run whenever `value` changes (e.g. undo/redo
|
|
418
|
+
// restores the model) so the dropdown title and `selectedTextStyle`
|
|
419
|
+
// reflect the model rather than a stale local flag. (BP-5811 / BP-5817)
|
|
420
|
+
syncTextStyleTypeSelection() {
|
|
421
|
+
const value = this.value as unknown as TextStyleDeclaration;
|
|
422
|
+
|
|
423
|
+
this.textStyleTypeOptions.forEach((option) => {
|
|
424
|
+
// The leading Boolean(...) guard is deliberate — do NOT simplify to
|
|
425
|
+
// `option.value === value?.textStyleType`. The "none" option's value
|
|
426
|
+
// is `undefined`, so that form would SELECT it whenever no style is
|
|
427
|
+
// set, whereas the convention (see emitChange) is all-flags-false
|
|
428
|
+
// when there is no textStyleType.
|
|
429
|
+
option.isSelected =
|
|
430
|
+
Boolean(value?.textStyleType) && option.value === value.textStyleType;
|
|
431
|
+
});
|
|
432
|
+
},
|
|
426
433
|
},
|
|
427
434
|
watch: {
|
|
428
435
|
value() {
|
|
436
|
+
this.syncTextStyleTypeSelection();
|
|
429
437
|
this.$nextTick(() => {
|
|
430
438
|
this.fontFamilySelectorKey = this.generateFontFamilySelectorKey();
|
|
431
439
|
});
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from '../brand-page/models';
|
|
11
11
|
import type { Content, ContentType } from '../brand-page/models/content.model';
|
|
12
12
|
import type { TextStyleDeclaration } from '../brand-page/models/styles.model';
|
|
13
|
-
import { hasRTEContent
|
|
13
|
+
import { hasRTEContent } from '../brand-page-maker/utils';
|
|
14
14
|
import { useExternalRichTextEditor } from '../brand-page-maker/composables/rich-text-editor/useRichTextEditor';
|
|
15
15
|
import type { Editor, JSONContent } from '@tiptap/vue-3';
|
|
16
16
|
import {
|
|
@@ -101,9 +101,6 @@ const convertText = (
|
|
|
101
101
|
value: unknown,
|
|
102
102
|
editor: Editor,
|
|
103
103
|
): RichTextValue | undefined => {
|
|
104
|
-
// Already Tiptap JSON (BP-5787 storage) — keep as-is.
|
|
105
|
-
if (isTiptapDocument(value)) return value;
|
|
106
|
-
|
|
107
104
|
if (typeof value !== 'string' || !value) return undefined;
|
|
108
105
|
|
|
109
106
|
if (hasRTEContent(value)) {
|
|
@@ -1,71 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,22 +0,0 @@
|
|
|
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)";
|
|
@@ -1,22 +0,0 @@
|
|
|
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)';
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BP-5787 — font-family with quote-stripping parse.
|
|
3
|
-
*
|
|
4
|
-
* The stock extension stores whatever `element.style.fontFamily` returns. For
|
|
5
|
-
* multi-word families (e.g. "Open Sans", "Playwrite AU") the browser quotes the
|
|
6
|
-
* value, so parsing HTML — pasting RTE-styled text between editors, or the
|
|
7
|
-
* HTML→JSON migration — yields a value that still contains the quote chars
|
|
8
|
-
* (`"Open Sans"`). That value then (a) no longer matches the font list and,
|
|
9
|
-
* worse, (b) is dropped by the native RTE sanitiser (`sanitizeRteJson`), whose
|
|
10
|
-
* `isSafeCssValue` rejects any value containing `"` `'` `;` — so the font is
|
|
11
|
-
* silently lost. Mirror the FontSize/LetterSpacing extensions and strip the
|
|
12
|
-
* quotes on parse so the stored value is always the bare family name.
|
|
13
|
-
*/
|
|
14
|
-
export declare const FontFamily: import("@tiptap/core").Extension<import("@tiptap/extension-font-family").FontFamilyOptions, any>;
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import TiptapFontFamily from '@tiptap/extension-font-family';
|
|
2
|
-
/**
|
|
3
|
-
* BP-5787 — font-family with quote-stripping parse.
|
|
4
|
-
*
|
|
5
|
-
* The stock extension stores whatever `element.style.fontFamily` returns. For
|
|
6
|
-
* multi-word families (e.g. "Open Sans", "Playwrite AU") the browser quotes the
|
|
7
|
-
* value, so parsing HTML — pasting RTE-styled text between editors, or the
|
|
8
|
-
* HTML→JSON migration — yields a value that still contains the quote chars
|
|
9
|
-
* (`"Open Sans"`). That value then (a) no longer matches the font list and,
|
|
10
|
-
* worse, (b) is dropped by the native RTE sanitiser (`sanitizeRteJson`), whose
|
|
11
|
-
* `isSafeCssValue` rejects any value containing `"` `'` `;` — so the font is
|
|
12
|
-
* silently lost. Mirror the FontSize/LetterSpacing extensions and strip the
|
|
13
|
-
* quotes on parse so the stored value is always the bare family name.
|
|
14
|
-
*/
|
|
15
|
-
export const FontFamily = TiptapFontFamily.extend({
|
|
16
|
-
addGlobalAttributes() {
|
|
17
|
-
return [
|
|
18
|
-
{
|
|
19
|
-
types: this.options.types,
|
|
20
|
-
attributes: {
|
|
21
|
-
fontFamily: {
|
|
22
|
-
default: null,
|
|
23
|
-
// Strip quotes only — keep '' for an absent value so the stored
|
|
24
|
-
// JSON shape matches the other textStyle attrs (FontSize etc.).
|
|
25
|
-
parseHTML: (element) => element.style.fontFamily?.replace(/['"]+/g, ''),
|
|
26
|
-
renderHTML: (attributes) => {
|
|
27
|
-
if (!attributes.fontFamily) {
|
|
28
|
-
return {};
|
|
29
|
-
}
|
|
30
|
-
return {
|
|
31
|
-
style: `font-family: ${attributes.fontFamily}`,
|
|
32
|
-
};
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
];
|
|
38
|
-
},
|
|
39
|
-
});
|
|
@@ -1,28 +0,0 @@
|
|
|
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;
|
|
@@ -1,66 +0,0 @@
|
|
|
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 ColorExtension from '@tiptap/extension-color';
|
|
6
|
-
import Highlight from '@tiptap/extension-highlight';
|
|
7
|
-
import Placeholder from '@tiptap/extension-placeholder';
|
|
8
|
-
import Link from '@tiptap/extension-link';
|
|
9
|
-
import { FontFamily } from './extensions/FontFamily';
|
|
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
|
-
};
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { type JSONContent } from '@tiptap/vue-3';
|
|
2
|
-
import { isTiptapDocument, getRteJsonPlainText } from './rich-text-editor.util';
|
|
3
|
-
export { isTiptapDocument, getRteJsonPlainText };
|
|
4
|
-
/**
|
|
5
|
-
* BP-5787 — the boundary plumbing for storing RTE as Tiptap JSON.
|
|
6
|
-
*
|
|
7
|
-
* An RTE field is either a legacy inline-HTML string (pre-migration, or a
|
|
8
|
-
* not-yet-migrated published site) or a Tiptap JSON document (post-migration).
|
|
9
|
-
* These helpers let every consumer treat the two uniformly:
|
|
10
|
-
*
|
|
11
|
-
* - isTiptapDocument — the canonical discriminator (object with type 'doc').
|
|
12
|
-
* - renderRteToHtml — render either form to final HTML for display.
|
|
13
|
-
* - toRteJson — normalise either form to JSON for storage.
|
|
14
|
-
*
|
|
15
|
-
* - renderRteToHtml uses a DOM-less serialiser (works on the server).
|
|
16
|
-
* - toRteJson uses a headless editor to PARSE HTML → JSON, so it only runs
|
|
17
|
-
* where a DOM exists (browser, or jsdom in tests); the server never parses
|
|
18
|
-
* HTML (migration-18 is DOM-guarded, translation/fraud walk JSON directly).
|
|
19
|
-
*/
|
|
20
|
-
export type RichTextValue = string | JSONContent;
|
|
21
|
-
/**
|
|
22
|
-
* Render an RTE value to final HTML. Legacy HTML strings pass through untouched
|
|
23
|
-
* (they are already HTML); Tiptap JSON is serialised by a DOM-less pure
|
|
24
|
-
* function (rte-serializer) — no editor, no DOM — so it works on the server
|
|
25
|
-
* (SSR/publish), not just the browser, and is cheap enough to run per render.
|
|
26
|
-
*/
|
|
27
|
-
export declare const renderRteToHtml: (value: RichTextValue | null | undefined) => string;
|
|
28
|
-
/**
|
|
29
|
-
* Normalise an RTE value to a Tiptap JSON document. HTML strings are parsed
|
|
30
|
-
* via the headless editor; documents pass through unchanged. Returns
|
|
31
|
-
* undefined for empty/blank input so callers can omit empty fields.
|
|
32
|
-
*/
|
|
33
|
-
export declare const toRteJson: (value: RichTextValue | null | undefined) => JSONContent | undefined;
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { Editor } from '@tiptap/vue-3';
|
|
2
|
-
import { getRteExtensions } from '../composables/rich-text-editor/getRteExtensions';
|
|
3
|
-
import { isTiptapDocument, getRteJsonPlainText } from './rich-text-editor.util';
|
|
4
|
-
import { serializeRteJsonToHtml } from './rte-serializer';
|
|
5
|
-
export { isTiptapDocument, getRteJsonPlainText };
|
|
6
|
-
/**
|
|
7
|
-
* Run a function with a short-lived headless editor (created then destroyed).
|
|
8
|
-
* Used only by toRteJson (HTML → JSON parsing), which needs a real DOM. Not
|
|
9
|
-
* used by the render path, which is DOM-less.
|
|
10
|
-
*/
|
|
11
|
-
const withHeadlessEditor = (fn) => {
|
|
12
|
-
const editor = new Editor({
|
|
13
|
-
editable: false,
|
|
14
|
-
extensions: getRteExtensions({ includeLinkRel: true }),
|
|
15
|
-
});
|
|
16
|
-
try {
|
|
17
|
-
return fn(editor);
|
|
18
|
-
}
|
|
19
|
-
finally {
|
|
20
|
-
editor.destroy();
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
/**
|
|
24
|
-
* Render an RTE value to final HTML. Legacy HTML strings pass through untouched
|
|
25
|
-
* (they are already HTML); Tiptap JSON is serialised by a DOM-less pure
|
|
26
|
-
* function (rte-serializer) — no editor, no DOM — so it works on the server
|
|
27
|
-
* (SSR/publish), not just the browser, and is cheap enough to run per render.
|
|
28
|
-
*/
|
|
29
|
-
export const renderRteToHtml = (value) => {
|
|
30
|
-
if (value == null || value === '')
|
|
31
|
-
return '';
|
|
32
|
-
if (typeof value === 'string')
|
|
33
|
-
return value;
|
|
34
|
-
return serializeRteJsonToHtml(value);
|
|
35
|
-
};
|
|
36
|
-
/**
|
|
37
|
-
* Normalise an RTE value to a Tiptap JSON document. HTML strings are parsed
|
|
38
|
-
* via the headless editor; documents pass through unchanged. Returns
|
|
39
|
-
* undefined for empty/blank input so callers can omit empty fields.
|
|
40
|
-
*/
|
|
41
|
-
export const toRteJson = (value) => {
|
|
42
|
-
if (value == null || value === '')
|
|
43
|
-
return undefined;
|
|
44
|
-
if (isTiptapDocument(value))
|
|
45
|
-
return value;
|
|
46
|
-
return withHeadlessEditor((editor) => {
|
|
47
|
-
editor.commands.setContent(value);
|
|
48
|
-
return editor.getJSON();
|
|
49
|
-
});
|
|
50
|
-
};
|