@designcrowd/library.brand-page 6.0.19-ast-btns-5 → 6.0.19
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-BtL6m52s.js → CreatedOnBrandCrowd-de-de-DCwwdYPj.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-BSLnZqDA.js → CreatedOnBrandCrowd-en-us-DTmZeDQ1.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-DJisqEzN.js → CreatedOnBrandCrowd-es-es-Dbrz6TXv.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-Br0QFIMc.js → CreatedOnBrandCrowd-fr-ca-DLQhgn8T.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-Br0QFIMc.js → CreatedOnBrandCrowd-fr-fr-DLQhgn8T.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-DW9cK-UT.js → CreatedOnBrandCrowd-pt-br-yAzzD2ik.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-DW9cK-UT.js → CreatedOnBrandCrowd-pt-pt-yAzzD2ik.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-C5QJae6P.js → CreatedOnDcom-de-de-CNmNNE5o.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-9AOMzc5W.js → CreatedOnDcom-en-us-BUmLFlS_.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-BDi3kJNO.js → CreatedOnDcom-es-es-DfygM_0q.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-BqaQPaNY.js → CreatedOnDcom-fr-ca-DIEft8xE.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-B6zLCUL7.js → CreatedOnDcom-fr-fr-Cn3jWG2n.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-DxPwUT6p.js → CreatedOnDcom-pt-br-DXhgGwEh.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-CqYO0kHy.js → CreatedOnDcom-pt-pt-BvE9ckw1.js} +1 -1
- package/lib/src/brand-page/components/collapsible/Collapsible.mixin.d.ts +0 -11
- package/lib/src/brand-page/components/collapsible/Collapsible.mixin.js +1 -13
- package/lib/src/brand-page/components/contact-form/ContactForm.mixin.d.ts +0 -1
- package/lib/src/brand-page/components/contact-form/ContactForm.mixin.js +8 -14
- package/lib/src/brand-page/components/mailing-list/MailingList.mixin.js +0 -5
- package/lib/src/brand-page/components/media/Media.mixin.d.ts +10 -1
- package/lib/src/brand-page/components/media/Media.mixin.js +13 -0
- package/lib/src/brand-page/components/page/Page.mixin.d.ts +5 -0
- package/lib/src/brand-page/components/page/Page.mixin.js +8 -1
- package/lib/src/brand-page/components/payment/Payment.mixin.js +0 -5
- package/lib/src/brand-page/models/index.d.ts +34 -27
- package/lib/src/brand-page/models/payment.model.d.ts +0 -2
- package/lib/src/brand-page/utils/button.util.d.ts +0 -5
- package/lib/src/brand-page/utils/button.util.js +0 -13
- package/lib/src/brand-page/utils/index.d.ts +1 -0
- package/lib/src/brand-page/utils/index.js +1 -0
- package/lib/src/brand-page/utils/migration/migration-19.js +60 -29
- package/lib/src/brand-page/utils/migration.util.js +5 -9
- package/lib/src/brand-page/utils/page-heading.util.d.ts +13 -0
- package/lib/src/brand-page/utils/page-heading.util.js +24 -0
- package/lib/src/brand-page/utils/text.util.d.ts +1 -1
- package/lib/src/brand-page/utils/text.util.js +32 -0
- package/lib/src/brand-page/utils/vcard.util.js +4 -4
- package/lib/src/brand-page/utils/view-model.util.d.ts +1 -0
- package/lib/src/brand-page/utils/view-model.util.js +68 -21
- package/lib/src/brand-page/view-models.d.ts +3 -3
- package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.d.ts +22 -0
- package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.js +22 -0
- package/lib/src/brand-page-maker/assessors/contact-form-visibility.assessor.d.ts +0 -1
- package/lib/src/brand-page-maker/assessors/contact-form-visibility.assessor.js +0 -6
- package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +1 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.d.ts +14 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.js +39 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +28 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.js +66 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +21 -100
- package/lib/src/brand-page-maker/constants/config-settings-constant.js +3 -40
- package/lib/src/brand-page-maker/default-getters/index.d.ts +0 -1
- package/lib/src/brand-page-maker/default-getters/index.js +0 -1
- package/lib/src/brand-page-maker/models/button-styles-config.js +3 -15
- package/lib/src/brand-page-maker/models/content-settings-configs.d.ts +1 -1
- package/lib/src/brand-page-maker/models/content-settings.js +3 -9
- package/lib/src/brand-page-maker/utils/index.d.ts +2 -0
- package/lib/src/brand-page-maker/utils/index.js +2 -0
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +19 -5
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +35 -5
- package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +33 -0
- package/lib/src/brand-page-maker/utils/rte-render.util.js +50 -0
- package/lib/src/brand-page-maker/utils/rte-sanitize.util.d.ts +6 -0
- package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +158 -0
- package/lib/src/brand-page-maker/utils/rte-serializer.d.ts +5 -0
- package/lib/src/brand-page-maker/utils/rte-serializer.js +112 -0
- package/lib/src/brand-page-maker/validators/field.validator.js +10 -2
- package/lib/src/css/library.brand-page-brandCrowd.min.css +189 -217
- package/lib/src/css/library.brand-page-brandPage.min.css +189 -217
- package/lib/src/css/library.brand-page-designCom.min.css +189 -217
- package/lib/src/css/library.brand-page-preview.min.css +189 -217
- package/lib/src/index.d.ts +1 -0
- package/lib/src/index.mjs +143 -138
- package/lib/src/{lib-C-uekd4C.js → lib-Bm-fYap1.js} +43780 -43825
- package/lib/src/lib.js +2 -0
- package/lib/src/metadata-builder/builders/update-content-text-builder.js +20 -15
- package/lib/src/metadata-builder/builders/update-content-text-style-builder.js +8 -3
- package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +2 -2
- package/lib/src/metadata-builder/helpers/rte.helpers.js +6 -3
- package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +14 -1
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +12 -8
- package/lib/src/utils/minify-brand-page-metadata.js +4 -1
- package/package.json +2 -2
- package/src/brand-page/components/about-section/AboutSection.vue +1 -1
- package/src/brand-page/components/about-section/AboutSection.vue.test.ts +11 -0
- package/src/brand-page/components/banner-section/BannerSection.vue +5 -5
- package/src/brand-page/components/booking-section/BookingSection.vue +1 -1
- package/src/brand-page/components/button/ButtonStyleWrapper.vue +44 -112
- package/src/brand-page/components/collapsible/Collapsible.mixin.ts +1 -14
- package/src/brand-page/components/contact-form/ContactForm.mixin.test.ts +0 -18
- package/src/brand-page/components/contact-form/ContactForm.mixin.ts +8 -17
- package/src/brand-page/components/contact-form/ContactForm.vue +39 -100
- package/src/brand-page/components/mailing-list/MailingList.mixin.ts +0 -6
- package/src/brand-page/components/mailing-list/MailingList.vue +11 -19
- package/src/brand-page/components/media/Media.mixin.ts +13 -0
- package/src/brand-page/components/media/Media.test.ts +65 -0
- package/src/brand-page/components/media/Media.vue +1 -1
- package/src/brand-page/components/page/Page.mixin.ts +8 -1
- package/src/brand-page/components/page/Page.vue +8 -0
- package/src/brand-page/components/page/Page.vue.test.ts +142 -0
- package/src/brand-page/components/payment/Payment.mixin.ts +0 -6
- package/src/brand-page/components/payment/Payment.vue +25 -44
- package/src/brand-page/components/payment/RequestPaymentStripe.vue +22 -26
- package/src/brand-page/components/profile/Profile.vue +1 -1
- package/src/brand-page/components/shop/Shop.vue +6 -15
- package/src/brand-page/components/social-links/SocialLinks.vue +1 -1
- package/src/brand-page/components/testimonial-section/TestimonialSection.vue +1 -1
- package/src/brand-page/components/text-media-section/TextMediaSection.vue +1 -1
- package/src/brand-page/components/text-wrapper/TextWrapper.vue +23 -7
- package/src/brand-page/models/index.ts +35 -27
- package/src/brand-page/models/payment.model.ts +0 -2
- package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
- package/src/brand-page/utils/button.util.ts +0 -17
- package/src/brand-page/utils/image.util.test.ts +25 -0
- package/src/brand-page/utils/index.ts +1 -0
- package/src/brand-page/utils/migration/migration-19.test.ts +47 -90
- package/src/brand-page/utils/migration/migration-19.ts +61 -34
- package/src/brand-page/utils/migration.util.ts +5 -10
- package/src/brand-page/utils/page-heading.util.test.ts +116 -0
- package/src/brand-page/utils/page-heading.util.ts +33 -0
- package/src/brand-page/utils/style.util.test.ts +51 -0
- package/src/brand-page/utils/text.util.test.ts +47 -0
- package/src/brand-page/utils/text.util.ts +43 -4
- package/src/brand-page/utils/vcard.util.ts +6 -4
- package/src/brand-page/utils/view-model.pageid-rte.test.ts +104 -0
- package/src/brand-page/utils/view-model.util.ts +94 -27
- package/src/brand-page/view-models.ts +3 -2
- package/src/brand-page-maker/__fixtures__/complex-rte.fixture.ts +23 -0
- package/src/brand-page-maker/assessors/contact-form-visibility.assessor.ts +0 -11
- package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.test.ts +52 -0
- package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.ts +42 -0
- package/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.ts +113 -0
- package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +23 -107
- package/src/brand-page-maker/constants/config-settings-constant.ts +2 -43
- package/src/brand-page-maker/default-getters/index.ts +0 -1
- package/src/brand-page-maker/models/button-styles-config.ts +3 -15
- package/src/brand-page-maker/models/content-settings-configs.ts +1 -1
- package/src/brand-page-maker/models/content-settings.ts +3 -10
- package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +200 -0
- package/src/brand-page-maker/utils/index.ts +2 -0
- package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +4 -3
- package/src/brand-page-maker/utils/rich-text-editor.util.ts +50 -11
- package/src/brand-page-maker/utils/rte-features.test.ts +330 -0
- package/src/brand-page-maker/utils/rte-render-parity.test.ts +156 -0
- package/src/brand-page-maker/utils/rte-render.util.ts +77 -0
- package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +158 -0
- package/src/brand-page-maker/utils/rte-sanitize.util.ts +195 -0
- package/src/brand-page-maker/utils/rte-serializer.ts +172 -0
- package/src/brand-page-maker/validators/field.validator.test.ts +55 -0
- package/src/brand-page-maker/validators/field.validator.ts +10 -2
- package/src/lib.ts +9 -0
- package/src/metadata-builder/builders/update-content-text-builder.ts +32 -18
- package/src/metadata-builder/builders/update-content-text-style-builder.ts +14 -4
- package/src/metadata-builder/helpers/rte.helpers.ts +11 -7
- package/src/shared/content-settings/components/GenericTextInput.mixin.ts +17 -1
- package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +16 -9
- package/src/shared/logo-settings/components/EditLogoSettings.vue +1 -0
- package/src/shared/style-settings/components/StyleSettings.vue +0 -1
- package/src/utils/minify-brand-page-metadata.ts +4 -1
- package/lib/src/brand-page/utils/migration/migration-20.d.ts +0 -2
- package/lib/src/brand-page/utils/migration/migration-20.js +0 -24
- package/lib/src/brand-page-maker/default-getters/contact-form-card-color.default-getter.d.ts +0 -8
- package/lib/src/brand-page-maker/default-getters/contact-form-card-color.default-getter.js +0 -16
- package/src/brand-page/components/button/ButtonStyleWrapper.vue.test.ts +0 -167
- package/src/brand-page/utils/migration/migration-20.test.ts +0 -118
- package/src/brand-page/utils/migration/migration-20.ts +0 -54
- package/src/brand-page-maker/default-getters/contact-form-card-color.default-getter.ts +0 -23
|
@@ -1,40 +1,71 @@
|
|
|
1
|
+
import { hasRTEContent } from '../../../brand-page-maker/utils';
|
|
2
|
+
import { useExternalRichTextEditor } from '../../../brand-page-maker/composables/rich-text-editor/useRichTextEditor';
|
|
1
3
|
/**
|
|
2
|
-
* BP-
|
|
4
|
+
* BP-5787 — migrate existing inline-HTML RTE content to Tiptap JSON.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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.
|
|
8
12
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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).
|
|
12
23
|
*/
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
+
};
|
|
18
43
|
export const migration19 = async (migrationResult) => {
|
|
19
44
|
const { brandPageModel } = migrationResult;
|
|
20
|
-
|
|
21
|
-
|
|
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') {
|
|
22
51
|
return migrationResult;
|
|
23
52
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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);
|
|
32
64
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
externalEditor.destroy();
|
|
69
|
+
}
|
|
39
70
|
return migrationResult;
|
|
40
71
|
};
|
|
@@ -22,7 +22,6 @@ import { migration16 } from './migration/migration-16';
|
|
|
22
22
|
import { migration17 } from './migration/migration-17';
|
|
23
23
|
import { migration18 } from './migration/migration-18';
|
|
24
24
|
import { migration19 } from './migration/migration-19';
|
|
25
|
-
import { migration20 } from './migration/migration-20';
|
|
26
25
|
const MIGRATION_TABLE = [
|
|
27
26
|
{
|
|
28
27
|
version: 1,
|
|
@@ -143,15 +142,12 @@ const MIGRATION_TABLE = [
|
|
|
143
142
|
{
|
|
144
143
|
version: 19,
|
|
145
144
|
migrateFn: migration19,
|
|
146
|
-
// runAlways:
|
|
147
|
-
//
|
|
145
|
+
// runAlways: normalise RTE to JSON on every load. Idempotent (only HTML
|
|
146
|
+
// strings are converted; JSON is skipped), so it also catches content the
|
|
147
|
+
// AI section builders still emit as HTML — converting it on the next read
|
|
148
|
+
// without a new migration or changes to those builders.
|
|
148
149
|
runAlways: true,
|
|
149
|
-
description: 'BP-
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
version: 20,
|
|
153
|
-
migrateFn: migration20,
|
|
154
|
-
description: 'BP-5800: contact-form BP/BC container colour moves to cardStyle — seed cardStyle from the button fill (its long-standing rendered colour) so existing sites render unchanged',
|
|
150
|
+
description: 'BP-5787: convert inline-HTML RTE content to Tiptap JSON on load (lazy migrate-on-read for web-renderer; persisted on next maker save; also normalises AI-built HTML)',
|
|
155
151
|
},
|
|
156
152
|
];
|
|
157
153
|
export const GetLatestModelVersion = () => Math.max(...MIGRATION_TABLE.map((x) => x.version));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BrandPageContext } from '../models';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves the text for the single page-level `<h1>` a published site renders
|
|
4
|
+
* (BP-5866 — Bing rejected indexing because every section emitted its own
|
|
5
|
+
* `<h1>`; the page now owns exactly one).
|
|
6
|
+
*
|
|
7
|
+
* Mirrors the SSR `<title>` rule so the crawlable heading matches the document
|
|
8
|
+
* title: a sub-page reads `"{business} - {page}"`, while the home/root page
|
|
9
|
+
* reads just the business/site name — never the weak, non-descriptive literal
|
|
10
|
+
* "Home". Falls back to the page name, then the slug, when the business name
|
|
11
|
+
* is unset.
|
|
12
|
+
*/
|
|
13
|
+
export declare const getPageHeadingText: (context: BrandPageContext) => string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { findNavItemByRelativePath } from './route.util';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves the text for the single page-level `<h1>` a published site renders
|
|
4
|
+
* (BP-5866 — Bing rejected indexing because every section emitted its own
|
|
5
|
+
* `<h1>`; the page now owns exactly one).
|
|
6
|
+
*
|
|
7
|
+
* Mirrors the SSR `<title>` rule so the crawlable heading matches the document
|
|
8
|
+
* title: a sub-page reads `"{business} - {page}"`, while the home/root page
|
|
9
|
+
* reads just the business/site name — never the weak, non-descriptive literal
|
|
10
|
+
* "Home". Falls back to the page name, then the slug, when the business name
|
|
11
|
+
* is unset.
|
|
12
|
+
*/
|
|
13
|
+
export const getPageHeadingText = (context) => {
|
|
14
|
+
const businessName = (context.pageTitle || '').trim();
|
|
15
|
+
const navItem = findNavItemByRelativePath(context.nav?.navItems || [], context.currentPageRelativePath || '');
|
|
16
|
+
const pageName = (navItem?.displayName || '').trim();
|
|
17
|
+
const isSubPage = Boolean(navItem && !navItem.isRoot && pageName);
|
|
18
|
+
if (businessName) {
|
|
19
|
+
return isSubPage ? `${businessName} - ${pageName}` : businessName;
|
|
20
|
+
}
|
|
21
|
+
// No business name: use the sub-page's name, else the slug — never the
|
|
22
|
+
// root page's own display name (usually the literal "Home").
|
|
23
|
+
return (isSubPage ? pageName : '') || context.brandPageSlug || '';
|
|
24
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const getRenderText: (text: string) => string;
|
|
2
|
-
export declare const escapeContentText: (text:
|
|
2
|
+
export declare const escapeContentText: (text: unknown) => string | undefined;
|
|
3
3
|
export declare const decodeHtmlEntities: (text: string) => string;
|
|
4
4
|
export declare const unescapeContentText: (text: string) => string;
|
|
5
5
|
export declare const encodeContentText: (text: string | undefined) => string | undefined;
|
|
@@ -1,11 +1,43 @@
|
|
|
1
1
|
import { ESCAPED_NEWLINE_CHARACTER } from '../../constants/ui';
|
|
2
|
+
/**
|
|
3
|
+
* BP-5787 — a Tiptap JSON document is an object with `type: 'doc'`.
|
|
4
|
+
* Inlined here (rather than importing from brand-page-maker) to avoid a
|
|
5
|
+
* circular import: rich-text-editor.util imports getRenderText from this file.
|
|
6
|
+
*/
|
|
7
|
+
const isTiptapDoc = (value) => typeof value === 'object' &&
|
|
8
|
+
value !== null &&
|
|
9
|
+
value.type === 'doc';
|
|
10
|
+
const jsonDocPlainText = (doc) => {
|
|
11
|
+
const parts = [];
|
|
12
|
+
const walk = (node) => {
|
|
13
|
+
if (typeof node?.text === 'string')
|
|
14
|
+
parts.push(node.text);
|
|
15
|
+
node?.content?.forEach(walk);
|
|
16
|
+
};
|
|
17
|
+
walk(doc);
|
|
18
|
+
return parts.join(' ').replace(/\s+/g, ' ').trim();
|
|
19
|
+
};
|
|
2
20
|
export const getRenderText = (text) => {
|
|
21
|
+
// BP-5787: RTE fields may now be Tiptap JSON objects. Non-strings pass
|
|
22
|
+
// through untouched so the renderer (TextWrapper) handles them via its
|
|
23
|
+
// JSON-aware path, instead of throwing on `.replace`.
|
|
24
|
+
if (typeof text !== 'string') {
|
|
25
|
+
return text;
|
|
26
|
+
}
|
|
3
27
|
return text.replace(new RegExp(`(${ESCAPED_NEWLINE_CHARACTER}|\\n)`, 'g'), '<br/>');
|
|
4
28
|
};
|
|
29
|
+
// Accepts a legacy inline-HTML string or a Tiptap JSON document (BP-5787):
|
|
30
|
+
// for JSON it returns the concatenated visible text; for HTML it strips tags.
|
|
5
31
|
export const escapeContentText = (text) => {
|
|
6
32
|
if (!text) {
|
|
7
33
|
return text;
|
|
8
34
|
}
|
|
35
|
+
if (isTiptapDoc(text)) {
|
|
36
|
+
return jsonDocPlainText(text);
|
|
37
|
+
}
|
|
38
|
+
if (typeof text !== 'string') {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
9
41
|
return text
|
|
10
42
|
.replace(/(<([^>]+)>)/gi, '')
|
|
11
43
|
.replace(/(\r\n|\r|\n)/g, ESCAPED_NEWLINE_CHARACTER);
|
|
@@ -150,9 +150,9 @@ const processSocialLinks = (socialLinksContent, vcardLines, addedEntries) => {
|
|
|
150
150
|
});
|
|
151
151
|
};
|
|
152
152
|
const addProfileInfo = (profile, vcardLines) => {
|
|
153
|
-
const name = sanitizeForVCard(profile.name || '');
|
|
154
|
-
const title = sanitizeForVCard(profile.title || '');
|
|
155
|
-
const description = sanitizeForVCard(profile.description || '');
|
|
153
|
+
const name = sanitizeForVCard(getPlainTextFromRTE(profile.name) || '');
|
|
154
|
+
const title = sanitizeForVCard(getPlainTextFromRTE(profile.title) || '');
|
|
155
|
+
const description = sanitizeForVCard(getPlainTextFromRTE(profile.description) || '');
|
|
156
156
|
if (name) {
|
|
157
157
|
const nameParts = name.trim().split(/\s+/);
|
|
158
158
|
const lastName = nameParts.length > 1 ? nameParts.pop() || '' : '';
|
|
@@ -205,6 +205,6 @@ const downloadVCard = (vcard, filename) => {
|
|
|
205
205
|
export const saveContact = (brandPageData) => {
|
|
206
206
|
const vcard = generateVCard(brandPageData);
|
|
207
207
|
const profile = findProfileContent(brandPageData);
|
|
208
|
-
const filename = generateVCardFilename(profile?.name);
|
|
208
|
+
const filename = generateVCardFilename(getPlainTextFromRTE(profile?.name));
|
|
209
209
|
downloadVCard(vcard, filename);
|
|
210
210
|
};
|
|
@@ -28,6 +28,7 @@ interface ViewModelMappingOptions {
|
|
|
28
28
|
setVideoUrlsWithToken?: (token: string, videoUrl: string, previewImageUrl: string, assetId: string) => void;
|
|
29
29
|
}
|
|
30
30
|
export declare const getBrandPageViewModelFromBrandPageModel: (model: BrandPageModel, brandPageToken: string | undefined, showBrandCrowdLogo: boolean | undefined, options: ViewModelMappingOptions) => BrandPageViewModel;
|
|
31
|
+
export declare const resolvePageIdsInRteJson: (doc: unknown, nav: NavViewModel, options: ViewModelMappingOptions) => unknown;
|
|
31
32
|
export declare const isLogoViewModel: (viewModel: ContentViewModel) => viewModel is LogoViewModel;
|
|
32
33
|
/** for service.brand-page website preview by logo draft token */
|
|
33
34
|
export declare const applyLogoImageToBrandPageViewModel: (viewModel: BrandPageViewModel, logoImage: LogoImageModel) => BrandPageViewModel;
|
|
@@ -10,7 +10,7 @@ import { isValidSvgUrl } from './image.util';
|
|
|
10
10
|
import { isLogoContent } from './model.util';
|
|
11
11
|
import { getBoxShadowValue } from './button.util';
|
|
12
12
|
import { findNavItemByPageId, getRouteNameByPageIndex, getRouteParamsByPage, } from './route.util';
|
|
13
|
-
import { hasRTEContent } from '../../brand-page-maker/utils';
|
|
13
|
+
import { hasRTEContent, isTiptapDocument } from '../../brand-page-maker/utils';
|
|
14
14
|
import { getPageIdFromHref, isHrefPageId, } from '../../brand-page-maker/composables/rich-text-editor/hyperlink';
|
|
15
15
|
const BUTTON_BORDER_RADIUS = '9999px';
|
|
16
16
|
const CONTENT_BORDER_RADIUS = '48px';
|
|
@@ -526,7 +526,6 @@ const mapToShapesSectionViewModel = (content, styles, brandPageType) => {
|
|
|
526
526
|
};
|
|
527
527
|
const mapToShopViewModel = (content, styles) => {
|
|
528
528
|
const buttonStyles = new ButtonStyles({
|
|
529
|
-
selectedButtonStyleId: styles.buttonStyles.selectedButtonStyleId,
|
|
530
529
|
style: {
|
|
531
530
|
...styles.buttonStyles.style,
|
|
532
531
|
},
|
|
@@ -864,13 +863,75 @@ const mapPageIdReferencesToRelativePaths = (pages, nav, options) => {
|
|
|
864
863
|
});
|
|
865
864
|
});
|
|
866
865
|
};
|
|
866
|
+
// Resolve a `pageid:<id>` internal-link href to the published relative path.
|
|
867
|
+
// Returns the resolved href, or null when `href` is not an internal pageid link
|
|
868
|
+
// (external/absolute links are left untouched). Falls back to the site root when
|
|
869
|
+
// the pageid can't be matched to a nav route.
|
|
870
|
+
const resolvePageIdHref = (href, nav, options) => {
|
|
871
|
+
// Only process internal links (no protocol, doesn't start with /)
|
|
872
|
+
if (!href || href.match(/^(https?:\/\/|\/)/) || !isHrefPageId(href)) {
|
|
873
|
+
return null;
|
|
874
|
+
}
|
|
875
|
+
const pageId = getPageIdFromHref(href);
|
|
876
|
+
const route = findNavItemByPageId(nav.navItems, pageId);
|
|
877
|
+
const brandPageSlugRoute = options.brandPageSlug
|
|
878
|
+
? `/${options.brandPageSlug}`
|
|
879
|
+
: '/';
|
|
880
|
+
if (route) {
|
|
881
|
+
if (route.isRoot) {
|
|
882
|
+
return brandPageSlugRoute;
|
|
883
|
+
}
|
|
884
|
+
return `${brandPageSlugRoute}${brandPageSlugRoute === '/' ? '' : '/'}${route.relativePath}`;
|
|
885
|
+
}
|
|
886
|
+
// fallback to root if pageid cannot be resolved.
|
|
887
|
+
return brandPageSlugRoute;
|
|
888
|
+
};
|
|
889
|
+
// BP-5847: walk a Tiptap JSON doc and rewrite every link mark whose href is a
|
|
890
|
+
// `pageid:` reference into its published relative path. Returns a NEW doc when
|
|
891
|
+
// something changed (so the shared model doc is never mutated), else the same
|
|
892
|
+
// reference.
|
|
893
|
+
export const resolvePageIdsInRteJson = (doc, nav, options) => {
|
|
894
|
+
let changed = false;
|
|
895
|
+
const walk = (node) => {
|
|
896
|
+
if (!node || typeof node !== 'object') {
|
|
897
|
+
return node;
|
|
898
|
+
}
|
|
899
|
+
let next = node;
|
|
900
|
+
if (Array.isArray(node.marks)) {
|
|
901
|
+
const marks = node.marks.map((mark) => {
|
|
902
|
+
if (mark?.type === 'link' && typeof mark.attrs?.href === 'string') {
|
|
903
|
+
const resolved = resolvePageIdHref(mark.attrs.href, nav, options);
|
|
904
|
+
if (resolved !== null) {
|
|
905
|
+
changed = true;
|
|
906
|
+
return { ...mark, attrs: { ...mark.attrs, href: resolved } };
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return mark;
|
|
910
|
+
});
|
|
911
|
+
next = { ...next, marks };
|
|
912
|
+
}
|
|
913
|
+
if (Array.isArray(node.content)) {
|
|
914
|
+
next = { ...next, content: node.content.map(walk) };
|
|
915
|
+
}
|
|
916
|
+
return next;
|
|
917
|
+
};
|
|
918
|
+
const result = walk(doc);
|
|
919
|
+
return changed ? result : doc;
|
|
920
|
+
};
|
|
867
921
|
const dynamicallyMapPageIdInHyperlinksToRelativePaths = (content, nav, options) => {
|
|
868
922
|
Object.keys(content).forEach((key) => {
|
|
869
923
|
const value = content[key];
|
|
870
924
|
if (value === undefined || value === null) {
|
|
871
925
|
return;
|
|
872
926
|
}
|
|
873
|
-
if (
|
|
927
|
+
if (isTiptapDocument(value)) {
|
|
928
|
+
// BP-5847: RTE fields may now be Tiptap JSON. Resolve pageid link marks
|
|
929
|
+
// here too, or internal links render as raw `pageid:` and don't navigate.
|
|
930
|
+
content[key] =
|
|
931
|
+
resolvePageIdsInRteJson(value, nav, options);
|
|
932
|
+
}
|
|
933
|
+
else if (typeof value === 'object' &&
|
|
934
|
+
(key === 'items' || key === 'textItems')) {
|
|
874
935
|
if (Array.isArray(value)) {
|
|
875
936
|
value.forEach((item) => {
|
|
876
937
|
dynamicallyMapPageIdInHyperlinksToRelativePaths(item, nav, options);
|
|
@@ -882,24 +943,10 @@ const dynamicallyMapPageIdInHyperlinksToRelativePaths = (content, nav, options)
|
|
|
882
943
|
}
|
|
883
944
|
else if (typeof value === 'string' && hasRTEContent(value)) {
|
|
884
945
|
content[key] = value.replace(/<a\s+([^>]*?)href="([^"]*?)"([^>]*?)>/gi, (match, existingAttrsStart, href, existingAttrsEnd) => {
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
const brandPageSlugRoute = options.brandPageSlug
|
|
890
|
-
? `/${options.brandPageSlug}`
|
|
891
|
-
: '/';
|
|
892
|
-
if (route) {
|
|
893
|
-
let href = `${brandPageSlugRoute}`;
|
|
894
|
-
if (!route.isRoot) {
|
|
895
|
-
href = `${brandPageSlugRoute}${brandPageSlugRoute === '/' ? '' : '/'}${route.relativePath}`;
|
|
896
|
-
}
|
|
897
|
-
return `<a ${existingAttrsStart}href="${href}"${existingAttrsEnd}>`;
|
|
898
|
-
}
|
|
899
|
-
// fallback to root if pageid cannot be resolved.
|
|
900
|
-
return `<a ${existingAttrsStart}href="${brandPageSlugRoute}"${existingAttrsEnd}>`;
|
|
901
|
-
}
|
|
902
|
-
return match;
|
|
946
|
+
const resolved = resolvePageIdHref(href, nav, options);
|
|
947
|
+
return resolved === null
|
|
948
|
+
? match
|
|
949
|
+
: `<a ${existingAttrsStart}href="${resolved}"${existingAttrsEnd}>`;
|
|
903
950
|
});
|
|
904
951
|
}
|
|
905
952
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ButtonWithModalContent, ProfileContent, TextContent, VideoContent, ImageContent, ContentType, ButtonStyles, LogoStyles, MapContent, ImageGalleryContent, GalleryStyles, MailingListContent, RequestPaymentContent, ShopContent, SupportMeContent, ContactFormContent, BannerSectionContent, BrandPageType, SocialLinksContent, TestimonialSectionContent, AboutSectionContent, DividerSectionContent, ShapesSectionContent, ButtonLinkType, PageHeaderMobile, PageHeaderDesktop, PageFooterMobile, PageFooterDesktop, TextMediaItem, TextShapeItem, TextStyleDeclaration, AspectRatio, LogoImageModel, LogoTemplateImageModel, CookieOptions, BookingSectionContent, MediaTypes, TextStyles, BackgroundStyle, BackgroundShape, PersonalisationRole, SaveContactOptions } from './models';
|
|
1
|
+
import type { ButtonWithModalContent, ProfileContent, TextContent, VideoContent, ImageContent, ContentType, ButtonStyles, LogoStyles, MapContent, ImageGalleryContent, GalleryStyles, MailingListContent, RequestPaymentContent, ShopContent, SupportMeContent, ContactFormContent, BannerSectionContent, BrandPageType, SocialLinksContent, TestimonialSectionContent, AboutSectionContent, DividerSectionContent, ShapesSectionContent, ButtonLinkType, PageHeaderMobile, PageHeaderDesktop, PageFooterMobile, PageFooterDesktop, TextMediaItem, TextShapeItem, TextStyleDeclaration, AspectRatio, LogoImageModel, LogoTemplateImageModel, CookieOptions, BookingSectionContent, MediaTypes, TextStyles, BackgroundStyle, BackgroundShape, PersonalisationRole, SaveContactOptions, RichTextValue } from './models';
|
|
2
2
|
import type { ImagesSectionContentDisplayStyle } from './constants';
|
|
3
3
|
export type ViewModelType = 'button' | 'image' | 'image-gallery' | 'logo' | 'map' | 'mailing-list' | 'profile' | 'support-me' | 'request-payment' | 'text' | 'video' | 'contact-form' | 'banner-section' | 'images-section' | 'about-section' | 'divider-section' | 'testimonial-section' | 'social-links' | 'shapes-section' | 'shop-section' | 'booking-section';
|
|
4
4
|
export interface ContentViewModel {
|
|
@@ -94,8 +94,8 @@ export interface TextImageSectionViewModel {
|
|
|
94
94
|
id: string;
|
|
95
95
|
type: 'images-section';
|
|
96
96
|
role: ContentType;
|
|
97
|
-
title:
|
|
98
|
-
description:
|
|
97
|
+
title: RichTextValue;
|
|
98
|
+
description: RichTextValue;
|
|
99
99
|
displayStyle: ImagesSectionContentDisplayStyle;
|
|
100
100
|
aspectRatio: AspectRatio;
|
|
101
101
|
areImagesFullWidth: boolean;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BP-5787 — golden-master fixture for the RTE HTML→JSON migration.
|
|
3
|
+
*
|
|
4
|
+
* A single rich-text value that exercises every mark/node the editor can emit,
|
|
5
|
+
* so one snapshot guards the whole serialisation surface. Captured as a baseline
|
|
6
|
+
* on the current HTML pipeline; after the migration the JSON pipeline must
|
|
7
|
+
* reproduce byte-identical rendered output (see rte-render-parity.test.ts).
|
|
8
|
+
*
|
|
9
|
+
* Coverage:
|
|
10
|
+
* - block nodes: heading (h2), paragraph, text-align (center / right)
|
|
11
|
+
* - inline marks: bold, italic, underline, strike
|
|
12
|
+
* - textStyle attrs: fontFamily, fontSize (em), letterSpacing (em), lineHeight (unitless)
|
|
13
|
+
* - colour: theme TOKEN ref (var(--logo-text-color)) AND a literal hex (#FF0000)
|
|
14
|
+
* - highlight (background colour via <mark>)
|
|
15
|
+
* - links: internal (target=_self) and external (target=_blank rel=noopener)
|
|
16
|
+
*/
|
|
17
|
+
export declare const COMPLEX_RTE_HTML = "<h2 style=\"text-align: center\"><span style=\"color: var(--logo-text-color)\">Themed heading</span></h2><p><strong>bold</strong> <em>italic</em> <u>underline</u> <s>strike</s> <span style=\"font-family: Montserrat; font-size: 1.5em; letter-spacing: 0.05em; line-height: 1.4\">styled span</span> <mark data-color=\"#ffe08a\" style=\"background-color: #ffe08a\">highlight</mark> <span style=\"color: #FF0000\">literal hex</span> <a target=\"_self\" href=\"/about\">internal link</a> <a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://example.com\">external link</a></p><p style=\"text-align: right\">second paragraph, right-aligned</p>";
|
|
18
|
+
/**
|
|
19
|
+
* The theme-token CSS variable that MUST survive the HTML→JSON round-trip
|
|
20
|
+
* (BP-5787 stores token refs instead of flattening them to rgb()).
|
|
21
|
+
*/
|
|
22
|
+
export declare const THEME_TOKEN_REF = "var(--logo-text-color)";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BP-5787 — golden-master fixture for the RTE HTML→JSON migration.
|
|
3
|
+
*
|
|
4
|
+
* A single rich-text value that exercises every mark/node the editor can emit,
|
|
5
|
+
* so one snapshot guards the whole serialisation surface. Captured as a baseline
|
|
6
|
+
* on the current HTML pipeline; after the migration the JSON pipeline must
|
|
7
|
+
* reproduce byte-identical rendered output (see rte-render-parity.test.ts).
|
|
8
|
+
*
|
|
9
|
+
* Coverage:
|
|
10
|
+
* - block nodes: heading (h2), paragraph, text-align (center / right)
|
|
11
|
+
* - inline marks: bold, italic, underline, strike
|
|
12
|
+
* - textStyle attrs: fontFamily, fontSize (em), letterSpacing (em), lineHeight (unitless)
|
|
13
|
+
* - colour: theme TOKEN ref (var(--logo-text-color)) AND a literal hex (#FF0000)
|
|
14
|
+
* - highlight (background colour via <mark>)
|
|
15
|
+
* - links: internal (target=_self) and external (target=_blank rel=noopener)
|
|
16
|
+
*/
|
|
17
|
+
export const COMPLEX_RTE_HTML = `<h2 style="text-align: center"><span style="color: var(--logo-text-color)">Themed heading</span></h2><p><strong>bold</strong> <em>italic</em> <u>underline</u> <s>strike</s> <span style="font-family: Montserrat; font-size: 1.5em; letter-spacing: 0.05em; line-height: 1.4">styled span</span> <mark data-color="#ffe08a" style="background-color: #ffe08a">highlight</mark> <span style="color: #FF0000">literal hex</span> <a target="_self" href="/about">internal link</a> <a target="_blank" rel="noopener noreferrer nofollow" href="https://example.com">external link</a></p><p style="text-align: right">second paragraph, right-aligned</p>`;
|
|
18
|
+
/**
|
|
19
|
+
* The theme-token CSS variable that MUST survive the HTML→JSON round-trip
|
|
20
|
+
* (BP-5787 stores token refs instead of flattening them to rgb()).
|
|
21
|
+
*/
|
|
22
|
+
export const THEME_TOKEN_REF = 'var(--logo-text-color)';
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { type ContactFormContent } from '@brand-page';
|
|
2
2
|
import type { VisibilityAssessor } from '../models';
|
|
3
3
|
export declare const contactCardDisplayTypeVisibilityAssessor: VisibilityAssessor<ContactFormContent | undefined>;
|
|
4
|
-
export declare const contactCardColorVisibilityAssessor: VisibilityAssessor<ContactFormContent | undefined>;
|
|
5
4
|
export declare const displayTypeVisibilityAssessor: VisibilityAssessor<ContactFormContent | undefined>;
|
|
6
5
|
export declare const imageUrlVisibilityAssessor: VisibilityAssessor<ContactFormContent | undefined>;
|
|
7
6
|
export declare const contactBackgroundDisplayTypeVisibilityAssessor: VisibilityAssessor<ContactFormContent | undefined>;
|
|
@@ -9,12 +9,6 @@ export const contactCardDisplayTypeVisibilityAssessor = (content, context) => {
|
|
|
9
9
|
ALIGN_RIGHT_FULL_IMAGE,
|
|
10
10
|
].includes(content?.layoutStyle)) && context.isBrandSite);
|
|
11
11
|
};
|
|
12
|
-
// Card colour picker: on brand-page/brand-contact it's the collapsible container
|
|
13
|
-
// colour (shown for every layout); on brand-site it's the card in card layouts.
|
|
14
|
-
export const contactCardColorVisibilityAssessor = (content, context) => {
|
|
15
|
-
return (!context.isBrandSite ||
|
|
16
|
-
contactCardDisplayTypeVisibilityAssessor(content, context));
|
|
17
|
-
};
|
|
18
12
|
export const displayTypeVisibilityAssessor = (content, context) => {
|
|
19
13
|
return Boolean(content?.layoutStyle === '' || context.isBrandPage);
|
|
20
14
|
};
|
package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts
CHANGED
|
@@ -1347,6 +1347,7 @@ declare const _default: import("vue").DefineComponent<{}, {
|
|
|
1347
1347
|
bpShowTopNavigationMobile(): boolean;
|
|
1348
1348
|
bpShowTopNavigationDesktop(): boolean;
|
|
1349
1349
|
slug(): string;
|
|
1350
|
+
pageHeadingText(): string;
|
|
1350
1351
|
shouldShowSaveContactWidget(): boolean;
|
|
1351
1352
|
isPremiumSubscription(): boolean;
|
|
1352
1353
|
shouldShowPopupBranding(): boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
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>;
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Extensions } from '@tiptap/vue-3';
|
|
2
|
+
/**
|
|
3
|
+
* Single source of truth for the Tiptap extension set (BP-5787).
|
|
4
|
+
*
|
|
5
|
+
* The live editor, the headless serialiser (useExternalRichTextEditor), and
|
|
6
|
+
* the upcoming render-from-JSON path must all use the SAME extensions, or
|
|
7
|
+
* rendered output drifts from what the user authored. This factory removes the
|
|
8
|
+
* previous duplication while preserving each consumer's exact configuration.
|
|
9
|
+
*
|
|
10
|
+
* Known, intentional differences are parameterised:
|
|
11
|
+
* - `restrictStarterKit` — the live editor disables list/hr/code nodes.
|
|
12
|
+
* - `includeLinkRel` — the headless/render path emits rel="noopener noreferrer";
|
|
13
|
+
* the live editing surface does not.
|
|
14
|
+
* - `editing` — only the live editor carries the Placeholder + MaxLength plugins
|
|
15
|
+
* (neither affects serialised output; they are editing-time only).
|
|
16
|
+
*/
|
|
17
|
+
export interface RteExtensionOptions {
|
|
18
|
+
/** Disable StarterKit nodes the editor doesn't use (lists, hr, code). */
|
|
19
|
+
restrictStarterKit?: boolean;
|
|
20
|
+
/** Emit rel="noopener noreferrer" on link HTMLAttributes (render path). */
|
|
21
|
+
includeLinkRel?: boolean;
|
|
22
|
+
/** When present, add the live-editing-only plugins (Placeholder, MaxLength). */
|
|
23
|
+
editing?: {
|
|
24
|
+
placeholder?: string;
|
|
25
|
+
maxLength?: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export declare const getRteExtensions: (options?: RteExtensionOptions) => Extensions;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
2
|
+
import Underline from '@tiptap/extension-underline';
|
|
3
|
+
import TextStyle from '@tiptap/extension-text-style';
|
|
4
|
+
import TextAlign from '@tiptap/extension-text-align';
|
|
5
|
+
import 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
|
+
};
|