@designcrowd/library.brand-page 5.10.20 → 5.10.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/lib/src/brand-page/models/index.d.ts +5 -0
  2. package/lib/src/brand-page/models/index.js +4 -0
  3. package/lib/src/brand-page/utils/style.util.d.ts +8 -1
  4. package/lib/src/brand-page/utils/style.util.js +58 -5
  5. package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.js +1 -1
  6. package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/content-settings-items.fixture.js +1 -0
  7. package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/page-content-items.d.ts +1 -1
  8. package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/page-content-items.js +2 -1
  9. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +33 -0
  10. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.js +35 -1
  11. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +1 -0
  12. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.js +17 -0
  13. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.d.ts +19 -0
  14. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.js +31 -0
  15. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/AutoLink.d.ts +2 -0
  16. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/AutoLink.js +161 -0
  17. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/MaxLength.js +3 -3
  18. package/lib/src/brand-page-maker/composables/rich-text-editor/font.d.ts +1 -1
  19. package/lib/src/brand-page-maker/composables/rich-text-editor/font.js +0 -1
  20. package/lib/src/brand-page-maker/composables/rich-text-editor/formatting.d.ts +1 -1
  21. package/lib/src/brand-page-maker/composables/rich-text-editor/formatting.js +0 -1
  22. package/lib/src/brand-page-maker/composables/rich-text-editor/hyperlink.d.ts +19 -0
  23. package/lib/src/brand-page-maker/composables/rich-text-editor/hyperlink.js +87 -0
  24. package/lib/src/brand-page-maker/composables/rich-text-editor/spacing.d.ts +1 -1
  25. package/lib/src/brand-page-maker/composables/rich-text-editor/spacing.js +0 -1
  26. package/lib/src/brand-page-maker/composables/rich-text-editor/textAlign.d.ts +1 -1
  27. package/lib/src/brand-page-maker/composables/rich-text-editor/textAlign.js +0 -1
  28. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +24 -3
  29. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +214 -33
  30. package/lib/src/brand-page-maker/models/button-group.model.d.ts +2 -0
  31. package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +19 -2
  32. package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +340 -8
  33. package/lib/src/{bundled-translations.de-DE-O4sKkzBX.js → bundled-translations.de-DE-DjQFwukR.js} +1 -1
  34. package/lib/src/{bundled-translations.es-ES-C4lGaf08.js → bundled-translations.es-ES--EbGRI7e.js} +1 -1
  35. package/lib/src/bundled-translations.fr-FR-BS3Kia5y.js +117 -0
  36. package/lib/src/{bundled-translations.fr-FR-CIy8KjAV.js → bundled-translations.fr-FR-VtC2JYUC.js} +1 -1
  37. package/lib/src/{bundled-translations.pt-PT-C8EcSdyX.js → bundled-translations.pt-PT-l9cEldEo.js} +1 -1
  38. package/lib/src/bundles/bundled-translations.fr-FR.json +2 -2
  39. package/lib/src/css/library.brand-page-brandCrowd.min.css +96 -70
  40. package/lib/src/css/library.brand-page-brandPage.min.css +96 -70
  41. package/lib/src/css/library.brand-page-designCom.min.css +96 -70
  42. package/lib/src/css/library.brand-page-preview.min.css +96 -70
  43. package/lib/src/index.d.ts +1 -0
  44. package/lib/src/index.mjs +29673 -27225
  45. package/lib/src/lib.js +1 -0
  46. package/lib/src/mockServiceWorker.js +91 -54
  47. package/lib/src/shared/content-settings/components/color-field/ColorField.mixin.d.ts +11 -0
  48. package/lib/src/shared/content-settings/components/color-field/ColorField.mixin.js +5 -0
  49. package/lib/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.mixin.d.ts +11 -1
  50. package/lib/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.mixin.js +4 -0
  51. package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +2 -0
  52. package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.js +12 -7
  53. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +2 -0
  54. package/package.json +6 -3
  55. package/src/brand-page/components/banner-section/BannerSection.vue +6 -0
  56. package/src/brand-page/components/text/Text.vue +2 -0
  57. package/src/brand-page/components/text-wrapper/TextWrapper.vue +31 -11
  58. package/src/brand-page/models/index.ts +6 -0
  59. package/src/brand-page/utils/style.util.ts +105 -5
  60. package/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.ts +1 -1
  61. package/src/brand-page-maker/components/brand-page-maker/__fixtures__/content-settings-items.fixture.ts +1 -0
  62. package/src/brand-page-maker/components/brand-page-maker/__fixtures__/page-content-items.ts +2 -0
  63. package/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.ts +44 -1
  64. package/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.vue +1 -0
  65. package/src/brand-page-maker/components/brandpage-settings/i18n/brand-page-settings.fr-FR.json +1 -1
  66. package/src/brand-page-maker/components/google-analytics/i18n/google-analytics.fr-FR.json +1 -1
  67. package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +539 -50
  68. package/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.ts +22 -0
  69. package/src/brand-page-maker/components/template-preview/TemplatePreview.vue +1 -1
  70. package/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.ts +42 -0
  71. package/src/brand-page-maker/composables/rich-text-editor/extensions/AutoLink.ts +227 -0
  72. package/src/brand-page-maker/composables/rich-text-editor/extensions/MaxLength.test.ts +1 -1
  73. package/src/brand-page-maker/composables/rich-text-editor/extensions/MaxLength.ts +4 -15
  74. package/src/brand-page-maker/composables/rich-text-editor/font.ts +1 -1
  75. package/src/brand-page-maker/composables/rich-text-editor/formatting.ts +1 -1
  76. package/src/brand-page-maker/composables/rich-text-editor/hyperlink.ts +114 -0
  77. package/src/brand-page-maker/composables/rich-text-editor/spacing.ts +1 -1
  78. package/src/brand-page-maker/composables/rich-text-editor/textAlign.ts +1 -1
  79. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +299 -41
  80. package/src/brand-page-maker/models/button-group.model.ts +2 -0
  81. package/src/brand-page-maker/stores/brandpage-maker/__snapshots__/brandpage-maker-store.test.ts.snap +1 -0
  82. package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +418 -0
  83. package/src/brand-page-maker/utils/rich-text-editor.util.ts +470 -25
  84. package/src/bundles/bundled-translations.fr-FR.json +2 -2
  85. package/src/lib.ts +1 -0
  86. package/src/shared/content-settings/components/color-field/ColorField.mixin.ts +5 -0
  87. package/src/shared/content-settings/components/color-field/ColorField.vue +1 -1
  88. package/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.mixin.ts +4 -0
  89. package/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.vue +2 -2
  90. package/src/shared/content-settings/components/text-field/TextField.vue +1 -1
  91. package/src/shared/content-settings/components/text-items-field/TextItemsField.vue +2 -2
  92. package/src/shared/content-settings/components/textarea-field/TextareaField.vue +3 -3
  93. package/src/shared/content-settings/components/url-field/UrlField.mixin.ts +15 -10
  94. package/src/shared/content-settings/components/url-field/UrlField.vue +1 -0
  95. package/lib/src/bundled-translations.fr-FR-CTnDppmf.js +0 -117
@@ -0,0 +1,87 @@
1
+ import { ref, computed } from 'vue';
2
+ // Store original colors before applying hyperlinks
3
+ const originalColors = new WeakMap();
4
+ export const useHyperlink = (editor) => {
5
+ const hyperlinkUrl = ref('');
6
+ const storeOriginalColor = () => {
7
+ const currentTextStyle = editor.getAttributes('textStyle');
8
+ const originalColor = currentTextStyle.color || null;
9
+ // Store the original color using the editor instance as key
10
+ originalColors.set(editor, originalColor);
11
+ return originalColor;
12
+ };
13
+ const restoreOriginalColor = () => {
14
+ const originalColor = originalColors.get(editor);
15
+ if (originalColor) {
16
+ // Restore the original color
17
+ editor.chain().focus().setColor(originalColor).run();
18
+ }
19
+ else {
20
+ // No original color, remove color completely
21
+ editor.chain().focus().unsetColor().run();
22
+ }
23
+ // Clean up stored color
24
+ originalColors.delete(editor);
25
+ };
26
+ const setHyperlinkUrl = (url) => {
27
+ const safeUrl = typeof url === 'string' ? url : '';
28
+ hyperlinkUrl.value = safeUrl;
29
+ storeOriginalColor();
30
+ // Set the link, apply blue color, and add underline by default
31
+ editor
32
+ .chain()
33
+ .focus()
34
+ .setLink({ href: safeUrl })
35
+ .setColor('#0097d7')
36
+ .setUnderline()
37
+ .run();
38
+ };
39
+ const setHyperlinkUrlValue = (url) => {
40
+ hyperlinkUrl.value = typeof url === 'string' ? url : '';
41
+ };
42
+ const hyperlinkValue = computed({
43
+ get: () => ({
44
+ id: 'id',
45
+ name: 'url',
46
+ value: hyperlinkUrl.value,
47
+ }),
48
+ set: (v) => {
49
+ setHyperlinkUrl(v?.value || '');
50
+ },
51
+ });
52
+ const applyHyperlinkToEditor = (url, text, selection) => {
53
+ storeOriginalColor();
54
+ const selectedText = editor.chain().focus();
55
+ // Replace content with text if provided
56
+ if (text && text.trim()) {
57
+ const currentPos = selection?.from || editor.state.selection.from;
58
+ selectedText
59
+ .deleteSelection()
60
+ .insertContent(text)
61
+ .setTextSelection({
62
+ from: currentPos,
63
+ to: currentPos + text.length,
64
+ });
65
+ }
66
+ selectedText
67
+ .setLink({ href: url })
68
+ .setColor('#0097d7')
69
+ .setUnderline()
70
+ .run();
71
+ };
72
+ const removeHyperlink = (clearUrlValue = false) => {
73
+ editor.chain().focus().unsetLink().unsetUnderline().run();
74
+ // Restore original color (or remove color if there was none)
75
+ restoreOriginalColor();
76
+ if (clearUrlValue) {
77
+ setHyperlinkUrlValue('');
78
+ }
79
+ };
80
+ return {
81
+ hyperlinkValue,
82
+ setHyperlinkUrl,
83
+ setHyperlinkUrlValue,
84
+ removeHyperlink,
85
+ applyHyperlinkToEditor,
86
+ };
87
+ };
@@ -1,4 +1,4 @@
1
- import { Editor } from '@tiptap/vue-3';
1
+ import type { Editor } from '@tiptap/vue-3';
2
2
  export declare const useSpacing: (editor: Editor) => {
3
3
  letterSpacingValue: import("vue").WritableComputedRef<string, string>;
4
4
  lineHeightValue: import("vue").WritableComputedRef<string, string>;
@@ -1,5 +1,4 @@
1
1
  import { ref, computed } from 'vue';
2
- import { Editor } from '@tiptap/vue-3';
3
2
  import debounce from 'lodash/debounce';
4
3
  import { letterSpacingValueConvertor } from '../../../brand-page-maker/convertors';
5
4
  import { skipStyleDetection } from '../../../brand-page-maker/utils';
@@ -1,4 +1,4 @@
1
- import { Editor } from '@tiptap/vue-3';
1
+ import type { Editor } from '@tiptap/vue-3';
2
2
  export declare const useTextAlign: (editor: Editor) => {
3
3
  currentAlignment: import("vue").ComputedRef<{
4
4
  icon: string;
@@ -1,5 +1,4 @@
1
1
  import { ref, computed } from 'vue';
2
- import { Editor } from '@tiptap/vue-3';
3
2
  import { skipStyleDetection } from '../../../brand-page-maker/utils';
4
3
  export const useTextAlign = (editor) => {
5
4
  const alignmentSettings = {
@@ -1,7 +1,8 @@
1
1
  import { type ModelRef, type ComputedRef, type Ref, type WritableComputedRef } from 'vue';
2
2
  import { Editor } from '@tiptap/vue-3';
3
3
  import type { StyleTextFontFamilyOption } from '../../../shared/style-settings/models';
4
- import { TextStyles, type TextItem, type TextStyleDeclaration } from '../../../brand-page';
4
+ import type { TextStyles } from '../../../brand-page';
5
+ import { type TextItem, type TextStyleDeclaration } from '../../../brand-page';
5
6
  import type { ButtonGroupOption } from '../../../brand-page-maker/models/button-group.model';
6
7
  interface UseRichTextEditorReturn {
7
8
  editor: Editor;
@@ -28,12 +29,32 @@ interface UseRichTextEditorReturn {
28
29
  setInitialStyleSettings: (selectedTextStyle?: BrandPageCssStyle) => void;
29
30
  copyGlobalStyleSettings: (selectedTextStyle?: BrandPageCssStyle) => void;
30
31
  unsetAllMarks: () => void;
32
+ handleHyperlink: (action: 'toggle' | 'apply' | 'remove', data?: {
33
+ href?: string;
34
+ text?: string;
35
+ }) => void;
36
+ hyperlinkValue: ComputedRef<{
37
+ id: string;
38
+ name: string;
39
+ value: string;
40
+ }>;
41
+ setHyperlinkUrlValue: (url: string) => void;
42
+ shouldShow: ({ editor, state }: {
43
+ editor: any;
44
+ state: any;
45
+ }) => boolean;
46
+ tippyOptions: Record<string, any>;
47
+ handleAdminColorChange: (color: {
48
+ id: string;
49
+ name: string;
50
+ value: string;
51
+ }, type: 'textColor' | 'backgroundColor') => void;
31
52
  }
32
53
  interface DropdownInstance {
33
54
  hideMenu: () => void;
34
55
  }
35
56
  interface EditorOptionType {
36
- type: 'fontFamily' | 'fontSize' | 'fontWeight' | 'fontStyle' | 'textDecorationLine' | 'textDecorationStrike' | 'textColor' | 'backgroundColor' | 'lineHeight' | 'letterSpacing' | 'textAlign';
57
+ type: 'hyperlink' | 'fontFamily' | 'fontSize' | 'fontWeight' | 'fontStyle' | 'textDecorationLine' | 'textDecorationStrike' | 'textColor' | 'backgroundColor' | 'lineHeight' | 'letterSpacing' | 'textAlign';
37
58
  value: string | StyleTextFontFamilyOption;
38
59
  }
39
60
  export declare const useExternalRichTextEditor: () => {
@@ -56,5 +77,5 @@ export declare const useRichTextEditor: (model: ModelRef<any>, props: {
56
77
  selectedFontSizePx: number;
57
78
  selectedTextColor: string;
58
79
  selectedBackgroundColor: string;
59
- }, fontStyleDropdownRef: Ref<DropdownInstance | undefined, DropdownInstance | undefined>, selectedGlobalTextStyles: WritableComputedRef<TextStyles, string>, onSelectTextStyleType: (option: ButtonGroupOption) => void, updateRichTextEditor: Ref<boolean>, textStyleTypeOptions: ButtonGroupOption[]) => UseRichTextEditorReturn;
80
+ }, fontStyleDropdownRef: Ref<DropdownInstance | undefined, DropdownInstance | undefined>, selectedGlobalTextStyles: WritableComputedRef<TextStyles, string>, onSelectTextStyleType: (option: ButtonGroupOption) => void, updateRichTextEditor: Ref<boolean>, textStyleTypeOptions: ButtonGroupOption[], markedText: Ref<string>, isHyperlinkPopupOpen: Ref<boolean>) => UseRichTextEditorReturn;
60
81
  export {};
@@ -11,18 +11,22 @@ import { LetterSpacing } from './extensions/LetterSpacing';
11
11
  import { LineHeight } from './extensions/LineHeight';
12
12
  import Highlight from '@tiptap/extension-highlight';
13
13
  import Placeholder from '@tiptap/extension-placeholder';
14
+ import Link from '@tiptap/extension-link';
14
15
  import { createFormattingActions } from './formatting';
15
16
  import { useTextAlign } from './textAlign';
16
17
  import { useFont } from './font';
17
18
  import { useSpacing } from './spacing';
18
19
  import Color from 'color';
19
- import { buildInitialMarks, emToPx, isStyleChanged, pxToEm, skipStyleDetection, } from '../../../brand-page-maker/utils';
20
- import { TextStyles, } from '../../../brand-page';
21
- import { letterSpacingValueConvertor } from '../../../brand-page-maker/convertors';
20
+ import { buildInitialMarks, emToPx, isStyleChanged, pxToEm, sanitiseClientSideRTE, skipStyleDetection, } from '../../../brand-page-maker/utils';
21
+ import {} from '../../../brand-page';
22
+ import { letterSpacingValueConvertor, colorConvertor, backgroundColorConvertor, } from '../../../brand-page-maker/convertors';
22
23
  import { useUiContentSettingsStore } from '../../../shared/content-settings/store';
23
- import { DOMSerializer } from 'prosemirror-model';
24
24
  import { AllSelection } from 'prosemirror-state';
25
25
  import { MaxLength } from './extensions/MaxLength';
26
+ import { AutoLink } from './extensions/AutoLink';
27
+ import { useHyperlink } from './hyperlink';
28
+ import { useBubbleMenu } from './bubbleMenu';
29
+ import { hasLogoColorVar, isLogoColor, getRTEColor, } from '../../utils/rich-text-editor.util';
26
30
  export const useExternalRichTextEditor = () => {
27
31
  const externalEditor = new Editor({
28
32
  editable: false,
@@ -45,13 +49,27 @@ export const useExternalRichTextEditor = () => {
45
49
  Highlight.configure({
46
50
  multicolor: true,
47
51
  }),
52
+ Link.configure({
53
+ openOnClick: false,
54
+ defaultProtocol: 'https',
55
+ autolink: true,
56
+ HTMLAttributes: (attrs) => {
57
+ const href = attrs.href;
58
+ const isExternal = href && (href.startsWith('http://') || href.startsWith('https://'));
59
+ return {
60
+ rel: 'noopener noreferrer',
61
+ target: isExternal ? '_blank' : null,
62
+ };
63
+ },
64
+ }),
65
+ AutoLink,
48
66
  ],
49
67
  });
50
68
  return {
51
69
  externalEditor,
52
70
  };
53
71
  };
54
- export const useRichTextEditor = (model, props, emit, fontSettings, fontStyleDropdownRef, selectedGlobalTextStyles, onSelectTextStyleType, updateRichTextEditor, textStyleTypeOptions) => {
72
+ export const useRichTextEditor = (model, props, emit, fontSettings, fontStyleDropdownRef, selectedGlobalTextStyles, onSelectTextStyleType, updateRichTextEditor, textStyleTypeOptions, markedText, isHyperlinkPopupOpen) => {
55
73
  const editor = new Editor({
56
74
  content: model.value ?? '',
57
75
  editable: !props.readOnly,
@@ -78,6 +96,19 @@ export const useRichTextEditor = (model, props, emit, fontSettings, fontStyleDro
78
96
  placeholder: props.placeholder,
79
97
  }),
80
98
  MaxLength.configure({ maxLength: props.maxLength }),
99
+ Link.configure({
100
+ openOnClick: false,
101
+ defaultProtocol: 'https',
102
+ autolink: true,
103
+ HTMLAttributes: (attrs) => {
104
+ const href = attrs.href;
105
+ const isExternal = href && (href.startsWith('http://') || href.startsWith('https://'));
106
+ return {
107
+ target: isExternal ? '_blank' : null,
108
+ };
109
+ },
110
+ }),
111
+ AutoLink,
81
112
  ],
82
113
  editorProps: {
83
114
  handleDOMEvents: {
@@ -100,23 +131,20 @@ export const useRichTextEditor = (model, props, emit, fontSettings, fontStyleDro
100
131
  return false;
101
132
  },
102
133
  copy(view, event) {
103
- // Prevent the browser’s default so we can inject our own blobs
104
- event.preventDefault();
105
- // 1) Plain text (already have this)
106
- const selectedText = view.state.doc.textBetween(view.state.selection.from, view.state.selection.to, '\n');
107
- event.clipboardData?.setData('text/plain', selectedText);
108
- // 2) custom style blob to add
134
+ const selection = view.state.selection;
135
+ if (selection.empty)
136
+ return;
137
+ const data = event.clipboardData;
109
138
  const styleData = JSON.stringify(props.styles);
110
- event.clipboardData?.setData('application/x-rte-styles', styleData);
111
- // 3) HTML serialization via ProseMirror’s DOMSerializer
112
- const { from, to } = view.state.selection;
113
- const slice = view.state.doc.slice(from, to);
114
- const serializer = DOMSerializer.fromSchema(view.state.schema);
115
- const fragment = serializer.serializeFragment(slice.content);
116
- // Wrap in a container DIV so we can grab `innerHTML`
117
- const wrapper = document.createElement('div');
118
- wrapper.appendChild(fragment);
119
- event.clipboardData?.setData('text/html', wrapper.innerHTML);
139
+ const slice = selection.content(), { dom, text } = view.serializeForClipboard(slice);
140
+ if (data) {
141
+ event.preventDefault();
142
+ data.clearData();
143
+ data.setData('text/html', dom.innerHTML);
144
+ data.setData('text/plain', text);
145
+ if (styleData)
146
+ data.setData('application/x-rte-styles', styleData);
147
+ }
120
148
  return true;
121
149
  },
122
150
  paste(view, event) {
@@ -159,6 +187,21 @@ export const useRichTextEditor = (model, props, emit, fontSettings, fontStyleDro
159
187
  dispatch(tr);
160
188
  return true;
161
189
  },
190
+ click(view, event) {
191
+ if (isHyperlinkPopupOpen?.value)
192
+ return false;
193
+ const target = event.target;
194
+ const linkElement = target.closest('a');
195
+ if (linkElement) {
196
+ const pos = view.posAtDOM(linkElement, 0);
197
+ if (pos !== null) {
198
+ editor.commands.setTextSelection(pos);
199
+ editor.commands.extendMarkRange('link');
200
+ return true; // Prevent default
201
+ }
202
+ }
203
+ return false;
204
+ },
162
205
  },
163
206
  },
164
207
  });
@@ -167,6 +210,8 @@ export const useRichTextEditor = (model, props, emit, fontSettings, fontStyleDro
167
210
  const { currentAlignment, toggleTextAlign, idx: currentAlignmentIndex, keys: alignmentKeys, } = useTextAlign(editor);
168
211
  const { selectedFont, toggleFont, toggleFontSize, toggleFontAll, toggleFontSizeAll, } = useFont(editor, fontSettings, fontStyleDropdownRef);
169
212
  const { letterSpacingValue, lineHeightValue, letterSpacing, lineHeight, setLetterSpaceAll, setLineHeightAll, } = useSpacing(editor);
213
+ const { hyperlinkValue, setHyperlinkUrl, setHyperlinkUrlValue, removeHyperlink: removeHyperlinkFromComposable, applyHyperlinkToEditor, } = useHyperlink(editor);
214
+ const { shouldShow, tippyOptions } = useBubbleMenu(editor, isHyperlinkPopupOpen);
170
215
  watch(() => props.styles.textAlign, (newAlignment) => {
171
216
  if (newAlignment && newAlignment !== currentAlignment.value.alignment) {
172
217
  skipStyleDetection(editor.chain().selectAll().setTextAlign(newAlignment)).run();
@@ -196,8 +241,8 @@ export const useRichTextEditor = (model, props, emit, fontSettings, fontStyleDro
196
241
  });
197
242
  // Update model when editor content changes
198
243
  editor.on('update', () => {
199
- const html = editor.getHTML();
200
- emit('update:modelValue', html);
244
+ const sanitizedData = sanitiseClientSideRTE(editor.getHTML());
245
+ emit('update:modelValue', sanitizedData.sanitizedHtml);
201
246
  });
202
247
  editor.on('selectionUpdate', () => {
203
248
  const textStyleAttributes = editor.getAttributes('textStyle');
@@ -229,13 +274,15 @@ export const useRichTextEditor = (model, props, emit, fontSettings, fontStyleDro
229
274
  fontSettings.selectedFontSizePx = parseInt(emToPx(fontSettings.selectedFontSize));
230
275
  }
231
276
  if (textStyleAttributes && textStyleAttributes.color) {
232
- fontSettings.selectedTextColor = Color(textStyleAttributes.color).hex();
277
+ if (!hasLogoColorVar(textStyleAttributes.color))
278
+ fontSettings.selectedTextColor = Color(textStyleAttributes.color).hex();
233
279
  }
234
280
  else {
235
281
  fontSettings.selectedTextColor = '#000000';
236
282
  }
237
283
  if (highlightAttributes && highlightAttributes.color) {
238
- fontSettings.selectedBackgroundColor = Color(highlightAttributes.color).hex();
284
+ if (!hasLogoColorVar(highlightAttributes.color))
285
+ fontSettings.selectedBackgroundColor = Color(highlightAttributes.color).hex();
239
286
  }
240
287
  else {
241
288
  fontSettings.selectedBackgroundColor = 'transparent';
@@ -287,9 +334,12 @@ export const useRichTextEditor = (model, props, emit, fontSettings, fontStyleDro
287
334
  fontSettings.selectedFont = globalStyles?.fontFamily ?? 'Lato';
288
335
  fontSettings.selectedFontSize = globalStyles?.fontSize ?? '2.25em';
289
336
  fontSettings.selectedFontSizePx = parseInt(emToPx(globalStyles?.fontSize ?? '2.25em'));
290
- fontSettings.selectedTextColor = globalStyles?.color ?? '#000000';
291
- fontSettings.selectedBackgroundColor =
292
- globalStyles?.backgroundColor ?? 'transparent';
337
+ fontSettings.selectedTextColor = hasLogoColorVar(globalStyles.color ?? '')
338
+ ? '#000000'
339
+ : globalStyles?.color ?? '#000000';
340
+ fontSettings.selectedBackgroundColor = hasLogoColorVar(globalStyles?.backgroundColor ?? '')
341
+ ? 'transparent'
342
+ : globalStyles?.backgroundColor ?? 'transparent';
293
343
  lineHeight.value = globalStyles?.lineHeight?.includes('em')
294
344
  ? emToPx(globalStyles?.lineHeight ?? '1em')
295
345
  : globalStyles?.lineHeight ?? '1';
@@ -305,9 +355,12 @@ export const useRichTextEditor = (model, props, emit, fontSettings, fontStyleDro
305
355
  fontSettings.selectedFont = props.styles?.fontFamily ?? 'Lato';
306
356
  fontSettings.selectedFontSize = props.styles?.fontSize ?? '2.25em';
307
357
  fontSettings.selectedFontSizePx = parseInt(emToPx(props.styles?.fontSize ?? '2.25em'));
308
- fontSettings.selectedTextColor = props.styles?.color ?? '#000000';
309
- fontSettings.selectedBackgroundColor =
310
- props.styles?.backgroundColor ?? 'transparent';
358
+ fontSettings.selectedTextColor = hasLogoColorVar(props.styles?.color ?? '#000000')
359
+ ? '#000000'
360
+ : props.styles?.color ?? '#000000';
361
+ fontSettings.selectedBackgroundColor = hasLogoColorVar(props.styles?.backgroundColor ?? '')
362
+ ? 'transparent'
363
+ : props.styles?.backgroundColor ?? 'transparent';
311
364
  lineHeight.value = props.styles?.lineHeight?.includes('em')
312
365
  ? emToPx(props.styles?.lineHeight ?? '1em')
313
366
  : props.styles?.lineHeight ?? '1';
@@ -329,8 +382,15 @@ export const useRichTextEditor = (model, props, emit, fontSettings, fontStyleDro
329
382
  globalStyles.textDecoration?.includes('line-through') &&
330
383
  toggleStrikeAll();
331
384
  if (globalStyles.color) {
332
- toggleTextColorAll(globalStyles.color);
333
- fontSettings.selectedTextColor = globalStyles.color;
385
+ if (!hasLogoColorVar(globalStyles.color)) {
386
+ toggleTextColorAll(globalStyles.color);
387
+ fontSettings.selectedTextColor = globalStyles.color;
388
+ }
389
+ else {
390
+ // Apply logo color using CSS variable
391
+ toggleTextColorAll(getRTEColor(globalStyles.color, 'text'));
392
+ fontSettings.selectedTextColor = '#000000';
393
+ }
334
394
  }
335
395
  globalStyles.fontFamily && toggleFontAll(globalStyles.fontFamily);
336
396
  globalStyles.fontSize && toggleFontSizeAll(globalStyles.fontSize);
@@ -410,6 +470,9 @@ export const useRichTextEditor = (model, props, emit, fontSettings, fontStyleDro
410
470
  toggleTextAlign();
411
471
  fontStyleChanges.textAlign = currentAlignment.value.alignment;
412
472
  break;
473
+ case 'hyperlink':
474
+ setHyperlinkUrl(editorOption.value);
475
+ break;
413
476
  }
414
477
  syncContentSettings(fontStyleChanges);
415
478
  };
@@ -441,6 +504,118 @@ export const useRichTextEditor = (model, props, emit, fontSettings, fontStyleDro
441
504
  const onBlur = () => {
442
505
  emit('blur');
443
506
  };
507
+ const handleHyperlink = (action, data) => {
508
+ switch (action) {
509
+ case 'toggle':
510
+ isHyperlinkPopupOpen.value = !isHyperlinkPopupOpen.value;
511
+ if (isHyperlinkPopupOpen.value) {
512
+ if (data?.href && data?.text) {
513
+ setHyperlinkUrlValue(data.href);
514
+ markedText.value = data.text;
515
+ }
516
+ else {
517
+ const linkAttrs = editor.getAttributes('link');
518
+ const selection = editor.state.selection;
519
+ if (linkAttrs.href) {
520
+ editor.commands.extendMarkRange('link');
521
+ const linkText = editor.state.doc.textBetween(selection.from, selection.to);
522
+ setHyperlinkUrlValue(linkAttrs.href);
523
+ markedText.value = linkText;
524
+ }
525
+ else {
526
+ const selectedText = selection.empty
527
+ ? ''
528
+ : editor.state.doc.textBetween(selection.from, selection.to);
529
+ setHyperlinkUrlValue('');
530
+ markedText.value = selectedText;
531
+ }
532
+ }
533
+ editor.commands.focus(); // Hide bubble menu
534
+ }
535
+ break;
536
+ case 'apply':
537
+ const url = hyperlinkValue.value.value;
538
+ const text = markedText.value;
539
+ if (!url.trim()) {
540
+ isHyperlinkPopupOpen.value = false;
541
+ return;
542
+ }
543
+ const selection = editor.state?.selection;
544
+ const selectionRange = selection
545
+ ? { from: selection.from, to: selection.to }
546
+ : undefined;
547
+ applyHyperlinkToEditor(url, text, selectionRange);
548
+ isHyperlinkPopupOpen.value = false;
549
+ break;
550
+ case 'remove':
551
+ removeHyperlinkFromComposable();
552
+ isHyperlinkPopupOpen.value = false;
553
+ break;
554
+ }
555
+ };
556
+ const handleAdminColorChange = (color, colorType) => {
557
+ const convertor = colorType === 'textColor' ? colorConvertor : backgroundColorConvertor;
558
+ const convertedColor = convertor.convertBack(color.value);
559
+ const colorValue = colorType === 'textColor'
560
+ ? convertedColor.color
561
+ : convertedColor.backgroundColor;
562
+ editor.commands.selectAll();
563
+ if (colorValue?.startsWith('#')) {
564
+ if (colorType === 'textColor') {
565
+ fontSettings.selectedTextColor = colorValue;
566
+ }
567
+ else {
568
+ fontSettings.selectedBackgroundColor = colorValue;
569
+ }
570
+ triggerEditorStyleChange({
571
+ type: colorType,
572
+ value: colorValue,
573
+ });
574
+ }
575
+ else {
576
+ // Handle logo theme colors using getRTEColor
577
+ if (isLogoColor(colorValue || '')) {
578
+ const processedColor = getRTEColor(colorValue, colorType === 'textColor' ? 'text' : 'background');
579
+ if (colorType === 'textColor') {
580
+ editor.commands.setColor(processedColor);
581
+ }
582
+ else {
583
+ editor.commands.setHighlight({
584
+ color: processedColor,
585
+ });
586
+ }
587
+ }
588
+ else {
589
+ if (colorType === 'textColor') {
590
+ editor.commands.unsetColor();
591
+ }
592
+ else {
593
+ editor.commands.unsetHighlight();
594
+ }
595
+ }
596
+ }
597
+ const updateValue = {
598
+ textStyleType: undefined,
599
+ ...(colorType === 'textColor'
600
+ ? { color: colorValue }
601
+ : { backgroundColor: colorValue }),
602
+ };
603
+ if (props.name === 'textItems') {
604
+ contentSettingStore.updateTextItemTextStyleTypeWithStyles({
605
+ id: props.rawId,
606
+ name: props.name,
607
+ value: updateValue,
608
+ index: props.textItemIndex,
609
+ });
610
+ }
611
+ else {
612
+ contentSettingStore.updateTextStyles({
613
+ id: props.rawId,
614
+ name: props.name,
615
+ value: updateValue,
616
+ });
617
+ }
618
+ };
444
619
  onBeforeUnmount(() => {
445
620
  editor.destroy();
446
621
  });
@@ -465,5 +640,11 @@ export const useRichTextEditor = (model, props, emit, fontSettings, fontStyleDro
465
640
  setInitialStyleSettings,
466
641
  copyGlobalStyleSettings,
467
642
  unsetAllMarks,
643
+ handleHyperlink,
644
+ hyperlinkValue,
645
+ setHyperlinkUrlValue,
646
+ shouldShow,
647
+ tippyOptions,
648
+ handleAdminColorChange,
468
649
  };
469
650
  };
@@ -3,6 +3,7 @@ export interface ButtonGroupBasicOption {
3
3
  label: string;
4
4
  isSelected: boolean;
5
5
  dataAttribute?: string;
6
+ disabled?: boolean;
6
7
  }
7
8
  export type ButtonGroupOption = ButtonGroupBasicOption | {
8
9
  label?: string;
@@ -13,4 +14,5 @@ export type ButtonGroupOption = ButtonGroupBasicOption | {
13
14
  iconViewBox?: string;
14
15
  iconSize?: string;
15
16
  dataAttribute?: string;
17
+ disabled?: boolean;
16
18
  };
@@ -1,9 +1,10 @@
1
1
  import type { ChainedCommands, Editor } from '@tiptap/vue-3';
2
- import { EditorState, Transaction, Selection } from 'prosemirror-state';
3
- import type { Content, TextStyleDeclaration, TextStyleType } from '../../brand-page';
2
+ import type { EditorState, Transaction, Selection } from 'prosemirror-state';
3
+ import { type Content, type TextStyleDeclaration, type TextStyleType } from '../../brand-page';
4
4
  import type { PageSettingsState } from '../../shared/page-settings/store';
5
5
  import type { StyleSettingsState } from '../../shared/style-settings/store';
6
6
  import type { Mark } from 'prosemirror-model';
7
+ import { type DOMPurify } from 'dompurify';
7
8
  export declare const isStyleChanged: (tr: Transaction) => boolean;
8
9
  export declare const skipStyleDetection: (chain: ChainedCommands, marks?: Mark[]) => ChainedCommands;
9
10
  export declare const hasRTEContent: (content: string) => boolean;
@@ -12,3 +13,19 @@ export declare const switchAllContentToRTE: (uiPageSettingStore: PageSettingsSta
12
13
  export declare const getUpdatedHTMLFromEditor: (externalEditor: Editor, contentText: string, newContentStyle: TextStyleDeclaration) => string;
13
14
  export declare const buildInitialMarks: (state: EditorState, style: TextStyleDeclaration) => any[];
14
15
  export declare const retainStyleOnEmptyLine: (editor: Editor, selection: Selection) => void;
16
+ export interface SanitizationResult {
17
+ sanitizedHtml: string;
18
+ removedElements: string[];
19
+ }
20
+ export declare const sanitiseClientSideRTE: (html: string) => SanitizationResult;
21
+ /**
22
+ * Sanitize HTML string using DOMPurify.
23
+ * Only allow tags and attributes that correspond to the enabled Tiptap extensions.
24
+ * Forbid dangerous tags and attributes.
25
+ * @param html HTML string to sanitize
26
+ * @returns sanitization result with sanitized HTML and removed elements info
27
+ */
28
+ export declare const sanitizeRTEHtml: (html: string, domPurify: DOMPurify) => SanitizationResult;
29
+ export declare const isLogoColor: (color: string) => boolean;
30
+ export declare const getRTEColor: (color: string | null | undefined, colorType?: 'text' | 'background', defaultValue?: string) => string;
31
+ export declare const hasLogoColorVar: (color: string) => boolean;