@jpmorganchase/mosaic-content-editor-plugin 0.1.0-beta.9

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 (249) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/README.md +7 -0
  3. package/dist/api/save.d.ts +5 -0
  4. package/dist/api/save.js +1 -0
  5. package/dist/chunk-3FJGHVH5.js +1 -0
  6. package/dist/chunk-3TDCKWYF.js +1 -0
  7. package/dist/chunk-57FSKQGH.js +1 -0
  8. package/dist/chunk-5GT4QAIQ.js +1 -0
  9. package/dist/chunk-6TM3EYFS.js +1 -0
  10. package/dist/chunk-76US6CXQ.js +1 -0
  11. package/dist/chunk-A6ZSQMSC.js +1 -0
  12. package/dist/chunk-ACFFLNAA.js +1 -0
  13. package/dist/chunk-BTI67NTQ.js +1 -0
  14. package/dist/chunk-FCCNBA44.js +1 -0
  15. package/dist/chunk-FGP7KEKC.js +1 -0
  16. package/dist/chunk-FI57IKHZ.js +1 -0
  17. package/dist/chunk-FM63P66J.js +1 -0
  18. package/dist/chunk-GR3B6XH6.js +1 -0
  19. package/dist/chunk-HL7D7RIZ.js +1 -0
  20. package/dist/chunk-HROTQ6LG.js +1 -0
  21. package/dist/chunk-I3Z6UENR.js +1 -0
  22. package/dist/chunk-IO4745QD.js +1 -0
  23. package/dist/chunk-IXKHPWRK.js +1 -0
  24. package/dist/chunk-JKQCT2R7.js +1 -0
  25. package/dist/chunk-JQUDHJIQ.js +1 -0
  26. package/dist/chunk-JVJ4H6LB.js +1 -0
  27. package/dist/chunk-JX5HLHOC.js +1 -0
  28. package/dist/chunk-KMK5XOLZ.js +1 -0
  29. package/dist/chunk-KYX7ZWMX.js +1 -0
  30. package/dist/chunk-LOZ4MGDL.js +1 -0
  31. package/dist/chunk-M5W2GPOK.js +1 -0
  32. package/dist/chunk-MXR5SEUG.js +1 -0
  33. package/dist/chunk-N3NO7L4H.js +1 -0
  34. package/dist/chunk-NXV7KCC5.js +1 -0
  35. package/dist/chunk-OFDMXZXG.js +1 -0
  36. package/dist/chunk-P3NF2XWE.js +1 -0
  37. package/dist/chunk-P5OBPOC4.js +0 -0
  38. package/dist/chunk-PPEVCQGG.js +1 -0
  39. package/dist/chunk-RHRDHZ4F.js +1 -0
  40. package/dist/chunk-RN54LVW4.js +1 -0
  41. package/dist/chunk-T2M7535Y.js +1 -0
  42. package/dist/chunk-UA5IM6NC.js +1 -0
  43. package/dist/chunk-UVZXP5EW.js +1 -0
  44. package/dist/chunk-UXLYVZ46.js +1 -0
  45. package/dist/chunk-V5Z3VNEW.js +1 -0
  46. package/dist/chunk-W44IYDHL.js +1 -0
  47. package/dist/chunk-WBUBW4PH.js +2 -0
  48. package/dist/chunk-WHPZZWKO.js +1 -0
  49. package/dist/chunk-WQSFQBP7.js +1 -0
  50. package/dist/chunk-WXWEGQ2Z.js +1 -0
  51. package/dist/chunk-XX4EWSFM.js +1 -0
  52. package/dist/chunk-YPBKG7ME.js +1 -0
  53. package/dist/chunk-ZCWG7UN7.js +1 -0
  54. package/dist/chunk-ZF6MK5QB.js +1 -0
  55. package/dist/chunk-ZK6NLAGG.js +1 -0
  56. package/dist/components/ActionMenu/ActionMenu.css.css +1 -0
  57. package/dist/components/ActionMenu/ActionMenu.css.d.ts +4 -0
  58. package/dist/components/ActionMenu/ActionMenu.css.js +1 -0
  59. package/dist/components/ActionMenu/ActionMenu.d.ts +10 -0
  60. package/dist/components/ActionMenu/ActionMenu.js +1 -0
  61. package/dist/components/ContentEditor.d.ts +3 -0
  62. package/dist/components/ContentEditor.js +1 -0
  63. package/dist/components/Editor.css +1 -0
  64. package/dist/components/Editor.css.css +1 -0
  65. package/dist/components/Editor.css.d.ts +10 -0
  66. package/dist/components/Editor.css.js +1 -0
  67. package/dist/components/Editor.d.ts +14 -0
  68. package/dist/components/Editor.js +1 -0
  69. package/dist/components/EditorControls.css +1 -0
  70. package/dist/components/EditorControls.css.css +1 -0
  71. package/dist/components/EditorControls.css.d.ts +4 -0
  72. package/dist/components/EditorControls.css.js +1 -0
  73. package/dist/components/EditorControls.d.ts +4 -0
  74. package/dist/components/EditorControls.js +1 -0
  75. package/dist/components/LinkEditor/EditLinkButton.d.ts +3 -0
  76. package/dist/components/LinkEditor/EditLinkButton.js +1 -0
  77. package/dist/components/LinkEditor/LinkEditor.css +1 -0
  78. package/dist/components/LinkEditor/LinkEditor.css.css +1 -0
  79. package/dist/components/LinkEditor/LinkEditor.css.d.ts +4 -0
  80. package/dist/components/LinkEditor/LinkEditor.css.js +1 -0
  81. package/dist/components/LinkEditor/LinkEditor.d.ts +2 -0
  82. package/dist/components/LinkEditor/LinkEditor.js +1 -0
  83. package/dist/components/LinkEditor/SaveAdornment.d.ts +7 -0
  84. package/dist/components/LinkEditor/SaveAdornment.js +1 -0
  85. package/dist/components/PersistEditDialog/PersistStatus.css +1 -0
  86. package/dist/components/PersistEditDialog/PersistStatus.css.css +1 -0
  87. package/dist/components/PersistEditDialog/PersistStatus.css.d.ts +7 -0
  88. package/dist/components/PersistEditDialog/PersistStatus.css.js +1 -0
  89. package/dist/components/PersistEditDialog/PersistStatus.d.ts +7 -0
  90. package/dist/components/PersistEditDialog/PersistStatus.js +1 -0
  91. package/dist/components/PersistEditDialog/index.css +1 -0
  92. package/dist/components/PersistEditDialog/index.d.ts +4 -0
  93. package/dist/components/PersistEditDialog/index.js +1 -0
  94. package/dist/components/Popper/Popper.css +1 -0
  95. package/dist/components/Popper/Popper.css.css +1 -0
  96. package/dist/components/Popper/Popper.css.d.ts +5 -0
  97. package/dist/components/Popper/Popper.css.js +1 -0
  98. package/dist/components/Popper/Popper.d.ts +11 -0
  99. package/dist/components/Popper/Popper.js +1 -0
  100. package/dist/components/SaveButton.d.ts +2 -0
  101. package/dist/components/SaveButton.js +1 -0
  102. package/dist/components/ScrollableSection/ScrollableSection.css +1 -0
  103. package/dist/components/ScrollableSection/ScrollableSection.css.css +1 -0
  104. package/dist/components/ScrollableSection/ScrollableSection.css.d.ts +4 -0
  105. package/dist/components/ScrollableSection/ScrollableSection.css.js +1 -0
  106. package/dist/components/ScrollableSection/ScrollableSection.d.ts +7 -0
  107. package/dist/components/ScrollableSection/ScrollableSection.js +1 -0
  108. package/dist/components/StatusBanner.d.ts +3 -0
  109. package/dist/components/StatusBanner.js +1 -0
  110. package/dist/components/Toolbar/InsertBlockDropdown.d.ts +6 -0
  111. package/dist/components/Toolbar/InsertBlockDropdown.js +1 -0
  112. package/dist/components/Toolbar/InsertHorizontalRule.d.ts +2 -0
  113. package/dist/components/Toolbar/InsertHorizontalRule.js +1 -0
  114. package/dist/components/Toolbar/InsertImage.css +1 -0
  115. package/dist/components/Toolbar/InsertImage.css.css +1 -0
  116. package/dist/components/Toolbar/InsertImage.css.d.ts +4 -0
  117. package/dist/components/Toolbar/InsertImage.css.js +1 -0
  118. package/dist/components/Toolbar/InsertImage.d.ts +2 -0
  119. package/dist/components/Toolbar/InsertImage.js +1 -0
  120. package/dist/components/Toolbar/InsertLink.css +1 -0
  121. package/dist/components/Toolbar/InsertLink.css.css +1 -0
  122. package/dist/components/Toolbar/InsertLink.css.d.ts +4 -0
  123. package/dist/components/Toolbar/InsertLink.css.js +1 -0
  124. package/dist/components/Toolbar/InsertLink.d.ts +3 -0
  125. package/dist/components/Toolbar/InsertLink.js +1 -0
  126. package/dist/components/Toolbar/InsertTable.css +1 -0
  127. package/dist/components/Toolbar/InsertTable.css.css +1 -0
  128. package/dist/components/Toolbar/InsertTable.css.d.ts +13 -0
  129. package/dist/components/Toolbar/InsertTable.css.js +1 -0
  130. package/dist/components/Toolbar/InsertTable.d.ts +10 -0
  131. package/dist/components/Toolbar/InsertTable.js +1 -0
  132. package/dist/components/Toolbar/TextFormatToolbarButton.css +1 -0
  133. package/dist/components/Toolbar/TextFormatToolbarButton.css.css +1 -0
  134. package/dist/components/Toolbar/TextFormatToolbarButton.css.d.ts +5 -0
  135. package/dist/components/Toolbar/TextFormatToolbarButton.css.js +1 -0
  136. package/dist/components/Toolbar/TextFormatToolbarButton.d.ts +7 -0
  137. package/dist/components/Toolbar/TextFormatToolbarButton.js +1 -0
  138. package/dist/components/Toolbar/TextFormatTooltray.css +1 -0
  139. package/dist/components/Toolbar/TextFormatTooltray.css.css +1 -0
  140. package/dist/components/Toolbar/TextFormatTooltray.css.d.ts +8 -0
  141. package/dist/components/Toolbar/TextFormatTooltray.css.js +1 -0
  142. package/dist/components/Toolbar/TextFormatTooltray.d.ts +4 -0
  143. package/dist/components/Toolbar/TextFormatTooltray.js +1 -0
  144. package/dist/components/Toolbar/Toolbar.css +1 -0
  145. package/dist/components/Toolbar/Toolbar.d.ts +3 -0
  146. package/dist/components/Toolbar/Toolbar.js +1 -0
  147. package/dist/hooks/useEditHistory.d.ts +6 -0
  148. package/dist/hooks/useEditHistory.js +1 -0
  149. package/dist/index.css +1 -0
  150. package/dist/index.d.ts +4 -0
  151. package/dist/index.js +1 -0
  152. package/dist/nodes/index.d.ts +8 -0
  153. package/dist/nodes/index.js +1 -0
  154. package/dist/plugins/FloatingToolbarPlugin.css +1 -0
  155. package/dist/plugins/FloatingToolbarPlugin.d.ts +2 -0
  156. package/dist/plugins/FloatingToolbarPlugin.js +1 -0
  157. package/dist/plugins/HorizontalRulePlugin.d.ts +1 -0
  158. package/dist/plugins/HorizontalRulePlugin.js +1 -0
  159. package/dist/plugins/MarkdownImagePlugin.d.ts +7 -0
  160. package/dist/plugins/MarkdownImagePlugin.js +1 -0
  161. package/dist/plugins/MarkdownLinkPlugin.css +1 -0
  162. package/dist/plugins/MarkdownLinkPlugin.d.ts +8 -0
  163. package/dist/plugins/MarkdownLinkPlugin.js +1 -0
  164. package/dist/plugins/PreviewPlugin.css +1 -0
  165. package/dist/plugins/PreviewPlugin.d.ts +5 -0
  166. package/dist/plugins/PreviewPlugin.js +1 -0
  167. package/dist/plugins/TableActionMenuPlugin.css +1 -0
  168. package/dist/plugins/TableActionMenuPlugin.css.css +1 -0
  169. package/dist/plugins/TableActionMenuPlugin.css.d.ts +60 -0
  170. package/dist/plugins/TableActionMenuPlugin.css.js +1 -0
  171. package/dist/plugins/TableActionMenuPlugin.d.ts +2 -0
  172. package/dist/plugins/TableActionMenuPlugin.js +1 -0
  173. package/dist/store/index.d.ts +38 -0
  174. package/dist/store/index.js +1 -0
  175. package/dist/theme/index.css +1 -0
  176. package/dist/theme/index.css.css +1 -0
  177. package/dist/theme/index.css.d.ts +15 -0
  178. package/dist/theme/index.css.js +1 -0
  179. package/dist/theme/index.d.ts +3 -0
  180. package/dist/theme/index.js +1 -0
  181. package/dist/transformers/horizontalRule.d.ts +2 -0
  182. package/dist/transformers/horizontalRule.js +1 -0
  183. package/dist/transformers/index.d.ts +2 -0
  184. package/dist/transformers/index.js +1 -0
  185. package/dist/transformers/link.d.ts +2 -0
  186. package/dist/transformers/link.js +1 -0
  187. package/dist/transformers/tableRule.d.ts +2 -0
  188. package/dist/transformers/tableRule.js +1 -0
  189. package/dist/utils/getSelectedNode.d.ts +2 -0
  190. package/dist/utils/getSelectedNode.js +1 -0
  191. package/package.json +54 -0
  192. package/src/api/save.ts +12 -0
  193. package/src/components/ActionMenu/ActionMenu.css.ts +9 -0
  194. package/src/components/ActionMenu/ActionMenu.tsx +28 -0
  195. package/src/components/ContentEditor.tsx +12 -0
  196. package/src/components/Editor.css.ts +64 -0
  197. package/src/components/Editor.tsx +143 -0
  198. package/src/components/EditorControls.css.ts +9 -0
  199. package/src/components/EditorControls.tsx +31 -0
  200. package/src/components/LinkEditor/EditLinkButton.tsx +9 -0
  201. package/src/components/LinkEditor/LinkEditor.css.ts +15 -0
  202. package/src/components/LinkEditor/LinkEditor.tsx +136 -0
  203. package/src/components/LinkEditor/SaveAdornment.tsx +16 -0
  204. package/src/components/PersistEditDialog/PersistStatus.css.ts +21 -0
  205. package/src/components/PersistEditDialog/PersistStatus.tsx +78 -0
  206. package/src/components/PersistEditDialog/__tests__/handlers.ts +9 -0
  207. package/src/components/PersistEditDialog/__tests__/index.test.tsx +93 -0
  208. package/src/components/PersistEditDialog/index.tsx +114 -0
  209. package/src/components/Popper/Popper.css.ts +12 -0
  210. package/src/components/Popper/Popper.tsx +31 -0
  211. package/src/components/SaveButton.tsx +23 -0
  212. package/src/components/ScrollableSection/ScrollableSection.css.ts +10 -0
  213. package/src/components/ScrollableSection/ScrollableSection.tsx +20 -0
  214. package/src/components/StatusBanner.tsx +16 -0
  215. package/src/components/Toolbar/InsertBlockDropdown.tsx +134 -0
  216. package/src/components/Toolbar/InsertHorizontalRule.tsx +18 -0
  217. package/src/components/Toolbar/InsertImage.css.ts +7 -0
  218. package/src/components/Toolbar/InsertImage.tsx +132 -0
  219. package/src/components/Toolbar/InsertLink.css.ts +7 -0
  220. package/src/components/Toolbar/InsertLink.tsx +145 -0
  221. package/src/components/Toolbar/InsertTable.css.ts +46 -0
  222. package/src/components/Toolbar/InsertTable.tsx +190 -0
  223. package/src/components/Toolbar/TextFormatToolbarButton.css.ts +10 -0
  224. package/src/components/Toolbar/TextFormatToolbarButton.tsx +29 -0
  225. package/src/components/Toolbar/TextFormatTooltray.css.ts +27 -0
  226. package/src/components/Toolbar/TextFormatTooltray.tsx +118 -0
  227. package/src/components/Toolbar/Toolbar.tsx +47 -0
  228. package/src/components/__tests__/EditorControls.test.tsx +69 -0
  229. package/src/components/__tests__/StatusBanner.test.tsx +33 -0
  230. package/src/hooks/useEditHistory.ts +49 -0
  231. package/src/index.ts +5 -0
  232. package/src/nodes/index.ts +23 -0
  233. package/src/plugins/FloatingToolbarPlugin.tsx +100 -0
  234. package/src/plugins/HorizontalRulePlugin.tsx +39 -0
  235. package/src/plugins/MarkdownImagePlugin.tsx +58 -0
  236. package/src/plugins/MarkdownLinkPlugin.tsx +69 -0
  237. package/src/plugins/PreviewPlugin.tsx +46 -0
  238. package/src/plugins/TableActionMenuPlugin.css.ts +29 -0
  239. package/src/plugins/TableActionMenuPlugin.tsx +278 -0
  240. package/src/store/__tests__/index.test.ts +171 -0
  241. package/src/store/index.ts +84 -0
  242. package/src/theme/index.css.ts +39 -0
  243. package/src/theme/index.ts +22 -0
  244. package/src/transformers/horizontalRule.ts +25 -0
  245. package/src/transformers/index.ts +6 -0
  246. package/src/transformers/link.ts +3 -0
  247. package/src/transformers/tableRule.ts +193 -0
  248. package/src/utils/getSelectedNode.ts +16 -0
  249. package/tsconfig.json +8 -0
@@ -0,0 +1,9 @@
1
+ import { style } from '@vanilla-extract/css';
2
+
3
+ import { POPPER_Z_INDEX } from '../Popper/Popper.css';
4
+
5
+ export default {
6
+ menu: style({
7
+ zIndex: POPPER_Z_INDEX + 1
8
+ })
9
+ };
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import { MenuButton, MenuDescriptor } from '@jpmorganchase/uitk-lab';
3
+ import { Icon } from '@jpmorganchase/mosaic-components';
4
+
5
+ import type { CascadingMenuProps } from '@jpmorganchase/uitk-lab';
6
+
7
+ import styles from './ActionMenu.css';
8
+
9
+ export type ActionMenuSource = MenuDescriptor;
10
+
11
+ interface ActionMenuProps {
12
+ initialSource: CascadingMenuProps['initialSource'];
13
+ onItemClick: CascadingMenuProps['onItemClick'];
14
+ }
15
+
16
+ export function ActionMenu({ initialSource, onItemClick }: ActionMenuProps) {
17
+ return (
18
+ <MenuButton
19
+ hideCaret
20
+ CascadingMenuProps={{
21
+ initialSource,
22
+ onItemClick
23
+ }}
24
+ >
25
+ <Icon name="menu" />
26
+ </MenuButton>
27
+ );
28
+ }
@@ -0,0 +1,12 @@
1
+ import * as React from 'react';
2
+ import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
3
+ import { ContentEditable } from '@lexical/react/LexicalContentEditable';
4
+
5
+ const ContentEditor = () => (
6
+ <RichTextPlugin
7
+ contentEditable={<ContentEditable />}
8
+ placeholder={<div>Enter some text...</div>}
9
+ />
10
+ );
11
+
12
+ export default ContentEditor;
@@ -0,0 +1,64 @@
1
+ import { globalStyle, style } from '@vanilla-extract/css';
2
+ import {
3
+ vars,
4
+ backgroundColor,
5
+ neutralBorder,
6
+ navigableBorder,
7
+ config
8
+ } from '@jpmorganchase/mosaic-theme';
9
+
10
+ globalStyle('div[contenteditable]', {
11
+ outline: 'none'
12
+ });
13
+
14
+ export default {
15
+ root: style({
16
+ display: 'grid',
17
+ gridTemplateColumns: '1fr',
18
+ gridTemplateRows: 'auto 1fr',
19
+ gridTemplateAreas: '"toolbar" "editor" '
20
+ }),
21
+ toolbarContainer: style([
22
+ style({
23
+ gridArea: 'toolbar',
24
+ position: 'sticky',
25
+ top: `${config.appHeader.height}px`,
26
+ zIndex: 2
27
+ }),
28
+ neutralBorder({
29
+ variant: 'low',
30
+ borderWidth: 'thin'
31
+ })
32
+ ]),
33
+ editorRoot: style({
34
+ display: 'flex',
35
+ marginTop: vars.space.vertical.x3,
36
+ flexWrap: 'wrap',
37
+ gridArea: 'editor'
38
+ }),
39
+ splitter: style({
40
+ display: 'flex',
41
+ flexDirection: 'row'
42
+ }),
43
+ gutter: style([
44
+ {
45
+ backgroundRepeat: 'no-repeat',
46
+ backgroundPosition: '50%',
47
+ backgroundImage:
48
+ 'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==")',
49
+ cursor: 'col-resize'
50
+ },
51
+ backgroundColor({ variant: 'emphasis' })
52
+ ]),
53
+ unfocused: neutralBorder({
54
+ variant: 'low',
55
+ borderTopWidth: 'thin',
56
+ borderLeftWidth: 'thin',
57
+ borderRightWidth: 'thin',
58
+ borderBottomWidth: 'none'
59
+ }),
60
+ focused: navigableBorder({
61
+ variant: 'focusRing',
62
+ borderWidth: 'medium'
63
+ })
64
+ };
@@ -0,0 +1,143 @@
1
+ import React, { ComponentType, FC, useRef, useState } from 'react';
2
+ import classnames from 'classnames';
3
+ import matter from 'gray-matter';
4
+ import { LexicalComposer } from '@lexical/react/LexicalComposer';
5
+ import { ListPlugin } from '@lexical/react/LexicalListPlugin';
6
+ import { TablePlugin } from '@lexical/react/LexicalTablePlugin';
7
+ import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin';
8
+ import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
9
+ import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin';
10
+ import Split from 'react-split';
11
+ import { $convertFromMarkdownString } from '@lexical/markdown';
12
+
13
+ import transformers from '../transformers';
14
+ import ContentEditor from './ContentEditor';
15
+ import { nodes } from '../nodes';
16
+ import { useEditorUser, usePreviewContent } from '../store';
17
+ import { PreviewPlugin } from '../plugins/PreviewPlugin';
18
+ import styles from './Editor.css';
19
+ import Toolbar from './Toolbar/Toolbar';
20
+ import theme from '../theme';
21
+ import { PersistDialog } from './PersistEditDialog';
22
+ import StatusBanner from './StatusBanner';
23
+ import { MarkdownImagePlugin } from '../plugins/MarkdownImagePlugin';
24
+ import { MarkdownLinkPlugin } from '../plugins/MarkdownLinkPlugin';
25
+ import { LinkEditor } from './LinkEditor/LinkEditor';
26
+ import { ScrollableSection } from './ScrollableSection/ScrollableSection';
27
+ import HorizontalRulePlugin from '../plugins/HorizontalRulePlugin';
28
+ import { FloatingToolbarPlugin } from '../plugins/FloatingToolbarPlugin';
29
+ import { TableActionMenuPlugin } from '../plugins/TableActionMenuPlugin';
30
+ import { InsertLinkDialog } from './Toolbar/InsertLink';
31
+ import type { PreviewPluginProps } from '../plugins/PreviewPlugin';
32
+
33
+ function onError(error: Error) {
34
+ console.error(error);
35
+ }
36
+
37
+ const initialConfig = {
38
+ namespace: 'mosaic-content-editor',
39
+ onError,
40
+ nodes,
41
+ theme
42
+ };
43
+
44
+ interface PreviewComponentProps {
45
+ source: any;
46
+ meta: any;
47
+ components: any;
48
+ }
49
+
50
+ interface EditorProps extends PreviewComponentProps, PreviewPluginProps {
51
+ content: string;
52
+ PreviewComponent: ComponentType<PreviewComponentProps>;
53
+ user?: any;
54
+ }
55
+
56
+ const gutter = () => {
57
+ const gutter = document.createElement('div');
58
+ gutter.className = styles.gutter;
59
+ return gutter;
60
+ };
61
+
62
+ const Editor: FC<EditorProps> = ({
63
+ compileMDX,
64
+ content,
65
+ source,
66
+ PreviewComponent,
67
+ user,
68
+ components
69
+ }) => {
70
+ const previewContent = usePreviewContent() || source;
71
+ const { setUser } = useEditorUser();
72
+ const [focused, setFocused] = useState(false);
73
+ const containerRef = useRef<HTMLDivElement | null>(null);
74
+ const { data: meta, content: markdown } = matter(content);
75
+
76
+ const handleEditorFocus = () => {
77
+ setFocused(true);
78
+ setUser(user);
79
+ };
80
+
81
+ const handleEditorBlur = () => {
82
+ setFocused(false);
83
+ };
84
+
85
+ return (
86
+ <LexicalComposer
87
+ initialConfig={{
88
+ ...initialConfig,
89
+ editorState: () => $convertFromMarkdownString(markdown, transformers)
90
+ }}
91
+ >
92
+ <div className={styles.root} onFocus={handleEditorFocus} onBlur={handleEditorBlur}>
93
+ <div className={styles.toolbarContainer}>
94
+ <Toolbar />
95
+ <PersistDialog meta={meta} />
96
+ <StatusBanner />
97
+ </div>
98
+ <div className={styles.editorRoot}>
99
+ <Split
100
+ className={styles.splitter}
101
+ cursor="col-resize"
102
+ direction="horizontal"
103
+ dragInterval={1}
104
+ expandToMin={false}
105
+ gutter={gutter}
106
+ gutterAlign="center"
107
+ minSize={100}
108
+ sizes={[50, 50]}
109
+ snapOffset={30}
110
+ >
111
+ <ScrollableSection
112
+ className={classnames({
113
+ [styles.focused]: focused,
114
+ [styles.unfocused]: !focused
115
+ })}
116
+ ref={containerRef}
117
+ >
118
+ <ContentEditor />
119
+ </ScrollableSection>
120
+ <ScrollableSection>
121
+ <PreviewComponent source={previewContent} meta={meta} components={components} />
122
+ </ScrollableSection>
123
+ </Split>
124
+ <HistoryPlugin />
125
+ <ListPlugin />
126
+ <TablePlugin />
127
+ <LinkPlugin />
128
+ <MarkdownShortcutPlugin transformers={transformers} />
129
+ <PreviewPlugin compileMDX={compileMDX} />
130
+ <MarkdownImagePlugin />
131
+ <MarkdownLinkPlugin />
132
+ <LinkEditor />
133
+ <HorizontalRulePlugin />
134
+ <FloatingToolbarPlugin />
135
+ <TableActionMenuPlugin />
136
+ <InsertLinkDialog />
137
+ </div>
138
+ </div>
139
+ </LexicalComposer>
140
+ );
141
+ };
142
+
143
+ export default Editor;
@@ -0,0 +1,9 @@
1
+ import { style } from '@vanilla-extract/css';
2
+ import { vars } from '@jpmorganchase/mosaic-theme';
3
+
4
+ export default {
5
+ root: style({
6
+ backgroundColor: 'inherit',
7
+ width: vars.space.none
8
+ })
9
+ };
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { Icon } from '@jpmorganchase/mosaic-components';
3
+ import { Toolbar as ToolkitToolbar, Tooltray, ToolbarButton } from '@jpmorganchase/uitk-lab';
4
+
5
+ import { default as useContentEditor } from '../store';
6
+ import styles from './EditorControls.css';
7
+
8
+ export const EditorControls = ({ isLoggedIn = false }) => {
9
+ const { pageState, startEditing, stopEditing } = useContentEditor();
10
+ const isEditing = pageState === 'EDIT';
11
+
12
+ const handleClick = () => (!isEditing ? startEditing() : stopEditing());
13
+
14
+ const loggedInLabel = isEditing ? 'Cancel Editing' : 'Edit Page';
15
+ const overflowLabel = !isLoggedIn ? 'Login Required to Edit' : loggedInLabel;
16
+
17
+ return (
18
+ <ToolkitToolbar aria-label="editor-controls" className={styles.root}>
19
+ <Tooltray aria-label="page editor controls tooltray">
20
+ <ToolbarButton
21
+ aria-label={isEditing ? 'cancel editing' : 'start editing'}
22
+ onClick={handleClick}
23
+ disabled={!isLoggedIn}
24
+ label={overflowLabel}
25
+ >
26
+ <Icon name={isEditing ? 'delete' : 'edit'} />
27
+ </ToolbarButton>
28
+ </Tooltray>
29
+ </ToolkitToolbar>
30
+ );
31
+ };
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import { Button, Icon } from '@jpmorganchase/mosaic-components';
3
+ import type { ButtonProps } from '@jpmorganchase/mosaic-components';
4
+
5
+ export const EditLinkButton = (props: ButtonProps) => (
6
+ <Button {...props}>
7
+ <Icon name="edit" />
8
+ </Button>
9
+ );
@@ -0,0 +1,15 @@
1
+ import { style } from '@vanilla-extract/css';
2
+ import { vars, neutralBorder } from '@jpmorganchase/mosaic-theme';
3
+
4
+ export default {
5
+ popper: style([
6
+ style({
7
+ padding: vars.space.vertical.x1,
8
+ marginLeft: vars.space.horizontal.x2
9
+ }),
10
+ neutralBorder({
11
+ variant: 'low',
12
+ borderWidth: 'thin'
13
+ })
14
+ ])
15
+ };
@@ -0,0 +1,136 @@
1
+ import { mergeRegister } from '@lexical/utils';
2
+ import {
3
+ $getSelection,
4
+ $isRangeSelection,
5
+ COMMAND_PRIORITY_LOW,
6
+ RangeSelection,
7
+ SELECTION_CHANGE_COMMAND
8
+ } from 'lexical';
9
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
10
+ import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
11
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
12
+ import { Input, useFloatingUI } from '@jpmorganchase/uitk-core';
13
+ import { useDismiss, useInteractions } from '@floating-ui/react-dom-interactions';
14
+
15
+ import styles from './LinkEditor.css';
16
+ import { SaveAdornment } from './SaveAdornment';
17
+ import { EditLinkButton } from './EditLinkButton';
18
+ import { Popper } from '../Popper/Popper';
19
+ import { getSelectedNode } from '../../utils/getSelectedNode';
20
+
21
+ export function LinkEditor() {
22
+ const [editor] = useLexicalComposerContext();
23
+ const inputRef = useRef<HTMLInputElement>(null);
24
+ const [linkUrl, setLinkUrl] = useState('');
25
+ const [isEdit, setIsEdit] = useState(false);
26
+ const [lastSelection, setLastSelection] = useState<RangeSelection | null>(null);
27
+
28
+ const reset = () => {
29
+ setLinkUrl('');
30
+ setIsEdit(false);
31
+ setLastSelection(null);
32
+ };
33
+
34
+ const { context, floating, reference, strategy, x, y } = useFloatingUI({
35
+ placement: 'right',
36
+ strategy: 'absolute',
37
+ open: linkUrl.length > 0,
38
+ onOpenChange: reset
39
+ });
40
+ const { getFloatingProps } = useInteractions([
41
+ useDismiss(context, {
42
+ ancestorScroll: true
43
+ })
44
+ ]);
45
+
46
+ const updateLinkEditor = useCallback(() => {
47
+ const selection = $getSelection();
48
+ if ($isRangeSelection(selection)) {
49
+ const node = getSelectedNode(selection);
50
+ const parent = node.getParent();
51
+ if ($isLinkNode(parent)) {
52
+ reference(editor.getElementByKey(parent.getKey()));
53
+ setLinkUrl(parent.getURL());
54
+ setLastSelection(selection);
55
+ } else if ($isLinkNode(node)) {
56
+ reference(editor.getElementByKey(node.getKey()));
57
+ setLinkUrl(node.getURL());
58
+ setLastSelection(selection);
59
+ } else {
60
+ // no links to edit on this selection
61
+ reset();
62
+ }
63
+ }
64
+ setIsEdit(false);
65
+ return true;
66
+ }, [editor, reference]);
67
+
68
+ useEffect(
69
+ () =>
70
+ mergeRegister(
71
+ editor.registerUpdateListener(({ editorState }) => {
72
+ editorState.read(() => {
73
+ updateLinkEditor();
74
+ });
75
+ }),
76
+
77
+ editor.registerCommand(
78
+ SELECTION_CHANGE_COMMAND,
79
+ () => {
80
+ updateLinkEditor();
81
+ return true;
82
+ },
83
+ COMMAND_PRIORITY_LOW
84
+ )
85
+ ),
86
+ [editor, updateLinkEditor]
87
+ );
88
+
89
+ useEffect(() => {
90
+ editor.getEditorState().read(() => {
91
+ updateLinkEditor();
92
+ });
93
+ }, [editor, updateLinkEditor]);
94
+
95
+ const handleSave = () => {
96
+ if (lastSelection !== null) {
97
+ if (linkUrl !== '') {
98
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, linkUrl);
99
+ setIsEdit(false);
100
+ }
101
+ }
102
+ };
103
+
104
+ const handleEdit = () => setIsEdit(true);
105
+
106
+ return (
107
+ <Popper
108
+ className={styles.popper}
109
+ ref={floating}
110
+ open={linkUrl.length > 0}
111
+ style={{ top: y ?? '', left: x ?? '', position: strategy }}
112
+ {...getFloatingProps({})}
113
+ >
114
+ {!isEdit ? (
115
+ <EditLinkButton onClick={handleEdit} />
116
+ ) : (
117
+ <Input
118
+ endAdornment={<SaveAdornment onSave={handleSave} />}
119
+ ref={inputRef}
120
+ value={linkUrl}
121
+ onChange={event => {
122
+ setLinkUrl(event.target.value);
123
+ }}
124
+ onKeyDown={event => {
125
+ if (event.key === 'Enter') {
126
+ event.preventDefault();
127
+ handleSave();
128
+ } else if (event.key === 'Escape') {
129
+ event.preventDefault();
130
+ }
131
+ }}
132
+ />
133
+ )}
134
+ </Popper>
135
+ );
136
+ }
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { Button, Icon } from '@jpmorganchase/mosaic-components';
3
+ import type { ButtonProps } from '@jpmorganchase/mosaic-components';
4
+ import { StaticInputAdornment } from '@jpmorganchase/uitk-core';
5
+
6
+ interface SaveAdornmentProps {
7
+ onSave: ButtonProps['onClick'];
8
+ }
9
+
10
+ export const SaveAdornment = ({ onSave }: SaveAdornmentProps) => (
11
+ <StaticInputAdornment>
12
+ <Button onClick={onSave}>
13
+ <Icon name="save" />
14
+ </Button>
15
+ </StaticInputAdornment>
16
+ );
@@ -0,0 +1,21 @@
1
+ import { style } from '@vanilla-extract/css';
2
+ import { vars } from '@jpmorganchase/mosaic-theme';
3
+
4
+ export default {
5
+ step: style({
6
+ padding: vars.space.vertical.x2,
7
+ opacity: vars.opacity.light.disabled
8
+ }),
9
+ stepActive: style({
10
+ fontSize: vars.fontSize.s80,
11
+ fontStyle: vars.fontWeight.semibold,
12
+ opacity: 1
13
+ }),
14
+ stepComplete: style({
15
+ opacity: 1
16
+ }),
17
+ stepText: style({
18
+ marginLeft: vars.space.horizontal.x2,
19
+ marginRight: vars.space.horizontal.x2
20
+ })
21
+ };
@@ -0,0 +1,78 @@
1
+ import React, { FC, useEffect, useState } from 'react';
2
+ import classnames from 'classnames';
3
+ import { P2 } from '@jpmorganchase/mosaic-components';
4
+
5
+ import styles from './PersistStatus.css';
6
+
7
+ const steps = ['Making a copy...', 'Applying changes...', 'Creating a Pull Request...'];
8
+ const lastStep = steps.length - 1;
9
+
10
+ interface StepProps {
11
+ isComplete: boolean;
12
+ isCurrent: boolean;
13
+ }
14
+
15
+ const Step: FC<StepProps> = ({ children, isComplete, isCurrent }) => (
16
+ <div
17
+ className={classnames(styles.step, {
18
+ [styles.stepComplete]: isComplete,
19
+ [styles.stepActive]: isCurrent
20
+ })}
21
+ >
22
+ <span role="img" aria-label="emoji with party hat">
23
+ {isComplete ? '🥳' : null}
24
+ </span>
25
+ <span role="img" aria-label="person grimacing emoji ">
26
+ {isCurrent ? '😃' : null}
27
+ </span>
28
+ <span role="img" aria-label="sleepy emoji">
29
+ {!isCurrent && !isComplete ? '💤' : null}
30
+ </span>
31
+ <span className={styles.stepText}>{children}</span>
32
+ <span role="img" aria-label="hourglass">
33
+ {isCurrent ? '⌛' : null}
34
+ </span>
35
+ </div>
36
+ );
37
+
38
+ interface StatusProps {
39
+ isRaising: boolean;
40
+ error: string | null;
41
+ }
42
+
43
+ export const PersistStatus = ({ isRaising, error }: StatusProps) => {
44
+ const [currentStep, setCurrentStep] = useState(0);
45
+
46
+ useEffect(() => {
47
+ const stepInterval = setInterval(() => {
48
+ if (currentStep === steps.length - 1) {
49
+ clearInterval(stepInterval);
50
+ } else {
51
+ setCurrentStep(prevState => prevState + 1);
52
+ }
53
+ }, 2000);
54
+ return function cleanUpStepInterval() {
55
+ clearInterval(stepInterval);
56
+ };
57
+ }, [currentStep]);
58
+
59
+ if (error) {
60
+ return <P2>😢 Sorry something has went wrong. Please try again later.</P2>;
61
+ }
62
+
63
+ return (
64
+ <>
65
+ {steps.map((step, index) => (
66
+ <Step
67
+ key={`step -${index}`}
68
+ isComplete={
69
+ currentStep === lastStep && index === lastStep ? !isRaising : currentStep > index
70
+ }
71
+ isCurrent={currentStep === index}
72
+ >
73
+ {step}
74
+ </Step>
75
+ ))}
76
+ </>
77
+ );
78
+ };
@@ -0,0 +1,9 @@
1
+ import { rest } from 'msw';
2
+
3
+ export const postSaveContentSuccessHandler = rest.post('/api/content/save', (req, res, ctx) =>
4
+ res(ctx.status(500), ctx.json({ links: { self: [{ href: 'http://pull-request-url' }] } }))
5
+ );
6
+
7
+ export const postSaveContentFailureHandler = rest.post('/api/content/save', (req, res, ctx) =>
8
+ res(ctx.status(500))
9
+ );
@@ -0,0 +1,93 @@
1
+ import React from 'react';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { setupServer } from 'msw/node';
5
+ import { LexicalComposer } from '@lexical/react/LexicalComposer';
6
+
7
+ import { PersistDialog } from '../index';
8
+ import { usePageState } from '../../../store';
9
+ import { nodes } from '../../../nodes';
10
+ import { postSaveContentSuccessHandler } from './handlers';
11
+
12
+ jest.mock('../../../store', () => ({
13
+ ...jest.requireActual('../../../store'),
14
+ __esModule: true,
15
+ usePageState: jest.fn()
16
+ }));
17
+
18
+ const setPageStateSpy = jest.fn();
19
+ const setErrorMessageSpy = jest.fn();
20
+ const mockMeta = { sourceId: 'source-1', route: 'route' };
21
+
22
+ jest.mock('@lexical/markdown', () => ({
23
+ ...jest.requireActual('@lexical/markdown'),
24
+ $convertToMarkdownString: jest.fn().mockReturnValue('## Test Markdown')
25
+ }));
26
+
27
+ const server = setupServer();
28
+
29
+ const renderPersistDialog = (props = { meta: mockMeta }) => {
30
+ render(
31
+ <LexicalComposer
32
+ initialConfig={{ namespace: 'persist-dialog-test', nodes, onError: jest.fn() }}
33
+ >
34
+ <PersistDialog {...props} />
35
+ </LexicalComposer>
36
+ );
37
+ };
38
+
39
+ describe('GIVEN a PersistEditDialog', () => {
40
+ beforeAll(() => {
41
+ server.listen({ onUnhandledRequest: 'error' });
42
+
43
+ jest.mocked(usePageState).mockReturnValue({
44
+ pageState: 'SAVING',
45
+ setErrorMessage: setErrorMessageSpy,
46
+ setPageState: setPageStateSpy,
47
+ errorMessage: undefined
48
+ });
49
+ });
50
+
51
+ afterAll(() => {
52
+ server.close();
53
+ });
54
+
55
+ test('THEN the dialog is open when SAVING', () => {
56
+ // arrange
57
+ renderPersistDialog();
58
+ // assert
59
+ expect(
60
+ screen.getByText(
61
+ 'The content of this page resides in a Git repository and to update it requires a Pull Request which will be reviewed by the content owners.'
62
+ )
63
+ ).toBeDefined();
64
+ });
65
+
66
+ test('THEN cancelling returns the page to edit mode', async () => {
67
+ // arrange
68
+ renderPersistDialog();
69
+ // act
70
+ const cancelButton = screen.getByText('Cancel');
71
+ await userEvent.click(cancelButton);
72
+ // assert
73
+ expect(setPageStateSpy).toBeCalledTimes(1);
74
+ expect(setPageStateSpy.mock.calls[0][0]).toEqual('EDIT');
75
+ });
76
+
77
+ describe.skip('AND WHEN we can create a Pull Request', () => {
78
+ beforeAll(() => {
79
+ server.use(postSaveContentSuccessHandler);
80
+ });
81
+ test('THEN raising a PR provides a link to the PR', async () => {
82
+ // arrange
83
+ renderPersistDialog();
84
+ // act
85
+ const raisePrButton = screen.getByText('Raise Pull Request');
86
+ userEvent.click(raisePrButton);
87
+ // assert
88
+ await waitFor(() => {
89
+ expect(screen.getByText('A Pull Request for your changes has been created.'));
90
+ });
91
+ });
92
+ });
93
+ });