@designcrowd/library.brand-page 6.0.19 → 6.0.20
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-DCwwdYPj.js → CreatedOnBrandCrowd-de-de-C52IyXZu.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-DTmZeDQ1.js → CreatedOnBrandCrowd-en-us-DhTy_dEV.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-Dbrz6TXv.js → CreatedOnBrandCrowd-es-es-BJelIAQ4.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-DLQhgn8T.js → CreatedOnBrandCrowd-fr-ca-DwJohy0T.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-DLQhgn8T.js → CreatedOnBrandCrowd-fr-fr-DwJohy0T.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-yAzzD2ik.js → CreatedOnBrandCrowd-pt-br-CMzKbxdG.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-yAzzD2ik.js → CreatedOnBrandCrowd-pt-pt-CMzKbxdG.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-CNmNNE5o.js → CreatedOnDcom-de-de-DAy1GHrw.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-BUmLFlS_.js → CreatedOnDcom-en-us-CgBDAkJN.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-DfygM_0q.js → CreatedOnDcom-es-es-Ds9d5rem.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-DIEft8xE.js → CreatedOnDcom-fr-ca-CZL0BQ-u.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-Cn3jWG2n.js → CreatedOnDcom-fr-fr-BVYh5Wvo.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-DXhgGwEh.js → CreatedOnDcom-pt-br-1hsWNXfq.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-BvE9ckw1.js → CreatedOnDcom-pt-pt-DvM0N-3M.js} +1 -1
- package/lib/src/brand-page/components/contact-form/ContactForm.mixin.js +2 -5
- package/lib/src/brand-page/models/index.d.ts +26 -34
- 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/composables/rich-text-editor/useRichTextEditor.js +100 -21
- 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 +138 -143
- package/lib/src/{lib-Bm-fYap1.js → lib-CK2FPBv5.js} +55098 -55367
- package/lib/src/lib.js +0 -2
- 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 -8
- package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +2 -2
- 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-item/ContentSettingsItemField.mixin.js +3 -12
- package/lib/src/utils/minify-brand-page-metadata.js +1 -4
- package/package.json +1 -1
- package/src/brand-page/components/contact-form/ContactForm.mixin.ts +2 -5
- 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/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/composables/rich-text-editor/useRichTextEditor.ts +107 -23
- 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/update-content-text-builder.ts +18 -32
- package/src/metadata-builder/builders/update-content-text-style-builder.ts +4 -14
- 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/utils/minify-brand-page-metadata.ts +1 -4
- package/lib/src/brand-page/utils/migration/migration-19.d.ts +0 -2
- package/lib/src/brand-page/utils/migration/migration-19.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-19.test.ts +0 -72
- package/src/brand-page/utils/migration/migration-19.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-features.test.ts +0 -330
- 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
|
@@ -119,7 +119,7 @@ import {
|
|
|
119
119
|
getRouteNameByPageIndex,
|
|
120
120
|
getRouteParamsByPage,
|
|
121
121
|
} from './route.util';
|
|
122
|
-
import { hasRTEContent
|
|
122
|
+
import { hasRTEContent } from '../../brand-page-maker/utils';
|
|
123
123
|
import {
|
|
124
124
|
getPageIdFromHref,
|
|
125
125
|
isHrefPageId,
|
|
@@ -1518,86 +1518,6 @@ const mapPageIdReferencesToRelativePaths = (
|
|
|
1518
1518
|
});
|
|
1519
1519
|
};
|
|
1520
1520
|
|
|
1521
|
-
// Resolve a `pageid:<id>` internal-link href to the published relative path.
|
|
1522
|
-
// Returns the resolved href, or null when `href` is not an internal pageid link
|
|
1523
|
-
// (external/absolute links are left untouched). Falls back to the site root when
|
|
1524
|
-
// the pageid can't be matched to a nav route.
|
|
1525
|
-
const resolvePageIdHref = (
|
|
1526
|
-
href: string,
|
|
1527
|
-
nav: NavViewModel,
|
|
1528
|
-
options: ViewModelMappingOptions,
|
|
1529
|
-
): string | null => {
|
|
1530
|
-
// Only process internal links (no protocol, doesn't start with /)
|
|
1531
|
-
if (!href || href.match(/^(https?:\/\/|\/)/) || !isHrefPageId(href)) {
|
|
1532
|
-
return null;
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
const pageId = getPageIdFromHref(href);
|
|
1536
|
-
const route = findNavItemByPageId(nav.navItems, pageId);
|
|
1537
|
-
const brandPageSlugRoute = options.brandPageSlug
|
|
1538
|
-
? `/${options.brandPageSlug}`
|
|
1539
|
-
: '/';
|
|
1540
|
-
|
|
1541
|
-
if (route) {
|
|
1542
|
-
if (route.isRoot) {
|
|
1543
|
-
return brandPageSlugRoute;
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
return `${brandPageSlugRoute}${brandPageSlugRoute === '/' ? '' : '/'}${route.relativePath}`;
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
// fallback to root if pageid cannot be resolved.
|
|
1550
|
-
return brandPageSlugRoute;
|
|
1551
|
-
};
|
|
1552
|
-
|
|
1553
|
-
// BP-5847: walk a Tiptap JSON doc and rewrite every link mark whose href is a
|
|
1554
|
-
// `pageid:` reference into its published relative path. Returns a NEW doc when
|
|
1555
|
-
// something changed (so the shared model doc is never mutated), else the same
|
|
1556
|
-
// reference.
|
|
1557
|
-
export const resolvePageIdsInRteJson = (
|
|
1558
|
-
doc: unknown,
|
|
1559
|
-
nav: NavViewModel,
|
|
1560
|
-
options: ViewModelMappingOptions,
|
|
1561
|
-
): unknown => {
|
|
1562
|
-
let changed = false;
|
|
1563
|
-
|
|
1564
|
-
const walk = (node: any): any => {
|
|
1565
|
-
if (!node || typeof node !== 'object') {
|
|
1566
|
-
return node;
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
let next = node;
|
|
1570
|
-
|
|
1571
|
-
if (Array.isArray(node.marks)) {
|
|
1572
|
-
const marks = node.marks.map((mark: any) => {
|
|
1573
|
-
if (mark?.type === 'link' && typeof mark.attrs?.href === 'string') {
|
|
1574
|
-
const resolved = resolvePageIdHref(mark.attrs.href, nav, options);
|
|
1575
|
-
|
|
1576
|
-
if (resolved !== null) {
|
|
1577
|
-
changed = true;
|
|
1578
|
-
|
|
1579
|
-
return { ...mark, attrs: { ...mark.attrs, href: resolved } };
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
return mark;
|
|
1584
|
-
});
|
|
1585
|
-
|
|
1586
|
-
next = { ...next, marks };
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
if (Array.isArray(node.content)) {
|
|
1590
|
-
next = { ...next, content: node.content.map(walk) };
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
return next;
|
|
1594
|
-
};
|
|
1595
|
-
|
|
1596
|
-
const result = walk(doc);
|
|
1597
|
-
|
|
1598
|
-
return changed ? result : doc;
|
|
1599
|
-
};
|
|
1600
|
-
|
|
1601
1521
|
const dynamicallyMapPageIdInHyperlinksToRelativePaths = (
|
|
1602
1522
|
content: any,
|
|
1603
1523
|
nav: NavViewModel,
|
|
@@ -1610,15 +1530,7 @@ const dynamicallyMapPageIdInHyperlinksToRelativePaths = (
|
|
|
1610
1530
|
return;
|
|
1611
1531
|
}
|
|
1612
1532
|
|
|
1613
|
-
if (
|
|
1614
|
-
// BP-5847: RTE fields may now be Tiptap JSON. Resolve pageid link marks
|
|
1615
|
-
// here too, or internal links render as raw `pageid:` and don't navigate.
|
|
1616
|
-
(content as unknown as Record<string, unknown>)[key] =
|
|
1617
|
-
resolvePageIdsInRteJson(value, nav, options);
|
|
1618
|
-
} else if (
|
|
1619
|
-
typeof value === 'object' &&
|
|
1620
|
-
(key === 'items' || key === 'textItems')
|
|
1621
|
-
) {
|
|
1533
|
+
if (typeof value === 'object' && (key === 'items' || key === 'textItems')) {
|
|
1622
1534
|
if (Array.isArray(value)) {
|
|
1623
1535
|
value.forEach((item) => {
|
|
1624
1536
|
dynamicallyMapPageIdInHyperlinksToRelativePaths(item, nav, options);
|
|
@@ -1630,11 +1542,31 @@ const dynamicallyMapPageIdInHyperlinksToRelativePaths = (
|
|
|
1630
1542
|
(content as unknown as Record<string, unknown>)[key] = value.replace(
|
|
1631
1543
|
/<a\s+([^>]*?)href="([^"]*?)"([^>]*?)>/gi,
|
|
1632
1544
|
(match, existingAttrsStart, href, existingAttrsEnd) => {
|
|
1633
|
-
|
|
1545
|
+
// Only process internal links (no protocol, doesn't start with /)
|
|
1546
|
+
if (href && !href.match(/^(https?:\/\/|\/)/) && isHrefPageId(href)) {
|
|
1547
|
+
const pageId = getPageIdFromHref(href);
|
|
1548
|
+
|
|
1549
|
+
const route = findNavItemByPageId(nav.navItems, pageId);
|
|
1550
|
+
|
|
1551
|
+
const brandPageSlugRoute = options.brandPageSlug
|
|
1552
|
+
? `/${options.brandPageSlug}`
|
|
1553
|
+
: '/';
|
|
1554
|
+
|
|
1555
|
+
if (route) {
|
|
1556
|
+
let href = `${brandPageSlugRoute}`;
|
|
1557
|
+
|
|
1558
|
+
if (!route.isRoot) {
|
|
1559
|
+
href = `${brandPageSlugRoute}${brandPageSlugRoute === '/' ? '' : '/'}${route.relativePath}`;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
return `<a ${existingAttrsStart}href="${href}"${existingAttrsEnd}>`;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// fallback to root if pageid cannot be resolved.
|
|
1566
|
+
return `<a ${existingAttrsStart}href="${brandPageSlugRoute}"${existingAttrsEnd}>`;
|
|
1567
|
+
}
|
|
1634
1568
|
|
|
1635
|
-
return
|
|
1636
|
-
? match
|
|
1637
|
-
: `<a ${existingAttrsStart}href="${resolved}"${existingAttrsEnd}>`;
|
|
1569
|
+
return match;
|
|
1638
1570
|
},
|
|
1639
1571
|
);
|
|
1640
1572
|
}
|
|
@@ -41,7 +41,6 @@ import type {
|
|
|
41
41
|
BackgroundShape,
|
|
42
42
|
PersonalisationRole,
|
|
43
43
|
SaveContactOptions,
|
|
44
|
-
RichTextValue,
|
|
45
44
|
} from './models';
|
|
46
45
|
import type { ImagesSectionContentDisplayStyle } from './constants';
|
|
47
46
|
|
|
@@ -197,8 +196,8 @@ export interface TextImageSectionViewModel {
|
|
|
197
196
|
id: string;
|
|
198
197
|
type: 'images-section';
|
|
199
198
|
role: ContentType;
|
|
200
|
-
title:
|
|
201
|
-
description:
|
|
199
|
+
title: string;
|
|
200
|
+
description: string;
|
|
202
201
|
displayStyle: ImagesSectionContentDisplayStyle;
|
|
203
202
|
aspectRatio: AspectRatio;
|
|
204
203
|
areImagesFullWidth: boolean;
|
|
@@ -8,7 +8,18 @@ import {
|
|
|
8
8
|
watch,
|
|
9
9
|
} from 'vue';
|
|
10
10
|
import { Editor } from '@tiptap/vue-3';
|
|
11
|
-
import
|
|
11
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
12
|
+
import Underline from '@tiptap/extension-underline';
|
|
13
|
+
import TextStyle from '@tiptap/extension-text-style';
|
|
14
|
+
import TextAlign from '@tiptap/extension-text-align';
|
|
15
|
+
import FontFamily from '@tiptap/extension-font-family';
|
|
16
|
+
import ColorExtension from '@tiptap/extension-color';
|
|
17
|
+
import { FontSize } from './extensions/FontSize';
|
|
18
|
+
import { LetterSpacing } from './extensions/LetterSpacing';
|
|
19
|
+
import { LineHeight } from './extensions/LineHeight';
|
|
20
|
+
import Highlight from '@tiptap/extension-highlight';
|
|
21
|
+
import Placeholder from '@tiptap/extension-placeholder';
|
|
22
|
+
import Link from '@tiptap/extension-link';
|
|
12
23
|
import type { StyleTextFontFamilyOption } from '@shared/style-settings/models';
|
|
13
24
|
import { createFormattingActions } from './formatting';
|
|
14
25
|
import { useTextAlign } from './textAlign';
|
|
@@ -20,7 +31,7 @@ import {
|
|
|
20
31
|
emToPx,
|
|
21
32
|
isStyleChanged,
|
|
22
33
|
pxToEm,
|
|
23
|
-
|
|
34
|
+
sanitiseClientSideRTE,
|
|
24
35
|
skipStyleDetection,
|
|
25
36
|
} from '@brand-page-maker/utils';
|
|
26
37
|
import type { HyperlinkType, TextStyles } from '@brand-page';
|
|
@@ -38,7 +49,9 @@ import {
|
|
|
38
49
|
import { useUiContentSettingsStore } from '@shared/content-settings/store';
|
|
39
50
|
import { useHistoryStore } from '@brand-page-maker/stores/history/history-store';
|
|
40
51
|
import { AllSelection } from 'prosemirror-state';
|
|
41
|
-
import {
|
|
52
|
+
import { MaxLength } from './extensions/MaxLength';
|
|
53
|
+
import { AutoLink } from './extensions/AutoLink';
|
|
54
|
+
import { isHrefPageId, useHyperlink } from './hyperlink';
|
|
42
55
|
import { useBubbleMenu } from './bubbleMenu';
|
|
43
56
|
import {
|
|
44
57
|
hasLogoColorVar,
|
|
@@ -116,7 +129,44 @@ export const useExternalRichTextEditor = (): {
|
|
|
116
129
|
} => {
|
|
117
130
|
const externalEditor = new Editor({
|
|
118
131
|
editable: false,
|
|
119
|
-
extensions:
|
|
132
|
+
extensions: [
|
|
133
|
+
StarterKit,
|
|
134
|
+
Underline,
|
|
135
|
+
TextStyle.configure({ mergeNestedSpanStyles: true }),
|
|
136
|
+
TextAlign.configure({
|
|
137
|
+
types: ['heading', 'paragraph'],
|
|
138
|
+
}),
|
|
139
|
+
FontFamily.configure({
|
|
140
|
+
types: ['textStyle'],
|
|
141
|
+
}),
|
|
142
|
+
FontSize,
|
|
143
|
+
LetterSpacing,
|
|
144
|
+
LineHeight,
|
|
145
|
+
ColorExtension.configure({
|
|
146
|
+
types: ['textStyle'],
|
|
147
|
+
}),
|
|
148
|
+
Highlight.configure({
|
|
149
|
+
multicolor: true,
|
|
150
|
+
}),
|
|
151
|
+
Link.configure({
|
|
152
|
+
openOnClick: false,
|
|
153
|
+
defaultProtocol: 'https',
|
|
154
|
+
autolink: true,
|
|
155
|
+
HTMLAttributes: (attrs) => {
|
|
156
|
+
const href = attrs.href;
|
|
157
|
+
const isExternal =
|
|
158
|
+
href && (href.startsWith('http://') || href.startsWith('https://'));
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
rel: 'noopener noreferrer',
|
|
162
|
+
target: isExternal ? '_blank' : null,
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
isAllowedUri: (url, ctx) =>
|
|
166
|
+
ctx.defaultValidate(url) || isHrefPageId(url),
|
|
167
|
+
}),
|
|
168
|
+
AutoLink,
|
|
169
|
+
],
|
|
120
170
|
});
|
|
121
171
|
|
|
122
172
|
return {
|
|
@@ -164,13 +214,54 @@ export const useRichTextEditor = (
|
|
|
164
214
|
const editor = new Editor({
|
|
165
215
|
content: model.value ?? '',
|
|
166
216
|
editable: !props.readOnly,
|
|
167
|
-
extensions:
|
|
168
|
-
|
|
169
|
-
|
|
217
|
+
extensions: [
|
|
218
|
+
StarterKit.configure({
|
|
219
|
+
bulletList: false,
|
|
220
|
+
orderedList: false,
|
|
221
|
+
listItem: false,
|
|
222
|
+
horizontalRule: false,
|
|
223
|
+
codeBlock: false,
|
|
224
|
+
code: false,
|
|
225
|
+
}),
|
|
226
|
+
Underline,
|
|
227
|
+
TextStyle.configure({ mergeNestedSpanStyles: true }),
|
|
228
|
+
TextAlign.configure({
|
|
229
|
+
types: ['heading', 'paragraph'],
|
|
230
|
+
}),
|
|
231
|
+
FontFamily.configure({
|
|
232
|
+
types: ['textStyle'],
|
|
233
|
+
}),
|
|
234
|
+
FontSize,
|
|
235
|
+
LetterSpacing,
|
|
236
|
+
LineHeight,
|
|
237
|
+
ColorExtension.configure({
|
|
238
|
+
types: ['textStyle'],
|
|
239
|
+
}),
|
|
240
|
+
Highlight.configure({
|
|
241
|
+
multicolor: true,
|
|
242
|
+
}),
|
|
243
|
+
Placeholder.configure({
|
|
170
244
|
placeholder: props.placeholder,
|
|
171
|
-
|
|
172
|
-
},
|
|
173
|
-
|
|
245
|
+
}),
|
|
246
|
+
MaxLength.configure({ maxLength: props.maxLength }),
|
|
247
|
+
Link.configure({
|
|
248
|
+
openOnClick: false,
|
|
249
|
+
defaultProtocol: 'https',
|
|
250
|
+
autolink: true,
|
|
251
|
+
HTMLAttributes: (attrs) => {
|
|
252
|
+
const href = attrs.href;
|
|
253
|
+
const isExternal =
|
|
254
|
+
href && (href.startsWith('http://') || href.startsWith('https://'));
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
target: isExternal ? '_blank' : null,
|
|
258
|
+
};
|
|
259
|
+
},
|
|
260
|
+
isAllowedUri: (url, ctx) =>
|
|
261
|
+
ctx.defaultValidate(url) || isHrefPageId(url),
|
|
262
|
+
}),
|
|
263
|
+
AutoLink,
|
|
264
|
+
],
|
|
174
265
|
editorProps: {
|
|
175
266
|
handleDOMEvents: {
|
|
176
267
|
drop(view, event: DragEvent) {
|
|
@@ -381,13 +472,7 @@ export const useRichTextEditor = (
|
|
|
381
472
|
|
|
382
473
|
const isExternalMutation = updateRichTextEditor.value || appliedSinceLast;
|
|
383
474
|
|
|
384
|
-
|
|
385
|
-
// seen as "changed" against the editor's HTML (which would re-run
|
|
386
|
-
// setContent and reset the user's just-applied styles).
|
|
387
|
-
const hasChanged =
|
|
388
|
-
JSON.stringify(editor.getJSON()) !== JSON.stringify(newValue);
|
|
389
|
-
|
|
390
|
-
if (isExternalMutation && hasChanged) {
|
|
475
|
+
if (isExternalMutation && editor.getHTML() !== newValue) {
|
|
391
476
|
editor.commands.setContent(newValue, false);
|
|
392
477
|
updateRichTextEditor.value = false;
|
|
393
478
|
//text style type is not switching when reordering
|
|
@@ -418,13 +503,12 @@ export const useRichTextEditor = (
|
|
|
418
503
|
}
|
|
419
504
|
});
|
|
420
505
|
|
|
421
|
-
// Update model when editor content changes
|
|
422
|
-
// JSON (BP-5787). getJSON() is already in hand (the editor holds the doc) and
|
|
423
|
-
// is sanitised natively — no getHTML(), no DOMPurify, no extra editor — so
|
|
424
|
-
// the per-keystroke path allocates nothing heavy.
|
|
506
|
+
// Update model when editor content changes
|
|
425
507
|
editor.on('update', () => {
|
|
508
|
+
const sanitizedData = sanitiseClientSideRTE(editor.getHTML());
|
|
509
|
+
|
|
426
510
|
// An update fired while the editor is unfocused is TipTap rewriting the
|
|
427
|
-
// model back into its canonical
|
|
511
|
+
// model back into its canonical HTML during mount rehydration or a
|
|
428
512
|
// programmatic setContent — not a user edit. Tag the emit so the write
|
|
429
513
|
// folds into the current undo head instead of pushing a phantom entry
|
|
430
514
|
// (BP-5810). The flag travels WITH the value through the debounced
|
|
@@ -438,7 +522,7 @@ export const useRichTextEditor = (
|
|
|
438
522
|
// the RTE only mounts inside an already-open settings drawer and the user
|
|
439
523
|
// focuses it before formatting, so this doesn't occur — revisit if a
|
|
440
524
|
// consumer ever drives toolbar actions on an unfocused editor.
|
|
441
|
-
emit('update:modelValue',
|
|
525
|
+
emit('update:modelValue', sanitizedData.sanitizedHtml, {
|
|
442
526
|
isHydrationWrite: !editor.isFocused,
|
|
443
527
|
});
|
|
444
528
|
});
|
|
@@ -7,8 +7,6 @@ export * from './url.util';
|
|
|
7
7
|
export * from './slug.util';
|
|
8
8
|
export * from './text.util';
|
|
9
9
|
export * from './rich-text-editor.util';
|
|
10
|
-
export * from './rte-render.util';
|
|
11
|
-
export * from './rte-sanitize.util';
|
|
12
10
|
|
|
13
11
|
export const isColorMatched = (color: string | null | undefined): boolean => {
|
|
14
12
|
if (!color) return false;
|
|
@@ -76,7 +76,7 @@ describe('hasHTML', () => {
|
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
describe('getUpdatedHTMLFromEditor', () => {
|
|
79
|
-
it('applies style commands and returns updated
|
|
79
|
+
it('applies style commands and returns updated HTML', () => {
|
|
80
80
|
// Mock editor
|
|
81
81
|
const runMock = vi.fn();
|
|
82
82
|
const chain = {
|
|
@@ -119,7 +119,7 @@ describe('getUpdatedHTMLFromEditor', () => {
|
|
|
119
119
|
},
|
|
120
120
|
},
|
|
121
121
|
chain: () => chain as any,
|
|
122
|
-
|
|
122
|
+
getHTML: () => '<div>styled</div>',
|
|
123
123
|
} as unknown as Editor;
|
|
124
124
|
|
|
125
125
|
const newStyle = {
|
|
@@ -137,8 +137,7 @@ describe('getUpdatedHTMLFromEditor', () => {
|
|
|
137
137
|
|
|
138
138
|
expect(externalEditor.commands.setContent).toHaveBeenCalledWith('foo');
|
|
139
139
|
expect(runMock).toHaveBeenCalled();
|
|
140
|
-
|
|
141
|
-
expect(result).toEqual({ type: 'doc', content: [{ type: 'paragraph' }] });
|
|
140
|
+
expect(result).toBe('<div>styled</div>');
|
|
142
141
|
});
|
|
143
142
|
});
|
|
144
143
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChainedCommands, Editor
|
|
1
|
+
import type { ChainedCommands, Editor } from '@tiptap/vue-3';
|
|
2
2
|
import type { EditorState, Transaction, Selection } from 'prosemirror-state';
|
|
3
3
|
import {
|
|
4
4
|
STYLE_THEME_KEYS,
|
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
type RequestPaymentContent,
|
|
20
20
|
type SupportMeContent,
|
|
21
21
|
type ShopContent,
|
|
22
|
-
type RichTextValue,
|
|
23
22
|
} from '@brand-page';
|
|
24
23
|
import type { PageSettingsState } from '@shared/page-settings/store';
|
|
25
24
|
import type { StyleSettingsState } from '@shared/style-settings/store';
|
|
@@ -244,38 +243,8 @@ export const skipStyleDetection = (chain: ChainedCommands, marks?: Mark[]) =>
|
|
|
244
243
|
return true;
|
|
245
244
|
});
|
|
246
245
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
* document is an object with `type: 'doc'`.
|
|
250
|
-
*/
|
|
251
|
-
export const isTiptapDocument = (value: unknown): value is JSONContent =>
|
|
252
|
-
typeof value === 'object' &&
|
|
253
|
-
value !== null &&
|
|
254
|
-
(value as JSONContent).type === 'doc';
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Extract visible plain text from a Tiptap JSON document (concatenated text
|
|
258
|
-
* nodes). Dependency-free — no editor instantiation.
|
|
259
|
-
*/
|
|
260
|
-
export const getRteJsonPlainText = (doc: JSONContent): string => {
|
|
261
|
-
const parts: string[] = [];
|
|
262
|
-
|
|
263
|
-
const walk = (node: JSONContent): void => {
|
|
264
|
-
if (typeof node.text === 'string') parts.push(node.text);
|
|
265
|
-
|
|
266
|
-
node.content?.forEach(walk);
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
walk(doc);
|
|
270
|
-
|
|
271
|
-
return parts.join(' ').replace(/\s+/g, ' ').trim();
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
// Accepts a legacy inline-HTML string or a Tiptap JSON document (BP-5787).
|
|
275
|
-
export const hasRTEContent = (content: unknown): boolean => {
|
|
276
|
-
if (isTiptapDocument(content)) return true;
|
|
277
|
-
|
|
278
|
-
if (typeof content !== 'string' || content.length === 0) return false;
|
|
246
|
+
export const hasRTEContent = (content: string): boolean => {
|
|
247
|
+
if (!content || content.length === 0) return false;
|
|
279
248
|
|
|
280
249
|
const trimmed = content.trim();
|
|
281
250
|
const tagWrapRegex = /^<([A-Za-z][A-Za-z0-9-]*)\b[^>]*>[\s\S]*<\/\1>$/;
|
|
@@ -283,11 +252,7 @@ export const hasRTEContent = (content: unknown): boolean => {
|
|
|
283
252
|
return tagWrapRegex.test(trimmed);
|
|
284
253
|
};
|
|
285
254
|
|
|
286
|
-
export const getPlainTextFromRTE = (content:
|
|
287
|
-
if (isTiptapDocument(content)) {
|
|
288
|
-
return getRteJsonPlainText(content);
|
|
289
|
-
}
|
|
290
|
-
|
|
255
|
+
export const getPlainTextFromRTE = (content: string): string => {
|
|
291
256
|
if (typeof content !== 'string') {
|
|
292
257
|
return '';
|
|
293
258
|
}
|
|
@@ -756,20 +721,16 @@ export const switchAllContentToRTE = (
|
|
|
756
721
|
|
|
757
722
|
export const getUpdatedHTMLFromEditor = (
|
|
758
723
|
externalEditor: Editor,
|
|
759
|
-
contentText:
|
|
724
|
+
contentText: string,
|
|
760
725
|
newContentStyle: TextStyleDeclaration,
|
|
761
726
|
) => {
|
|
762
727
|
if (!contentText) {
|
|
763
728
|
return contentText;
|
|
764
729
|
}
|
|
765
730
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
typeof contentText === 'string' &&
|
|
770
|
-
contentText.includes(ESCAPED_NEWLINE_CHARACTER)
|
|
771
|
-
? getRenderText(contentText)
|
|
772
|
-
: contentText;
|
|
731
|
+
const content = contentText.includes(ESCAPED_NEWLINE_CHARACTER)
|
|
732
|
+
? getRenderText(contentText)
|
|
733
|
+
: contentText;
|
|
773
734
|
|
|
774
735
|
externalEditor.commands.setContent(content);
|
|
775
736
|
|
|
@@ -827,9 +788,9 @@ export const getUpdatedHTMLFromEditor = (
|
|
|
827
788
|
chain = newContentStyle?.fontStyle ? chain.setItalic() : chain.unsetItalic();
|
|
828
789
|
|
|
829
790
|
chain.run();
|
|
791
|
+
const html = externalEditor.getHTML();
|
|
830
792
|
|
|
831
|
-
|
|
832
|
-
return externalEditor.getJSON();
|
|
793
|
+
return html;
|
|
833
794
|
};
|
|
834
795
|
|
|
835
796
|
// Helper to build stored marks from a style declaration
|
|
@@ -953,7 +914,7 @@ export const sanitiseClientSideRTE = (html: string): SanitizationResult => {
|
|
|
953
914
|
/**
|
|
954
915
|
* Validates if an href value is safe and returns a sanitized version
|
|
955
916
|
*/
|
|
956
|
-
|
|
917
|
+
const validateAndSanitizeHref = (href: string): string | null => {
|
|
957
918
|
if (!href || typeof href !== 'string') return null;
|
|
958
919
|
|
|
959
920
|
// Trim whitespace and decode entities
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isURL, isEmail, isTel } from '@utils/validator.util';
|
|
2
2
|
import type { ItemFieldType, Validator } from '@brand-page-maker/models';
|
|
3
|
-
import { YOUTUBE_URL_RE
|
|
3
|
+
import { YOUTUBE_URL_RE } from '@brand-page-maker/utils';
|
|
4
4
|
import { validatorTr } from '@src/composables/useTranslate';
|
|
5
5
|
|
|
6
6
|
const addressValueValidator: Validator = {
|
|
@@ -76,15 +76,7 @@ export const dummyValidator: Validator = {
|
|
|
76
76
|
|
|
77
77
|
const nonEmptyStringValidator: Validator = {
|
|
78
78
|
validate: (name, value: any) => {
|
|
79
|
-
|
|
80
|
-
// An RTE field's stored value used to be inline HTML — an empty editor was
|
|
81
|
-
// "<p></p>", a NON-empty string, so the required check always passed for RTE
|
|
82
|
-
// fields (it only ever failed for genuinely empty PLAIN inputs). Now RTE
|
|
83
|
-
// fields are Tiptap JSON docs; treat any doc as "present" so behaviour
|
|
84
|
-
// stays identical. A plain string is still required to be non-empty.
|
|
85
|
-
const isValid = isTiptapDocument(value)
|
|
86
|
-
? true
|
|
87
|
-
: typeof value === 'string' && value.length > 0;
|
|
79
|
+
const isValid = typeof value === 'string' && value.length > 0;
|
|
88
80
|
|
|
89
81
|
return {
|
|
90
82
|
isValid,
|
package/src/lib.ts
CHANGED
|
@@ -85,15 +85,6 @@ export {
|
|
|
85
85
|
recalculateHeaderLogoSizes,
|
|
86
86
|
} from './brand-page/utils/logo.util';
|
|
87
87
|
export { sanitizeRTEHtml } from './brand-page-maker/utils';
|
|
88
|
-
// BP-5787: RTE JSON storage boundary helpers (render/normalise/sanitise/extract).
|
|
89
|
-
export {
|
|
90
|
-
renderRteToHtml,
|
|
91
|
-
toRteJson,
|
|
92
|
-
isTiptapDocument,
|
|
93
|
-
sanitizeRteJson,
|
|
94
|
-
getRteJsonPlainText,
|
|
95
|
-
type RichTextValue,
|
|
96
|
-
} from './brand-page-maker/utils';
|
|
97
88
|
export { getPageSlugFromPageTitle } from './brand-page-maker/utils/slug.util';
|
|
98
89
|
|
|
99
90
|
// AI-pipeline preview (longclaw per-turn snapshots): web-renderer's
|
|
@@ -6,36 +6,7 @@ import {
|
|
|
6
6
|
type BuilderResult,
|
|
7
7
|
type MetadataBuilderRequest,
|
|
8
8
|
} from '../models';
|
|
9
|
-
import {
|
|
10
|
-
hasRTEContent,
|
|
11
|
-
replaceTextInRTE,
|
|
12
|
-
isTiptapDocument,
|
|
13
|
-
renderRteToHtml,
|
|
14
|
-
toRteJson,
|
|
15
|
-
} from '../../brand-page-maker/utils';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* BP-5787 — replace the text of an RTE field while preserving formatting, for
|
|
19
|
-
* either storage form. JSON is rendered to HTML, edited via the existing
|
|
20
|
-
* HTML-preserving replace, then converted back to JSON; plain/legacy strings
|
|
21
|
-
* keep the original behaviour.
|
|
22
|
-
*/
|
|
23
|
-
const updateRteFieldValue = (existing: unknown, newText: string): unknown => {
|
|
24
|
-
const wasJson = isTiptapDocument(existing);
|
|
25
|
-
const existingHtml = wasJson ? renderRteToHtml(existing) : existing;
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
-
existingHtml &&
|
|
29
|
-
typeof existingHtml === 'string' &&
|
|
30
|
-
hasRTEContent(existingHtml)
|
|
31
|
-
) {
|
|
32
|
-
const updatedHtml = replaceTextInRTE(existingHtml, newText);
|
|
33
|
-
|
|
34
|
-
return wasJson ? toRteJson(updatedHtml) : updatedHtml;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return newText;
|
|
38
|
-
};
|
|
9
|
+
import { hasRTEContent, replaceTextInRTE } from '../../brand-page-maker/utils';
|
|
39
10
|
|
|
40
11
|
export interface UpdateTextPayload {
|
|
41
12
|
contentId: string;
|
|
@@ -65,7 +36,12 @@ const updateTextField = (target: any, fieldName: string, newText: string) => {
|
|
|
65
36
|
|
|
66
37
|
if (Array.isArray(array) && array[+index]) {
|
|
67
38
|
const existingText = array[+index][property];
|
|
68
|
-
const updatedValue =
|
|
39
|
+
const updatedValue =
|
|
40
|
+
existingText &&
|
|
41
|
+
typeof existingText === 'string' &&
|
|
42
|
+
hasRTEContent(existingText)
|
|
43
|
+
? replaceTextInRTE(existingText, newText)
|
|
44
|
+
: newText;
|
|
69
45
|
|
|
70
46
|
// Create a new array with the updated item to trigger reactivity
|
|
71
47
|
const newArray = [...array];
|
|
@@ -78,7 +54,17 @@ const updateTextField = (target: any, fieldName: string, newText: string) => {
|
|
|
78
54
|
}
|
|
79
55
|
} else {
|
|
80
56
|
// Simple field
|
|
81
|
-
|
|
57
|
+
const existingText = target[fieldName];
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
existingText &&
|
|
61
|
+
typeof existingText === 'string' &&
|
|
62
|
+
hasRTEContent(existingText)
|
|
63
|
+
) {
|
|
64
|
+
target[fieldName] = replaceTextInRTE(existingText, newText);
|
|
65
|
+
} else {
|
|
66
|
+
target[fieldName] = newText;
|
|
67
|
+
}
|
|
82
68
|
}
|
|
83
69
|
};
|
|
84
70
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type BrandPageModel,
|
|
3
3
|
type Content,
|
|
4
|
-
type RichTextValue,
|
|
5
4
|
type TextStyleDeclaration,
|
|
6
5
|
type TextStyleOptions,
|
|
7
6
|
} from '../../brand-page/models';
|
|
@@ -69,26 +68,17 @@ const mergeStyles = (
|
|
|
69
68
|
return merged;
|
|
70
69
|
};
|
|
71
70
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const updateRTEText = (text: unknown, style: TextStyleDeclaration): unknown => {
|
|
75
|
-
// `text` arrives via dynamic content[field] access, so it's untyped here.
|
|
76
|
-
// hasRTEContent runtime-narrows it to an actual RTE value (HTML string or
|
|
77
|
-
// Tiptap JSON doc) — i.e. a RichTextValue — before we hand it to the editor.
|
|
78
|
-
if (!hasRTEContent(text)) {
|
|
71
|
+
const updateRTEText = (text: string, style: TextStyleDeclaration): string => {
|
|
72
|
+
if (!text || typeof text !== 'string' || !hasRTEContent(text)) {
|
|
79
73
|
return text;
|
|
80
74
|
}
|
|
81
75
|
|
|
82
76
|
const { externalEditor } = useExternalRichTextEditor();
|
|
83
|
-
const
|
|
84
|
-
externalEditor,
|
|
85
|
-
text as RichTextValue,
|
|
86
|
-
style,
|
|
87
|
-
);
|
|
77
|
+
const updatedText = applyStyleToRTEText(externalEditor, text, style);
|
|
88
78
|
|
|
89
79
|
externalEditor.destroy();
|
|
90
80
|
|
|
91
|
-
return
|
|
81
|
+
return updatedText;
|
|
92
82
|
};
|
|
93
83
|
|
|
94
84
|
const findStyleKey = (target: Content, property: string): string | null => {
|