@frontify/guideline-blocks-settings 0.27.0 → 0.28.0

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 (214) hide show
  1. package/.eslintrc.js +1 -1
  2. package/CHANGELOG.md +15 -0
  3. package/README.md +24 -5
  4. package/package.json +46 -10
  5. package/postcss.config.js +8 -0
  6. package/setupTests.ts +13 -0
  7. package/src/components/Attachments/AttachmentItem.tsx +257 -0
  8. package/src/components/Attachments/Attachments.spec.ct.tsx +151 -0
  9. package/src/components/Attachments/Attachments.tsx +221 -0
  10. package/src/components/Attachments/index.ts +4 -0
  11. package/src/components/Attachments/types.ts +30 -0
  12. package/src/components/BlockInjectButton/BlockInjectButton.spec.ct.tsx +48 -0
  13. package/src/components/BlockInjectButton/BlockInjectButton.tsx +212 -0
  14. package/src/components/BlockInjectButton/index.ts +4 -0
  15. package/src/components/BlockInjectButton/types.ts +18 -0
  16. package/src/components/BlockItemWrapper/BlockItemWrapper.spec.ct.tsx +146 -0
  17. package/src/components/BlockItemWrapper/BlockItemWrapper.tsx +76 -0
  18. package/src/components/BlockItemWrapper/Toolbar.tsx +128 -0
  19. package/src/components/BlockItemWrapper/constants.ts +4 -0
  20. package/src/components/BlockItemWrapper/index.ts +5 -0
  21. package/src/components/BlockItemWrapper/types.ts +46 -0
  22. package/src/components/DownloadButton/DownloadButton.spec.ct.tsx +20 -0
  23. package/src/components/DownloadButton/DownloadButton.tsx +36 -0
  24. package/src/components/DownloadButton/index.ts +3 -0
  25. package/src/components/DownloadButton/types.ts +5 -0
  26. package/src/components/RichTextEditor/RichTextEditor.spec.ct.tsx +204 -0
  27. package/src/components/RichTextEditor/RichTextEditor.tsx +62 -0
  28. package/src/components/RichTextEditor/SerializedText.tsx +25 -0
  29. package/src/components/RichTextEditor/constants.ts +3 -0
  30. package/src/components/RichTextEditor/index.ts +6 -0
  31. package/src/components/RichTextEditor/pluginPresets/defaultPluginsWithLinkChooser.tsx +53 -0
  32. package/src/components/RichTextEditor/pluginPresets/index.ts +3 -0
  33. package/src/components/RichTextEditor/plugins/ButtonPlugin/ButtonMarkupElement/ButtonMarkupElementNode.tsx +74 -0
  34. package/src/components/RichTextEditor/plugins/ButtonPlugin/ButtonMarkupElement/index.ts +11 -0
  35. package/src/components/RichTextEditor/plugins/ButtonPlugin/components/ButtonButton.tsx +20 -0
  36. package/src/components/RichTextEditor/plugins/ButtonPlugin/components/ButtonToolbarButton.tsx +56 -0
  37. package/src/components/RichTextEditor/plugins/ButtonPlugin/components/FloatingButton/CustomFloatingButton.tsx +19 -0
  38. package/src/components/RichTextEditor/plugins/ButtonPlugin/components/FloatingButton/EditButtonModal/EditModal.tsx +42 -0
  39. package/src/components/RichTextEditor/plugins/ButtonPlugin/components/FloatingButton/FloatingButton.tsx +37 -0
  40. package/src/components/RichTextEditor/plugins/ButtonPlugin/components/FloatingButton/FloatingButtonEditButton.tsx +22 -0
  41. package/src/components/RichTextEditor/plugins/ButtonPlugin/components/FloatingButton/FloatingButtonUrlInput.tsx +30 -0
  42. package/src/components/RichTextEditor/plugins/ButtonPlugin/components/FloatingButton/InsertButtonModal/InsertButtonModal.tsx +81 -0
  43. package/src/components/RichTextEditor/plugins/ButtonPlugin/components/FloatingButton/InsertButtonModal/types.ts +13 -0
  44. package/src/components/RichTextEditor/plugins/ButtonPlugin/components/FloatingButton/InsertButtonModal/useInsertModal.ts +143 -0
  45. package/src/components/RichTextEditor/plugins/ButtonPlugin/components/FloatingButton/UnlinkButton.tsx +31 -0
  46. package/src/components/RichTextEditor/plugins/ButtonPlugin/components/FloatingButton/floatingButtonStore.ts +46 -0
  47. package/src/components/RichTextEditor/plugins/ButtonPlugin/components/FloatingButton/index.ts +12 -0
  48. package/src/components/RichTextEditor/plugins/ButtonPlugin/components/FloatingButton/useFloatingButtonEdit.ts +113 -0
  49. package/src/components/RichTextEditor/plugins/ButtonPlugin/components/FloatingButton/useFloatingButtonEnter.ts +21 -0
  50. package/src/components/RichTextEditor/plugins/ButtonPlugin/components/FloatingButton/useFloatingButtonEscape.ts +30 -0
  51. package/src/components/RichTextEditor/plugins/ButtonPlugin/components/FloatingButton/useFloatingButtonInsert.ts +71 -0
  52. package/src/components/RichTextEditor/plugins/ButtonPlugin/components/FloatingButton/useVirtualFloatingButton.ts +22 -0
  53. package/src/components/RichTextEditor/plugins/ButtonPlugin/components/index.ts +3 -0
  54. package/src/components/RichTextEditor/plugins/ButtonPlugin/createButtonPlugin.ts +116 -0
  55. package/src/components/RichTextEditor/plugins/ButtonPlugin/index.ts +7 -0
  56. package/src/components/RichTextEditor/plugins/ButtonPlugin/transforms/index.ts +8 -0
  57. package/src/components/RichTextEditor/plugins/ButtonPlugin/transforms/insertButton.ts +17 -0
  58. package/src/components/RichTextEditor/plugins/ButtonPlugin/transforms/submitFloatingButton.ts +40 -0
  59. package/src/components/RichTextEditor/plugins/ButtonPlugin/transforms/unwrapButton.ts +68 -0
  60. package/src/components/RichTextEditor/plugins/ButtonPlugin/transforms/upsertButton.ts +198 -0
  61. package/src/components/RichTextEditor/plugins/ButtonPlugin/transforms/upsertButtonText.ts +40 -0
  62. package/src/components/RichTextEditor/plugins/ButtonPlugin/transforms/wrapButton.ts +30 -0
  63. package/src/components/RichTextEditor/plugins/ButtonPlugin/types.ts +13 -0
  64. package/src/components/RichTextEditor/plugins/ButtonPlugin/utils/createButtonNode.ts +28 -0
  65. package/src/components/RichTextEditor/plugins/ButtonPlugin/utils/getButtonStyle.ts +14 -0
  66. package/src/components/RichTextEditor/plugins/ButtonPlugin/utils/getUrl.ts +18 -0
  67. package/src/components/RichTextEditor/plugins/ButtonPlugin/utils/index.ts +8 -0
  68. package/src/components/RichTextEditor/plugins/ButtonPlugin/utils/styles.ts +77 -0
  69. package/src/components/RichTextEditor/plugins/ButtonPlugin/utils/triggerFloatingButton.ts +23 -0
  70. package/src/components/RichTextEditor/plugins/ButtonPlugin/utils/triggerFloatingButtonEdit.ts +30 -0
  71. package/src/components/RichTextEditor/plugins/ButtonPlugin/utils/triggerFloatingButtonInsert.ts +45 -0
  72. package/src/components/RichTextEditor/plugins/ButtonPlugin/withButton.ts +106 -0
  73. package/src/components/RichTextEditor/plugins/LinkPlugin/FloatingLink/CustomFloatingLink.tsx +26 -0
  74. package/src/components/RichTextEditor/plugins/LinkPlugin/FloatingLink/EditLinkModal/EditModal.tsx +43 -0
  75. package/src/components/RichTextEditor/plugins/LinkPlugin/FloatingLink/EditLinkModal/index.ts +4 -0
  76. package/src/components/RichTextEditor/plugins/LinkPlugin/FloatingLink/EditLinkModal/useFloatingLinkEdit.ts +113 -0
  77. package/src/components/RichTextEditor/plugins/LinkPlugin/FloatingLink/FloatingLink.tsx +45 -0
  78. package/src/components/RichTextEditor/plugins/LinkPlugin/FloatingLink/InsertLinkModal/InsertLinkModal.tsx +5 -0
  79. package/src/components/RichTextEditor/plugins/LinkPlugin/FloatingLink/InsertLinkModal/InsertModal.tsx +105 -0
  80. package/src/components/RichTextEditor/plugins/LinkPlugin/FloatingLink/InsertLinkModal/index.ts +4 -0
  81. package/src/components/RichTextEditor/plugins/LinkPlugin/FloatingLink/InsertLinkModal/types.ts +16 -0
  82. package/src/components/RichTextEditor/plugins/LinkPlugin/FloatingLink/InsertLinkModal/useFloatingLinkInsert.ts +73 -0
  83. package/src/components/RichTextEditor/plugins/LinkPlugin/FloatingLink/InsertLinkModal/useInsertModal.ts +136 -0
  84. package/src/components/RichTextEditor/plugins/LinkPlugin/LinkButton.tsx +38 -0
  85. package/src/components/RichTextEditor/plugins/LinkPlugin/LinkMarkupElement/LinkMarkupElementNode.tsx +36 -0
  86. package/src/components/RichTextEditor/plugins/LinkPlugin/LinkMarkupElement/index.ts +11 -0
  87. package/src/components/RichTextEditor/plugins/LinkPlugin/id.ts +3 -0
  88. package/src/components/RichTextEditor/plugins/LinkPlugin/index.ts +48 -0
  89. package/src/components/RichTextEditor/plugins/LinkPlugin/types.ts +12 -0
  90. package/src/components/RichTextEditor/plugins/LinkPlugin/utils/getUrl.ts +30 -0
  91. package/src/components/RichTextEditor/plugins/LinkPlugin/utils/index.ts +4 -0
  92. package/src/components/RichTextEditor/plugins/LinkPlugin/utils/relativeUrlRegex.spec.ts +35 -0
  93. package/src/components/RichTextEditor/plugins/LinkPlugin/utils/relativeUrlRegex.ts +3 -0
  94. package/src/components/RichTextEditor/plugins/LinkPlugin/utils/url.spec.ts +75 -0
  95. package/src/components/RichTextEditor/plugins/LinkPlugin/utils/url.ts +21 -0
  96. package/src/components/RichTextEditor/plugins/TextStylePlugins/custom1Plugin.tsx +61 -0
  97. package/src/components/RichTextEditor/plugins/TextStylePlugins/custom2Plugin.tsx +61 -0
  98. package/src/components/RichTextEditor/plugins/TextStylePlugins/custom3Plugin.tsx +62 -0
  99. package/src/components/RichTextEditor/plugins/TextStylePlugins/heading1Plugin.tsx +61 -0
  100. package/src/components/RichTextEditor/plugins/TextStylePlugins/heading2Plugin.tsx +58 -0
  101. package/src/components/RichTextEditor/plugins/TextStylePlugins/heading3Plugin.tsx +58 -0
  102. package/src/components/RichTextEditor/plugins/TextStylePlugins/heading4Plugin.tsx +59 -0
  103. package/src/components/RichTextEditor/plugins/TextStylePlugins/helpers.tsx +44 -0
  104. package/src/components/RichTextEditor/plugins/TextStylePlugins/imageCaptionPlugin.tsx +61 -0
  105. package/src/components/RichTextEditor/plugins/TextStylePlugins/imageTitlePlugin.tsx +61 -0
  106. package/src/components/RichTextEditor/plugins/TextStylePlugins/index.ts +15 -0
  107. package/src/components/RichTextEditor/plugins/TextStylePlugins/paragraphPlugin.tsx +58 -0
  108. package/src/components/RichTextEditor/plugins/TextStylePlugins/quotePlugin.tsx +62 -0
  109. package/src/components/RichTextEditor/plugins/index.ts +6 -0
  110. package/src/components/RichTextEditor/plugins/shared/LinkSelector/DocumentLink.tsx +80 -0
  111. package/src/components/RichTextEditor/plugins/shared/LinkSelector/DocumentLinks.tsx +97 -0
  112. package/src/components/RichTextEditor/plugins/shared/LinkSelector/LinkSelector.spec.ct.tsx +138 -0
  113. package/src/components/RichTextEditor/plugins/shared/LinkSelector/LinkSelector.tsx +80 -0
  114. package/src/components/RichTextEditor/plugins/shared/LinkSelector/PageLink.tsx +83 -0
  115. package/src/components/RichTextEditor/plugins/shared/LinkSelector/PageLinks.tsx +68 -0
  116. package/src/components/RichTextEditor/plugins/shared/LinkSelector/SectionLink.tsx +37 -0
  117. package/src/components/RichTextEditor/plugins/shared/LinkSelector/index.ts +3 -0
  118. package/src/components/RichTextEditor/plugins/styles.ts +179 -0
  119. package/src/components/RichTextEditor/serializer/index.ts +3 -0
  120. package/src/components/RichTextEditor/serializer/nodes/button.ts +25 -0
  121. package/src/components/RichTextEditor/serializer/nodes/checkItemNode.ts +29 -0
  122. package/src/components/RichTextEditor/serializer/nodes/default.ts +52 -0
  123. package/src/components/RichTextEditor/serializer/nodes/link.ts +25 -0
  124. package/src/components/RichTextEditor/serializer/nodes/mentionHtmlNode.ts +17 -0
  125. package/src/components/RichTextEditor/serializer/serializeNodesToHtmlRecursive.ts +134 -0
  126. package/src/components/RichTextEditor/serializer/serializeToHtml.ts +49 -0
  127. package/src/components/RichTextEditor/serializer/utlis/reactCssPropsToCss.ts +21 -0
  128. package/src/components/RichTextEditor/serializer/utlis/serializeLeafToHtml.ts +32 -0
  129. package/src/components/RichTextEditor/types.ts +23 -0
  130. package/src/components/index.ts +7 -0
  131. package/src/helpers/addHttps.spec.ts +42 -0
  132. package/src/helpers/addHttps.ts +15 -0
  133. package/src/helpers/convertToRichTextValue.spec.ts +32 -0
  134. package/src/helpers/convertToRichTextValue.ts +6 -0
  135. package/src/helpers/customCoordinatesGetterFactory.spec.ts +69 -0
  136. package/src/helpers/customCoordinatesGetterFactory.ts +39 -0
  137. package/src/helpers/hasRichTextValue.spec.ts +63 -0
  138. package/src/helpers/hasRichTextValue.ts +29 -0
  139. package/src/helpers/index.ts +8 -0
  140. package/src/helpers/isDownloadable.spec.ts +47 -0
  141. package/src/helpers/isDownloadable.ts +7 -0
  142. package/src/helpers/mapColorPalettes.spec.ts +146 -0
  143. package/src/helpers/mapColorPalettes.ts +22 -0
  144. package/src/hooks/index.ts +4 -0
  145. package/src/hooks/useAttachments.spec.ts +79 -0
  146. package/src/hooks/useAttachments.ts +46 -0
  147. package/src/hooks/useDndSensors.spec.ts +40 -0
  148. package/src/hooks/useDndSensors.ts +23 -0
  149. package/src/index.ts +8 -0
  150. package/src/settings/background.spec.ts +173 -0
  151. package/src/settings/background.ts +49 -0
  152. package/src/settings/border.spec.ts +76 -0
  153. package/src/settings/border.ts +90 -0
  154. package/src/settings/borderRadius.spec.ts +30 -0
  155. package/src/settings/borderRadius.ts +73 -0
  156. package/src/settings/borderRadiusExtended.spec.ts +52 -0
  157. package/src/settings/borderRadiusExtended.ts +84 -0
  158. package/src/settings/defaultValues.ts +21 -0
  159. package/src/settings/gutter.spec.ts +60 -0
  160. package/src/settings/gutter.ts +75 -0
  161. package/src/settings/index.ts +14 -0
  162. package/src/settings/margin.spec.ts +42 -0
  163. package/src/settings/margin.ts +72 -0
  164. package/src/settings/marginExtended.spec.ts +45 -0
  165. package/src/settings/marginExtended.ts +91 -0
  166. package/src/settings/padding.spec.ts +42 -0
  167. package/src/settings/padding.ts +73 -0
  168. package/src/settings/paddingExtended.spec.ts +45 -0
  169. package/src/settings/paddingExtended.ts +91 -0
  170. package/src/settings/security.spec.ts +87 -0
  171. package/src/settings/security.ts +61 -0
  172. package/src/settings/securityDownloadable.spec.ts +46 -0
  173. package/src/settings/securityDownloadable.ts +33 -0
  174. package/src/settings/securityGlobalControl.ts +42 -0
  175. package/src/settings/types.ts +128 -0
  176. package/src/utilities/color/getReadableColor.spec.ts +32 -0
  177. package/src/utilities/color/getReadableColor.ts +34 -0
  178. package/src/utilities/color/index.ts +10 -0
  179. package/src/utilities/color/isDark.spec.ts +33 -0
  180. package/src/utilities/color/isDark.ts +29 -0
  181. package/src/utilities/color/setAlpha.spec.ts +28 -0
  182. package/src/utilities/color/setAlpha.ts +14 -0
  183. package/src/utilities/color/toColorObject.spec.ts +19 -0
  184. package/src/utilities/color/toColorObject.ts +16 -0
  185. package/src/utilities/color/toHex8String.spec.ts +17 -0
  186. package/src/utilities/color/toHex8String.ts +14 -0
  187. package/src/utilities/color/toHexString.spec.ts +17 -0
  188. package/src/utilities/color/toHexString.ts +10 -0
  189. package/src/utilities/color/toRgbaString.spec.ts +12 -0
  190. package/src/utilities/color/toRgbaString.ts +14 -0
  191. package/src/utilities/color/toShortRgba.spec.ts +16 -0
  192. package/src/utilities/color/toShortRgba.ts +35 -0
  193. package/src/utilities/index.ts +5 -0
  194. package/src/utilities/moveItemInArray.spec.ts +17 -0
  195. package/src/utilities/moveItemInArray.ts +21 -0
  196. package/src/utilities/react/getBackgroundColorStyles.spec.ts +18 -0
  197. package/src/utilities/react/getBackgroundColorStyles.ts +11 -0
  198. package/src/utilities/react/getBorderStyles.spec.ts +39 -0
  199. package/src/utilities/react/getBorderStyles.ts +21 -0
  200. package/src/utilities/react/getRadiusStyles.spec.ts +25 -0
  201. package/src/utilities/react/getRadiusStyles.ts +8 -0
  202. package/src/utilities/react/index.ts +6 -0
  203. package/src/utilities/react/joinClassNames.spec.ts +18 -0
  204. package/src/utilities/react/joinClassNames.ts +10 -0
  205. package/tailwind.config.js +27 -0
  206. package/tsconfig.json +3 -1
  207. package/vite.config.ts +11 -1
  208. package/dist/index.cjs.js +0 -2
  209. package/dist/index.cjs.js.map +0 -1
  210. package/dist/index.d.ts +0 -147
  211. package/dist/index.es.js +0 -9
  212. package/dist/index.es.js.map +0 -1
  213. package/dist/index.umd.js +0 -2
  214. package/dist/index.umd.js.map +0 -1
@@ -0,0 +1,204 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { DocumentApiDummy, getAppBridgeBlockStub } from '@frontify/app-bridge';
4
+ import { PluginComposer } from '@frontify/fondue';
5
+ import { mount } from 'cypress/react18';
6
+ import { SinonStub } from 'cypress/types/sinon';
7
+ import { ButtonPlugin, LinkPlugin, RichTextEditor, TextStyles } from '.';
8
+ import { convertToRteValue } from '../../helpers';
9
+
10
+ const RteHtmlSelector = '[data-test-id="rte-content-html"]';
11
+ const RichTextSelector = '[data-test-id="rich-text-editor"]';
12
+ const ButtonSelector = '[data-test-id="button"]';
13
+ const ToolbarButtonSelector = '[data-testid="ToolbarButton"]';
14
+ const InternalDocumentLinkSelector = '[data-test-id="internal-link-selector-document-link"]';
15
+ const FloatingLinkModalSelector = '[data-test-id="floating-link-insert"]';
16
+ const FloatingButtonModalSelector = '[data-test-id="floating-button-insert"]';
17
+ const UrlInputSelector = 'input[id="url"]';
18
+
19
+ const apiDocuments = [{ ...DocumentApiDummy.with(1), permanentLink: '/r/document' }];
20
+
21
+ const appBridge = getAppBridgeBlockStub({
22
+ blockId: 1,
23
+ });
24
+
25
+ describe('RichTextEditor', () => {
26
+ it('should render a rich text editor in edit mode', () => {
27
+ mount(<RichTextEditor isEditing />);
28
+ cy.get(RichTextSelector).should('exist');
29
+ });
30
+
31
+ it('should render a rich text html in view mode', () => {
32
+ mount(<RichTextEditor isEditing={false} value="test" />);
33
+ cy.get(RteHtmlSelector).should('exist');
34
+ });
35
+
36
+ it('should render a json value in view mode', () => {
37
+ mount(<RichTextEditor isEditing={false} value={convertToRteValue(TextStyles.heading1, 'Test Heading')} />);
38
+ cy.get(RteHtmlSelector).should('exist');
39
+ });
40
+
41
+ it('should render a html value in view mode', () => {
42
+ mount(<RichTextEditor isEditing={false} value="<p>Test Paragraph</p>" />);
43
+ cy.get(RteHtmlSelector).should('exist');
44
+ });
45
+
46
+ it('should not render html output if value is empty', () => {
47
+ mount(<RichTextEditor isEditing={false} value="" />);
48
+ cy.get(RteHtmlSelector).should('not.exist');
49
+ });
50
+
51
+ it('should not render html output if value is undefined', () => {
52
+ mount(<RichTextEditor isEditing={false} />);
53
+ cy.get(RteHtmlSelector).should('not.exist');
54
+ });
55
+
56
+ it('should be able to select internal link', () => {
57
+ (appBridge.getDocumentGroups as SinonStub) = cy.stub().returns([]);
58
+ (appBridge.getAllDocuments as SinonStub) = cy.stub().returns(Promise.resolve(apiDocuments));
59
+
60
+ mount(
61
+ <RichTextEditor
62
+ isEditing={true}
63
+ plugins={new PluginComposer().setPlugin([new LinkPlugin({ appBridge })])}
64
+ value={convertToRteValue('p', 'This is a link')}
65
+ />
66
+ );
67
+ cy.get(RichTextSelector).click();
68
+ cy.get(RichTextSelector).type('{selectall}');
69
+ cy.get(ToolbarButtonSelector).click();
70
+ cy.get(ButtonSelector).first().click();
71
+ cy.get(InternalDocumentLinkSelector).click();
72
+ cy.get(ButtonSelector).last().click();
73
+ cy.get(FloatingLinkModalSelector).find(ButtonSelector).last().click();
74
+ cy.get(RichTextSelector).find('a[href="/r/document"]').should('exist');
75
+ });
76
+
77
+ it('should prepend the URL with https:// if not exists', () => {
78
+ (appBridge.getDocumentGroups as SinonStub) = cy.stub().returns([]);
79
+ (appBridge.getAllDocuments as SinonStub) = cy.stub().returns(Promise.resolve(apiDocuments));
80
+
81
+ mount(
82
+ <RichTextEditor
83
+ isEditing={true}
84
+ plugins={new PluginComposer().setPlugin([new LinkPlugin({ appBridge })])}
85
+ value={convertToRteValue('p', 'This is a link')}
86
+ />
87
+ );
88
+ cy.get(RichTextSelector).click();
89
+ cy.get(RichTextSelector).type('{selectall}');
90
+ cy.get(ToolbarButtonSelector).click();
91
+ cy.get(UrlInputSelector).type('frontify.com');
92
+ cy.get(FloatingLinkModalSelector).find(ButtonSelector).last().click();
93
+ cy.get(RichTextSelector).find('a[href="https://frontify.com"]').should('exist');
94
+ });
95
+
96
+ it('should allow URLs that start with /document/', () => {
97
+ (appBridge.getDocumentGroups as SinonStub) = cy.stub().returns([]);
98
+ (appBridge.getAllDocuments as SinonStub) = cy.stub().returns(Promise.resolve(apiDocuments));
99
+
100
+ mount(
101
+ <RichTextEditor
102
+ isEditing={true}
103
+ plugins={new PluginComposer().setPlugin([new LinkPlugin({ appBridge })])}
104
+ value={convertToRteValue('p', 'This is a link')}
105
+ />
106
+ );
107
+
108
+ cy.get(RichTextSelector).click();
109
+ cy.get(RichTextSelector).type('{selectall}');
110
+ cy.get(ToolbarButtonSelector).click();
111
+ cy.get(UrlInputSelector).type('/document/test');
112
+ cy.get(FloatingLinkModalSelector).find(ButtonSelector).last().click();
113
+ cy.get(RichTextSelector).find('a[href="/document/test"]').should('exist');
114
+ });
115
+
116
+ it('should not add https:// to the URL for mailto: links', () => {
117
+ (appBridge.getDocumentGroups as SinonStub) = cy.stub().returns([]);
118
+ (appBridge.getAllDocuments as SinonStub) = cy.stub().returns(Promise.resolve(apiDocuments));
119
+
120
+ mount(
121
+ <RichTextEditor
122
+ isEditing={true}
123
+ plugins={new PluginComposer().setPlugin([new LinkPlugin({ appBridge })])}
124
+ value={convertToRteValue('p', 'This is a link')}
125
+ />
126
+ );
127
+ cy.get(RichTextSelector).click();
128
+ cy.get(RichTextSelector).type('{selectall}');
129
+ cy.get(ToolbarButtonSelector).click();
130
+ cy.get(UrlInputSelector).type('mailto:info@frontify.com');
131
+ cy.get(FloatingLinkModalSelector).find(ButtonSelector).last().click();
132
+ cy.get(RichTextSelector).find('a[href="mailto:info@frontify.com"]').should('exist');
133
+ });
134
+
135
+ it('should create a link with a link typed in the RTE', () => {
136
+ (appBridge.getDocumentGroups as SinonStub) = cy.stub().returns([]);
137
+ (appBridge.getAllDocuments as SinonStub) = cy.stub().returns(Promise.resolve(apiDocuments));
138
+
139
+ mount(
140
+ <RichTextEditor
141
+ isEditing={true}
142
+ plugins={new PluginComposer().setPlugin([new LinkPlugin({ appBridge })])}
143
+ />
144
+ );
145
+ cy.get(RichTextSelector).click();
146
+ cy.get(RichTextSelector).type('mailto:info@frontify.com {enter}');
147
+ cy.get(RichTextSelector).find('a[href="mailto:info@frontify.com"]').should('exist');
148
+ });
149
+
150
+ it('should not create a link with a : after a word', () => {
151
+ (appBridge.getDocumentGroups as SinonStub) = cy.stub().returns([]);
152
+ (appBridge.getAllDocuments as SinonStub) = cy.stub().returns(Promise.resolve(apiDocuments));
153
+
154
+ mount(
155
+ <RichTextEditor
156
+ isEditing={true}
157
+ plugins={new PluginComposer().setPlugin([new LinkPlugin({ appBridge })])}
158
+ />
159
+ );
160
+ cy.get(RichTextSelector).click();
161
+ cy.get(RichTextSelector).type('list:{enter}');
162
+ cy.get(RichTextSelector).find('a').should('not.exist');
163
+ });
164
+
165
+ it('should allow URLs that start with /document/', () => {
166
+ (appBridge.getDocumentGroups as SinonStub) = cy.stub().returns([]);
167
+ (appBridge.getAllDocuments as SinonStub) = cy.stub().returns(Promise.resolve(apiDocuments));
168
+
169
+ mount(
170
+ <RichTextEditor
171
+ isEditing={true}
172
+ plugins={new PluginComposer().setPlugin([new LinkPlugin({ appBridge })])}
173
+ value={convertToRteValue('p', 'This is a link')}
174
+ />
175
+ );
176
+ cy.get(RichTextSelector).click();
177
+ cy.get(RichTextSelector).type('{selectall}');
178
+ cy.get(ToolbarButtonSelector).click();
179
+ cy.get(UrlInputSelector).type('/document/test');
180
+ cy.get(FloatingLinkModalSelector).find(ButtonSelector).last().click();
181
+ cy.get(RichTextSelector).find('a[href="/document/test"]').should('exist');
182
+ });
183
+
184
+ it('should be able to select internal button link', () => {
185
+ (appBridge.getDocumentGroups as SinonStub) = cy.stub().returns([]);
186
+ (appBridge.getAllDocuments as SinonStub) = cy.stub().returns(Promise.resolve(apiDocuments));
187
+
188
+ mount(
189
+ <RichTextEditor
190
+ isEditing={true}
191
+ plugins={new PluginComposer().setPlugin([new ButtonPlugin({ appBridge })])}
192
+ value={convertToRteValue('p', 'This is a button')}
193
+ />
194
+ );
195
+ cy.get(RichTextSelector).click();
196
+ cy.get(RichTextSelector).type('{selectall}');
197
+ cy.get(ToolbarButtonSelector).click();
198
+ cy.get(ButtonSelector).first().click();
199
+ cy.get(InternalDocumentLinkSelector).click();
200
+ cy.get(ButtonSelector).last().click();
201
+ cy.get(FloatingButtonModalSelector).find(ButtonSelector).last().click();
202
+ cy.get(RichTextSelector).find('a[href="/r/document"]').should('exist');
203
+ });
204
+ });
@@ -0,0 +1,62 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { useEffect, useState } from 'react';
4
+
5
+ import { RichTextEditor as FondueRichTextEditor } from '@frontify/fondue';
6
+ import { RichTextEditorProps } from './types';
7
+ import { SerializedText } from './SerializedText';
8
+ import { floatingButtonActions, floatingButtonSelectors } from './plugins/ButtonPlugin/components';
9
+
10
+ export const RichTextEditor = ({
11
+ id = 'rte',
12
+ isEditing,
13
+ value,
14
+ columns,
15
+ gap,
16
+ placeholder,
17
+ plugins,
18
+ onTextChange,
19
+ showSerializedText,
20
+ }: RichTextEditorProps) => {
21
+ const [shouldPreventPageLeave, setShouldPreventPageLeave] = useState(false);
22
+
23
+ const saveText = (newContent: string) => {
24
+ if (onTextChange && newContent !== value) {
25
+ onTextChange(newContent);
26
+ }
27
+ setShouldPreventPageLeave(false);
28
+ };
29
+
30
+ useEffect(() => {
31
+ const unloadHandler = (event: BeforeUnloadEvent) => {
32
+ event.preventDefault();
33
+ return (event.returnValue = 'Unprocessed changes');
34
+ };
35
+
36
+ if (shouldPreventPageLeave) {
37
+ window.addEventListener('beforeunload', unloadHandler);
38
+ }
39
+
40
+ return () => window.removeEventListener('beforeunload', unloadHandler);
41
+ }, [shouldPreventPageLeave]);
42
+
43
+ if (isEditing) {
44
+ return (
45
+ <FondueRichTextEditor
46
+ id={id}
47
+ value={value}
48
+ border={false}
49
+ placeholder={placeholder}
50
+ plugins={plugins}
51
+ onValueChanged={() => setShouldPreventPageLeave(true)}
52
+ onTextChange={saveText}
53
+ hideExternalFloatingModals={(editorId: string) => {
54
+ if (floatingButtonSelectors.isOpen(editorId)) {
55
+ floatingButtonActions.reset();
56
+ }
57
+ }}
58
+ />
59
+ );
60
+ }
61
+ return <SerializedText value={value} columns={columns} gap={gap} show={showSerializedText} plugins={plugins} />;
62
+ };
@@ -0,0 +1,25 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { SerializedTextProps } from './types';
5
+ import { serializeRawToHtmlAsync } from './serializer';
6
+
7
+ export const SerializedText = ({ value = '', gap, columns, show = true, plugins }: SerializedTextProps) => {
8
+ const [html, setHtml] = useState<string | null>(null);
9
+
10
+ useEffect(() => {
11
+ (async () => {
12
+ setHtml(await serializeRawToHtmlAsync(value, columns, gap, plugins));
13
+ })();
14
+ }, [value, columns, gap, plugins]);
15
+
16
+ if (!show || html === '<br />') {
17
+ return null;
18
+ }
19
+
20
+ return html !== null ? (
21
+ <div className="tw-w-full" data-test-id="rte-content-html" dangerouslySetInnerHTML={{ __html: html }} />
22
+ ) : (
23
+ <div className="tw-rounded-sm tw-bg-base-alt tw-animate-pulse tw-h-full tw-min-h-[10px] tw-w-full" />
24
+ );
25
+ };
@@ -0,0 +1,3 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ export const THEME_PREFIX = '--f-theme-settings-';
@@ -0,0 +1,6 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ export * from './RichTextEditor';
4
+ export * from './plugins';
5
+ export * from './pluginPresets';
6
+ export * from './constants';
@@ -0,0 +1,53 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { AppBridgeBlock } from '@frontify/app-bridge';
4
+ import {
5
+ AlignCenterPlugin,
6
+ AlignJustifyPlugin,
7
+ AlignLeftPlugin,
8
+ AlignRightPlugin,
9
+ BoldPlugin,
10
+ CheckboxListPlugin,
11
+ CodePlugin,
12
+ ItalicPlugin,
13
+ OrderedListPlugin,
14
+ PluginComposer,
15
+ ResetFormattingPlugin,
16
+ SoftBreakPlugin,
17
+ StrikethroughPlugin,
18
+ TextStylePlugin,
19
+ UnderlinePlugin,
20
+ UnorderedListPlugin,
21
+ } from '@frontify/fondue';
22
+ import { ButtonPlugin, LinkPlugin, TextStylePluginsWithoutImage, TextStylesWithoutImage } from '../plugins';
23
+
24
+ export const getDefaultPluginsWithLinkChooser = (appBridge: AppBridgeBlock) => {
25
+ return new PluginComposer()
26
+ .setPlugin(
27
+ new SoftBreakPlugin(),
28
+ new TextStylePlugin({
29
+ textStyles: TextStylePluginsWithoutImage,
30
+ })
31
+ )
32
+ .setPlugin(
33
+ [
34
+ new BoldPlugin(),
35
+ new ItalicPlugin(),
36
+ new UnderlinePlugin(),
37
+ new StrikethroughPlugin(),
38
+ new LinkPlugin({ appBridge }),
39
+ new ButtonPlugin({ appBridge }),
40
+ new CodePlugin(),
41
+ ],
42
+ [
43
+ new AlignLeftPlugin({ validTypes: TextStylesWithoutImage }),
44
+ new AlignCenterPlugin({ validTypes: TextStylesWithoutImage }),
45
+ new AlignRightPlugin({ validTypes: TextStylesWithoutImage }),
46
+ new AlignJustifyPlugin({ validTypes: TextStylesWithoutImage }),
47
+ new UnorderedListPlugin(),
48
+ new CheckboxListPlugin(),
49
+ new OrderedListPlugin(),
50
+ new ResetFormattingPlugin(),
51
+ ]
52
+ );
53
+ };
@@ -0,0 +1,3 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ export * from './defaultPluginsWithLinkChooser';
@@ -0,0 +1,74 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { HTMLPropsAs, PlateRenderElementProps, Value, useElementProps } from '@udecode/plate';
4
+ import { CSSProperties, HTMLAttributeAnchorTarget, ReactElement, ReactNode, useState } from 'react';
5
+ import { RichTextButtonStyle, TButtonElement } from '../types';
6
+ import { BlockButtonStyles } from '../utils';
7
+
8
+ export type ButtonRootProps = PlateRenderElementProps<Value, TButtonElement> & HTMLPropsAs<'a'>;
9
+
10
+ const useButton = (props: ButtonRootProps): HTMLPropsAs<'a'> & { buttonStyle: RichTextButtonStyle } => {
11
+ const _props = useElementProps<TButtonElement, 'a'>({
12
+ ...props,
13
+ elementToAttributes: (element) => ({
14
+ url: element.href,
15
+ buttonStyle: element.buttonStyle || 'primary',
16
+ target: element.target || '_blank',
17
+ }),
18
+ });
19
+
20
+ return {
21
+ ...(_props as HTMLPropsAs<'a'> & { buttonStyle: RichTextButtonStyle }),
22
+ // quick fix: hovering <a> with href loses the editor focus
23
+ onMouseOver: (e) => {
24
+ e.stopPropagation();
25
+ },
26
+ };
27
+ };
28
+
29
+ export const ButtonMarkupElementNode = (props: ButtonRootProps) => {
30
+ const { href, target, buttonStyle } = useButton(props);
31
+ const { attributes, children } = props;
32
+
33
+ return (
34
+ <HoverableButtonLink
35
+ attributes={attributes}
36
+ href={href}
37
+ target={target}
38
+ styles={BlockButtonStyles[`button${buttonStyle.charAt(0).toUpperCase() + buttonStyle.slice(1)}`]}
39
+ >
40
+ {children}
41
+ </HoverableButtonLink>
42
+ );
43
+ };
44
+
45
+ type Props = {
46
+ attributes: ButtonRootProps['attributes'];
47
+ children: ReactNode;
48
+ styles?: CSSProperties & { hover?: CSSProperties };
49
+ href?: string;
50
+ target?: HTMLAttributeAnchorTarget;
51
+ };
52
+
53
+ const HoverableButtonLink = ({
54
+ attributes,
55
+ styles = { hover: {} },
56
+ children,
57
+ href = '#',
58
+ target,
59
+ }: Props): ReactElement => {
60
+ const [hovered, setHovered] = useState(false);
61
+
62
+ return (
63
+ <a
64
+ {...attributes}
65
+ onMouseEnter={() => setHovered(true)}
66
+ onMouseLeave={() => setHovered(false)}
67
+ href={href}
68
+ target={target}
69
+ style={hovered ? { ...styles, ...styles.hover } : styles}
70
+ >
71
+ {children}
72
+ </a>
73
+ );
74
+ };
@@ -0,0 +1,11 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { MarkupElement } from '@frontify/fondue';
4
+ import { ELEMENT_BUTTON } from '../createButtonPlugin';
5
+ import { ButtonMarkupElementNode } from './ButtonMarkupElementNode';
6
+
7
+ export class ButtonMarkupElement extends MarkupElement {
8
+ constructor(id = ELEMENT_BUTTON, node = ButtonMarkupElementNode) {
9
+ super(id, node);
10
+ }
11
+ }
@@ -0,0 +1,20 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { IconButton16, PluginButtonProps, buttonStyles } from '@frontify/fondue';
4
+ import { getPluginType } from '@udecode/plate';
5
+ import { ELEMENT_BUTTON } from '../createButtonPlugin';
6
+ import { ButtonToolbarButton } from './ButtonToolbarButton';
7
+
8
+ export const ButtonButton = ({ editor, id }: PluginButtonProps) => (
9
+ <div data-plugin-id={id}>
10
+ <ButtonToolbarButton
11
+ type={getPluginType(editor, ELEMENT_BUTTON)}
12
+ icon={
13
+ <span className="tw-p-2 tw-h-8 tw-justify-center tw-items-center tw-flex">
14
+ <IconButton16 />
15
+ </span>
16
+ }
17
+ styles={buttonStyles}
18
+ />
19
+ </div>
20
+ );
@@ -0,0 +1,56 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { getButtonClassNames, getHotkeyByPlatform, getTooltip } from '@frontify/fondue';
4
+ import {
5
+ BlockToolbarButtonProps,
6
+ ToolbarButton,
7
+ focusEditor,
8
+ isRangeInSameBlock,
9
+ someNode,
10
+ useEditorRef,
11
+ } from '@udecode/plate';
12
+
13
+ import { triggerFloatingButton } from '../utils';
14
+
15
+ export interface LinkToolbarButtonProps extends BlockToolbarButtonProps {
16
+ /**
17
+ * Default onMouseDown is getting the link url by calling this promise before inserting the image.
18
+ */
19
+ getLinkUrl?: (prevUrl: string | null) => Promise<string | null>;
20
+ }
21
+
22
+ export const ButtonToolbarButton = ({ type, ...props }: LinkToolbarButtonProps) => {
23
+ const editor = useEditorRef();
24
+ const isEnabled = !!isRangeInSameBlock(editor, {
25
+ at: editor.selection,
26
+ });
27
+
28
+ const isLink = !!editor?.selection && someNode(editor, { match: { type } });
29
+
30
+ return (
31
+ <ToolbarButton
32
+ tooltip={getTooltip(
33
+ isEnabled
34
+ ? `Button\n${getHotkeyByPlatform('Ctrl+Shift+K')}`
35
+ : 'Buttons can only be set for a single text block.'
36
+ )}
37
+ classNames={getButtonClassNames(isEnabled)}
38
+ active={isLink}
39
+ onMouseDown={async (event) => {
40
+ if (!editor) {
41
+ return;
42
+ }
43
+
44
+ event.preventDefault();
45
+ event.stopPropagation();
46
+
47
+ focusEditor(editor, editor.selection ?? editor.prevSelection ?? undefined);
48
+
49
+ setTimeout(() => {
50
+ triggerFloatingButton(editor, { focused: true });
51
+ }, 0);
52
+ }}
53
+ {...props}
54
+ />
55
+ );
56
+ };
@@ -0,0 +1,19 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { EditModal } from './EditButtonModal/EditModal';
4
+ import { FloatingButton } from './FloatingButton';
5
+ import { useFloatingButtonSelectors } from './floatingButtonStore';
6
+ import { InsertButtonModal } from './InsertButtonModal/InsertButtonModal';
7
+
8
+ export const CustomFloatingButton = () => {
9
+ const isEditing = useFloatingButtonSelectors().isEditing();
10
+
11
+ const input = <InsertButtonModal />;
12
+ const editContent = isEditing ? input : <EditModal />;
13
+ return (
14
+ <>
15
+ <FloatingButton.InsertRoot>{input}</FloatingButton.InsertRoot>
16
+ <FloatingButton.EditRoot>{editContent}</FloatingButton.EditRoot>
17
+ </>
18
+ );
19
+ };
@@ -0,0 +1,42 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { IconPen16, IconTrashBin16 } from '@frontify/fondue';
4
+ import { FloatingButton, useFloatingButtonUrlInput } from '..';
5
+ import { BlockStyles } from '../../../..';
6
+
7
+ export const EditModal = () => {
8
+ const urlHtmlProps = useFloatingButtonUrlInput({});
9
+
10
+ return (
11
+ <div data-test-id="floating-button-edit" className="tw-bg-white tw-rounded tw-shadow tw-p-4 tw-min-w-[400px]">
12
+ <span data-test-id="preview-button-flyout" className="tw-flex tw-justify-between">
13
+ <span className="tw-pointer-events-none" style={BlockStyles.p}>
14
+ {urlHtmlProps.defaultValue}
15
+ </span>
16
+ <span className="tw-flex tw-gap-2">
17
+ <span
18
+ role="button"
19
+ tabIndex={0}
20
+ data-test-id="edit-button-button"
21
+ className="tw-transition tw-cursor-pointer tw-rounded hover:tw-bg-black-10 tw-p-1"
22
+ >
23
+ <FloatingButton.EditButton>
24
+ <IconPen16 />
25
+ </FloatingButton.EditButton>
26
+ </span>
27
+
28
+ <span
29
+ role="button"
30
+ tabIndex={0}
31
+ data-test-id="remove-button-button"
32
+ className="tw-transition tw-cursor-pointer tw-rounded hover:tw-bg-black-10 tw-p-1"
33
+ >
34
+ <FloatingButton.UnlinkButton>
35
+ <IconTrashBin16 />
36
+ </FloatingButton.UnlinkButton>
37
+ </span>
38
+ </span>
39
+ </span>
40
+ </div>
41
+ );
42
+ };
@@ -0,0 +1,37 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { HTMLPropsAs, UseVirtualFloatingOptions, createComponentAs, createElementAs } from '@udecode/plate';
4
+ import { useFloatingButtonEdit } from './useFloatingButtonEdit';
5
+ import { useFloatingButtonInsert } from './useFloatingButtonInsert';
6
+ import { FloatingButtonEditButton } from './FloatingButtonEditButton';
7
+ import { UnlinkButton } from './UnlinkButton';
8
+
9
+ export type FloatingButtonProps = HTMLPropsAs<'div'> & {
10
+ floatingOptions?: UseVirtualFloatingOptions;
11
+ };
12
+
13
+ export const FloatingButtonEditRoot = createComponentAs<FloatingButtonProps>((props) => {
14
+ const htmlProps = useFloatingButtonEdit(props);
15
+
16
+ if (htmlProps.style?.display === 'none') {
17
+ return null;
18
+ }
19
+
20
+ return createElementAs('div', htmlProps);
21
+ });
22
+
23
+ export const FloatingButtonInsertRoot = createComponentAs<FloatingButtonProps>((props) => {
24
+ const htmlProps = useFloatingButtonInsert(props);
25
+
26
+ if (htmlProps.style?.display === 'none') {
27
+ return null;
28
+ }
29
+ return createElementAs('div', htmlProps);
30
+ });
31
+
32
+ export const FloatingButton = {
33
+ EditRoot: FloatingButtonEditRoot,
34
+ InsertRoot: FloatingButtonInsertRoot,
35
+ EditButton: FloatingButtonEditButton,
36
+ UnlinkButton,
37
+ };
@@ -0,0 +1,22 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { useCallback } from 'react';
4
+ import { AsProps, HTMLPropsAs, createComponentAs, createElementAs, useEditorRef } from '@udecode/plate';
5
+ import { triggerFloatingButtonEdit } from '../../utils/triggerFloatingButtonEdit';
6
+
7
+ export const useFloatingButtonEditButton = (props: HTMLPropsAs<'button'>): HTMLPropsAs<'button'> => {
8
+ const editor = useEditorRef();
9
+
10
+ return {
11
+ onClick: useCallback(() => {
12
+ triggerFloatingButtonEdit(editor);
13
+ }, [editor]),
14
+ ...props,
15
+ };
16
+ };
17
+
18
+ export const FloatingButtonEditButton = createComponentAs<AsProps<'button'>>((props) => {
19
+ const htmlProps = useFloatingButtonEditButton(props);
20
+
21
+ return createElementAs('button', htmlProps);
22
+ });