@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,46 @@
1
+ import { style } from '@vanilla-extract/css';
2
+ import {
3
+ selectableColor,
4
+ table,
5
+ neutralBorder,
6
+ vars,
7
+ backgroundColor,
8
+ foregroundColor
9
+ } from '@jpmorganchase/mosaic-theme';
10
+
11
+ export default {
12
+ tableContainer: style({
13
+ position: 'relative',
14
+ margin: vars.space.horizontal.x2
15
+ }),
16
+ table: table({ context: 'component' }),
17
+ addColumnArea: style({
18
+ position: 'absolute',
19
+ top: 0,
20
+ width: '25px'
21
+ }),
22
+ addRowArea: style({
23
+ position: 'absolute',
24
+ left: 0,
25
+ height: '25px'
26
+ }),
27
+ headerRow: selectableColor({ variant: 'inEdit' }),
28
+ row: selectableColor({ variant: 'hover' }),
29
+ cell: style([
30
+ neutralBorder({
31
+ variant: 'high',
32
+ borderWidth: 'thin'
33
+ })
34
+ ]),
35
+ dimensions: style([
36
+ style({
37
+ display: 'flex',
38
+ justifyContent: 'center',
39
+ alignItems: 'center',
40
+ paddingTop: vars.space.vertical.x2
41
+ }),
42
+ foregroundColor({ variant: 'high' })
43
+ ]),
44
+ popper: style([style({ zIndex: 1600 }), backgroundColor({ variant: 'regular' })]),
45
+ spacer: style({ marginLeft: vars.space.horizontal.x1 })
46
+ };
@@ -0,0 +1,190 @@
1
+ import React, { ReactElement, FC, MouseEventHandler, useRef, useState } from 'react';
2
+ import { Icon } from '@jpmorganchase/mosaic-components';
3
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
4
+ import { INSERT_TABLE_COMMAND } from '@lexical/table';
5
+
6
+ import { useFloatingUI } from '@jpmorganchase/uitk-core';
7
+ import { useDismiss, useInteractions } from '@floating-ui/react-dom-interactions';
8
+ import { Popper } from '../Popper/Popper';
9
+ import { TextFormatToolbarButton } from './TextFormatToolbarButton';
10
+ import styles from './InsertTable.css';
11
+
12
+ interface IndexableEntity {
13
+ index: number;
14
+ }
15
+
16
+ const Cell: FC<IndexableEntity> = ({ index, ...rest }) => (
17
+ <td className={styles.cell} data-column={index} {...rest} />
18
+ );
19
+
20
+ const Row: FC<IndexableEntity> = ({ children, index }) => (
21
+ <tr className={index === 1 ? styles.headerRow : styles.row} data-row={index}>
22
+ {children}
23
+ </tr>
24
+ );
25
+
26
+ interface TableBodyProps {
27
+ rowCount: number;
28
+ columnCount: number;
29
+ }
30
+
31
+ const TableBody: FC<TableBodyProps> = ({ rowCount, columnCount }) => {
32
+ const cells: ReactElement[] = [];
33
+ const rows: ReactElement[] = [];
34
+ for (let i = 1; i <= columnCount; i++) {
35
+ cells.push(<Cell key={i} index={i} />);
36
+ }
37
+
38
+ for (let i = 1; i <= rowCount; i++) {
39
+ rows.push(
40
+ <Row key={i} index={i}>
41
+ {cells}
42
+ </Row>
43
+ );
44
+ }
45
+ return <tbody>{rows}</tbody>;
46
+ };
47
+
48
+ function getPosition(node: HTMLElement) {
49
+ const columnIndex = parseInt(node?.dataset?.column || '0', 10);
50
+ const parentRow = node.parentNode as HTMLTableRowElement;
51
+ const rowIndex = parseInt(parentRow?.dataset?.row || '0', 10);
52
+
53
+ return { columnIndex, rowIndex };
54
+ }
55
+
56
+ interface InsertTableProps {
57
+ minRows?: number;
58
+ minColumns?: number;
59
+ maxColumns?: number;
60
+ initialRowCount?: number;
61
+ initialColumnCount?: number;
62
+ }
63
+
64
+ export const InsertTable: FC<InsertTableProps> = ({
65
+ minRows = 2,
66
+ minColumns = 2,
67
+ maxColumns = 10,
68
+ initialRowCount = 4,
69
+ initialColumnCount = 4
70
+ }) => {
71
+ const [editor] = useLexicalComposerContext();
72
+ const tableRef = useRef<HTMLTableElement>(null);
73
+ const boundaryTouchedRef = useRef(false);
74
+ const [columnCount, setColumnCount] = useState(initialColumnCount);
75
+ const [rowCount, setRowCount] = useState(initialRowCount);
76
+ const [isOpen, setIsOpen] = useState(false);
77
+
78
+ const togglePopper = () => {
79
+ reset();
80
+ setIsOpen(prevState => !prevState);
81
+ };
82
+
83
+ const { context, floating, reference, strategy, x, y } = useFloatingUI({
84
+ placement: 'bottom',
85
+ strategy: 'absolute',
86
+ open: isOpen,
87
+ onOpenChange: togglePopper
88
+ });
89
+
90
+ const { getFloatingProps } = useInteractions([
91
+ useDismiss(context, {
92
+ ancestorScroll: true
93
+ })
94
+ ]);
95
+
96
+ const handleCellMouseOver: MouseEventHandler<HTMLDivElement> = event => {
97
+ const target = event.nativeEvent.target as HTMLElement;
98
+ if (!tableRef.current?.contains(target) || !boundaryTouchedRef.current) {
99
+ return;
100
+ }
101
+
102
+ const { columnIndex, rowIndex } = getPosition(target);
103
+
104
+ if (columnIndex < columnCount && columnIndex >= minColumns) {
105
+ setColumnCount(columnIndex);
106
+ }
107
+ if (rowIndex < rowCount && rowIndex >= minRows) {
108
+ setRowCount(rowIndex);
109
+ }
110
+ };
111
+
112
+ function reset() {
113
+ setColumnCount(initialColumnCount);
114
+ setRowCount(initialRowCount);
115
+ boundaryTouchedRef.current = false;
116
+ }
117
+
118
+ const addColumn = () => {
119
+ boundaryTouchedRef.current = true;
120
+ if (columnCount + 1 <= maxColumns) {
121
+ setColumnCount(prevState => prevState + 1);
122
+ }
123
+ };
124
+
125
+ const addRow = () => {
126
+ boundaryTouchedRef.current = true;
127
+ setRowCount(prevState => prevState + 1);
128
+ };
129
+
130
+ const handleSelect = () => {
131
+ editor.dispatchCommand(INSERT_TABLE_COMMAND, {
132
+ columns: columnCount.toString(),
133
+ rows: rowCount.toString()
134
+ });
135
+ reset();
136
+ setIsOpen(false);
137
+ };
138
+
139
+ const cellSize = 25;
140
+ const tableHeight = rowCount * cellSize;
141
+ const tableWidth = columnCount * cellSize;
142
+
143
+ return (
144
+ <>
145
+ <TextFormatToolbarButton ref={reference} onClick={togglePopper} active={isOpen}>
146
+ <Icon name="grid" />
147
+ <div className={styles.spacer} />
148
+ <Icon name="chevronDown" />
149
+ </TextFormatToolbarButton>
150
+ <Popper
151
+ className={styles.popper}
152
+ ref={floating}
153
+ open={isOpen}
154
+ style={{ top: y ?? '', left: x ?? '', position: strategy }}
155
+ {...getFloatingProps({})}
156
+ >
157
+ <div
158
+ className={styles.tableContainer}
159
+ onMouseOver={handleCellMouseOver}
160
+ onClick={handleSelect}
161
+ >
162
+ <table
163
+ className={styles.table}
164
+ style={{ width: tableWidth, height: tableHeight }}
165
+ ref={tableRef}
166
+ >
167
+ <TableBody rowCount={rowCount} columnCount={columnCount} />
168
+ </table>
169
+ <div className={styles.dimensions}>{`${columnCount} x ${rowCount}`}</div>
170
+ <div
171
+ className={styles.addColumnArea}
172
+ style={{
173
+ left: tableWidth,
174
+ height: tableHeight + cellSize
175
+ }}
176
+ onMouseOver={addColumn}
177
+ />
178
+ <div
179
+ className={styles.addRowArea}
180
+ style={{
181
+ top: tableHeight,
182
+ width: tableWidth + cellSize
183
+ }}
184
+ onMouseOver={addRow}
185
+ />
186
+ </div>
187
+ </Popper>
188
+ </>
189
+ );
190
+ };
@@ -0,0 +1,10 @@
1
+ import { style } from '@vanilla-extract/css';
2
+ import { primaryColor } from '@jpmorganchase/mosaic-theme';
3
+
4
+ export default {
5
+ active: primaryColor({ variant: 'regular' }),
6
+ root: style({
7
+ width: '28px',
8
+ height: '28px'
9
+ })
10
+ };
@@ -0,0 +1,29 @@
1
+ import React, { forwardRef, Ref } from 'react';
2
+ import classnames from 'classnames';
3
+ import { ToolbarButton, ToolbarButtonProps } from '@jpmorganchase/uitk-lab';
4
+
5
+ import styles from './TextFormatToolbarButton.css';
6
+
7
+ interface TextFormatToolbarButtonProps extends ToolbarButtonProps {
8
+ active: boolean;
9
+ }
10
+
11
+ export const TextFormatToolbarButton = forwardRef(function TextFormatToolbarButton(
12
+ { active, className, onClick, children, ...rest }: TextFormatToolbarButtonProps,
13
+ ref: Ref<HTMLButtonElement>
14
+ ) {
15
+ return (
16
+ <ToolbarButton
17
+ ref={ref}
18
+ onClick={onClick}
19
+ // TODO remove once UITK Odyssey ToolbarButton supports className overrides
20
+ style={{ width: 28, height: 28 }}
21
+ className={classnames(className, styles.root, {
22
+ [styles.active]: active
23
+ })}
24
+ {...rest}
25
+ >
26
+ {children}
27
+ </ToolbarButton>
28
+ );
29
+ });
@@ -0,0 +1,27 @@
1
+ import { style } from '@vanilla-extract/css';
2
+ import { vars, code } from '@jpmorganchase/mosaic-theme';
3
+
4
+ export default {
5
+ // UITK Odyssey wraps all Tooltray items in a Formfields which adds default padding
6
+ // Tooltray needs refactoring so the user can compose Formfields themselves.
7
+ root: style({
8
+ margin: '-8px 0px 0px 0px'
9
+ }),
10
+ icon: style({
11
+ width: '14px',
12
+ fontSize: '12px'
13
+ }),
14
+ bold: style({ fontWeight: vars.fontWeight.bold }),
15
+ italic: style({ fontStyle: 'italic' }),
16
+ code: style([
17
+ code({ variant: 'regular' }),
18
+ style({
19
+ width: '22px',
20
+ height: '18px',
21
+ backgroundColor: 'inherit!important',
22
+ display: 'inline-flex',
23
+ justifyContent: 'center',
24
+ alignItems: 'center'
25
+ })
26
+ ])
27
+ };
@@ -0,0 +1,118 @@
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
+ import classnames from 'classnames';
3
+ import {
4
+ FORMAT_TEXT_COMMAND,
5
+ SELECTION_CHANGE_COMMAND,
6
+ COMMAND_PRIORITY_CRITICAL,
7
+ $getSelection,
8
+ $isRangeSelection,
9
+ TextFormatType
10
+ } from 'lexical';
11
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
12
+ import { $isListNode, ListNode } from '@lexical/list';
13
+ import { $getNearestNodeOfType } from '@lexical/utils';
14
+ import { $isHeadingNode } from '@lexical/rich-text';
15
+ import { $isCodeNode } from '@lexical/code';
16
+ import { Icon } from '@jpmorganchase/mosaic-components';
17
+
18
+ import { Tooltray } from '@jpmorganchase/uitk-lab';
19
+ import { TextFormatToolbarButton } from './TextFormatToolbarButton';
20
+ import styles from './TextFormatTooltray.css';
21
+ import { InsertBlockDropdown } from './InsertBlockDropdown';
22
+
23
+ export function TextFormatTooltray({ floating = false }) {
24
+ const [editor] = useLexicalComposerContext();
25
+ const [activeEditor, setActiveEditor] = useState(editor);
26
+ const [isBold, setIsBold] = useState(false);
27
+ const [isItalic, setIsItalic] = useState(false);
28
+ const [isCode, setIsCode] = useState(false);
29
+ const [blockType, setBlockType] = useState<string>('paragraph');
30
+
31
+ const handleFormat = (format: TextFormatType) => {
32
+ activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
33
+ };
34
+ const updateToolbar = useCallback(() => {
35
+ const selection = $getSelection();
36
+ if ($isRangeSelection(selection)) {
37
+ setIsBold(selection.hasFormat('bold'));
38
+ setIsItalic(selection.hasFormat('italic'));
39
+ setIsCode(selection.hasFormat('code'));
40
+
41
+ const anchorNode = selection.anchor.getNode();
42
+ const element =
43
+ anchorNode.getKey() === 'root' ? anchorNode : anchorNode.getTopLevelElementOrThrow();
44
+ const elementKey = element.getKey();
45
+ const elementDOM = activeEditor.getElementByKey(elementKey);
46
+
47
+ if (elementDOM !== null) {
48
+ if ($isListNode(element)) {
49
+ const parentList = $getNearestNodeOfType<ListNode>(anchorNode, ListNode);
50
+ const type = parentList ? parentList.getListType() : element.getListType();
51
+ setBlockType(type);
52
+ } else {
53
+ const type = $isHeadingNode(element) ? element.getTag() : element.getType();
54
+ if (type) {
55
+ setBlockType(type);
56
+ }
57
+ if ($isCodeNode(element)) {
58
+ setIsCode(true);
59
+ }
60
+ }
61
+ }
62
+ }
63
+ }, [activeEditor]);
64
+
65
+ useEffect(
66
+ () =>
67
+ activeEditor.registerUpdateListener(({ editorState }) => {
68
+ editorState.read(() => {
69
+ updateToolbar();
70
+ });
71
+ }),
72
+
73
+ [activeEditor, updateToolbar]
74
+ );
75
+
76
+ useEffect(
77
+ () =>
78
+ editor.registerCommand(
79
+ SELECTION_CHANGE_COMMAND,
80
+ (_payload, newEditor) => {
81
+ updateToolbar();
82
+ setActiveEditor(newEditor);
83
+ return false;
84
+ },
85
+ COMMAND_PRIORITY_CRITICAL
86
+ ),
87
+ [editor, updateToolbar]
88
+ );
89
+
90
+ return (
91
+ <div className={styles.root}>
92
+ {!floating ? <span /> : null}
93
+ <Tooltray data-collapsible="instant">
94
+ {!floating && <InsertBlockDropdown editor={activeEditor} type={blockType} />}
95
+ <TextFormatToolbarButton active={isBold} onClick={() => handleFormat('bold')} label="Bold">
96
+ <span className={classnames(styles.icon, styles.bold)}>B</span>
97
+ </TextFormatToolbarButton>
98
+ <TextFormatToolbarButton
99
+ active={isItalic}
100
+ onClick={() => handleFormat('italic')}
101
+ label="Italic"
102
+ >
103
+ <span className={classnames(styles.icon, styles.italic)}>I</span>
104
+ </TextFormatToolbarButton>
105
+ <TextFormatToolbarButton
106
+ active={isCode}
107
+ onClick={() => handleFormat('code')}
108
+ label="Inline Code"
109
+ >
110
+ <span className={classnames(styles.code)}>
111
+ <Icon name="chevronLeft" />
112
+ <Icon name="chevronRight" />
113
+ </span>
114
+ </TextFormatToolbarButton>
115
+ </Tooltray>
116
+ </div>
117
+ );
118
+ }
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+ import { Toolbar as ToolkitToolbar, ToolbarButton, Tooltray } from '@jpmorganchase/uitk-lab';
3
+ import { Icon } from '@jpmorganchase/mosaic-components';
4
+
5
+ import { useEditHistory } from '../../hooks/useEditHistory';
6
+ import { usePageState } from '../../store';
7
+ import { SaveButton } from '../SaveButton';
8
+ import { TextFormatTooltray } from './TextFormatTooltray';
9
+ import { InsertTable } from './InsertTable';
10
+ import { InsertImage } from './InsertImage';
11
+ import { InsertLinkButton } from './InsertLink';
12
+ import { InsertHorizontalRule } from './InsertHorizontalRule';
13
+
14
+ const Toolbar = () => {
15
+ const { canRedo, canUndo, redoEdit, undoEdit } = useEditHistory();
16
+ const { setPageState } = usePageState();
17
+
18
+ return (
19
+ <ToolkitToolbar aria-label="page editing toolbar" style={{ minWidth: '100px' }}>
20
+ <Tooltray aria-label="history tooltray">
21
+ <ToolbarButton label="Undo" disabled={!canUndo} onClick={undoEdit}>
22
+ <Icon name="undo" />
23
+ </ToolbarButton>
24
+ <ToolbarButton label="Redo" disabled={!canRedo} onClick={redoEdit}>
25
+ <Icon name="redo" />
26
+ </ToolbarButton>
27
+ </Tooltray>
28
+ <TextFormatTooltray />
29
+ <Tooltray aria-label="text format tooltray">
30
+ <InsertLinkButton />
31
+ <InsertTable />
32
+ <InsertImage />
33
+ <InsertHorizontalRule />
34
+ </Tooltray>
35
+ <Tooltray aria-label="stop editing tooltray" alignEnd>
36
+ <ToolbarButton label="Cancel Editing" onClick={() => setPageState('VIEW')}>
37
+ <Icon name="delete" />
38
+ </ToolbarButton>
39
+ </Tooltray>
40
+ <Tooltray aria-label="copy tooltray" data-pad-start>
41
+ <SaveButton />
42
+ </Tooltray>
43
+ </ToolkitToolbar>
44
+ );
45
+ };
46
+
47
+ export default Toolbar;
@@ -0,0 +1,69 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+
5
+ import { EditorControls } from '../EditorControls';
6
+ import store from '../../store';
7
+
8
+ jest.mock('../../store', () => ({
9
+ ...jest.requireActual('../../store'),
10
+ __esModule: true,
11
+ default: jest.fn()
12
+ }));
13
+
14
+ const startEditingSpy = jest.fn();
15
+ const stopEditingSpy = jest.fn();
16
+
17
+ describe('GIVEN an EditorControls component', () => {
18
+ describe('WHEN **NOT** logged in', () => {
19
+ beforeAll(() => {
20
+ jest.mocked(store).mockReturnValue({
21
+ startEditing: startEditingSpy,
22
+ isEditing: false
23
+ });
24
+ });
25
+ test('THEN the edit button is disabled', () => {
26
+ render(<EditorControls isLoggedIn={false} />);
27
+ const tooltips = screen.queryAllByRole('tooltip');
28
+ expect(tooltips.length).toBe(0);
29
+ });
30
+ });
31
+
32
+ describe('WHEN logged in', () => {
33
+ test('THEN the edit button is enabled', () => {
34
+ render(<EditorControls isLoggedIn />);
35
+ const button = screen.getByRole('button');
36
+ expect(button.getAttribute('aria-disabled')).toBe('false');
37
+ });
38
+
39
+ describe('AND WHEN **NOT** editing and the edit button is clicked', () => {
40
+ beforeAll(() => {
41
+ jest.mocked(store).mockReturnValue({
42
+ startEditing: startEditingSpy,
43
+ pageState: 'VIEW'
44
+ });
45
+ });
46
+ test('THEN editing is started', async () => {
47
+ render(<EditorControls isLoggedIn />);
48
+ const button = screen.getByLabelText('start editing');
49
+ await userEvent.click(button);
50
+ expect(startEditingSpy).toHaveBeenCalledTimes(1);
51
+ });
52
+ });
53
+
54
+ describe('AND WHEN editing and the edit button is clicked', () => {
55
+ beforeAll(() => {
56
+ jest.mocked(store).mockReturnValue({
57
+ stopEditing: stopEditingSpy,
58
+ pageState: 'EDIT'
59
+ });
60
+ });
61
+ test('THEN editing is stopped', async () => {
62
+ render(<EditorControls isLoggedIn />);
63
+ const button = screen.getByLabelText('cancel editing');
64
+ await userEvent.click(button);
65
+ expect(stopEditingSpy).toHaveBeenCalledTimes(1);
66
+ });
67
+ });
68
+ });
69
+ });
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+
4
+ import * as store from '../../store';
5
+ import StatusBanner from '../StatusBanner';
6
+
7
+ jest.mock('../../store');
8
+
9
+ describe('GIVEN a StatusBanner ', () => {
10
+ const { usePageState } = store;
11
+
12
+ test('THEN it does not report on non-error states', () => {
13
+ usePageState.mockReturnValue({
14
+ pageState: 'WARNING',
15
+ errorMessage: 'some warning'
16
+ });
17
+ // arrange
18
+ render(<StatusBanner />);
19
+ // assert
20
+ expect(screen.queryByText('some warning')).not.toBeInTheDocument();
21
+ });
22
+
23
+ test('THEN it does report on error states', () => {
24
+ usePageState.mockReturnValue({
25
+ pageState: 'ERROR',
26
+ errorMessage: 'some error'
27
+ });
28
+ // arrange
29
+ render(<StatusBanner />);
30
+ // assert
31
+ expect(screen.getByText('some error')).toBeDefined();
32
+ });
33
+ });
@@ -0,0 +1,49 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
3
+ import {
4
+ CAN_REDO_COMMAND,
5
+ CAN_UNDO_COMMAND,
6
+ REDO_COMMAND,
7
+ UNDO_COMMAND,
8
+ COMMAND_PRIORITY_CRITICAL
9
+ } from 'lexical';
10
+ import { mergeRegister } from '@lexical/utils';
11
+
12
+ export function useEditHistory() {
13
+ const [editor] = useLexicalComposerContext();
14
+ const [canUndo, setCanUndo] = useState(false);
15
+ const [canRedo, setCanRedo] = useState(false);
16
+
17
+ useEffect(
18
+ () =>
19
+ mergeRegister(
20
+ editor.registerCommand<boolean>(
21
+ CAN_UNDO_COMMAND,
22
+ payload => {
23
+ setCanUndo(payload);
24
+ return false;
25
+ },
26
+ COMMAND_PRIORITY_CRITICAL
27
+ ),
28
+ editor.registerCommand<boolean>(
29
+ CAN_REDO_COMMAND,
30
+ payload => {
31
+ setCanRedo(payload);
32
+ return false;
33
+ },
34
+ COMMAND_PRIORITY_CRITICAL
35
+ )
36
+ ),
37
+ [editor]
38
+ );
39
+
40
+ const undoEdit = useCallback(() => editor.dispatchCommand(UNDO_COMMAND, undefined), [editor]);
41
+ const redoEdit = useCallback(() => editor.dispatchCommand(REDO_COMMAND, undefined), [editor]);
42
+
43
+ return {
44
+ canRedo,
45
+ canUndo,
46
+ undoEdit,
47
+ redoEdit
48
+ };
49
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ import Editor from './components/Editor';
2
+ import { EditorControls } from './components/EditorControls';
3
+ import { default as useContentEditor, usePageState } from './store';
4
+
5
+ export { Editor, EditorControls, usePageState, useContentEditor };
@@ -0,0 +1,23 @@
1
+ import { HeadingNode, QuoteNode } from '@lexical/rich-text';
2
+ import { ListNode, ListItemNode } from '@lexical/list';
3
+ import { AutoLinkNode, LinkNode } from '@lexical/link';
4
+ import { TableCellNode, TableNode, TableRowNode } from '@lexical/table';
5
+ import { CodeHighlightNode, CodeNode } from '@lexical/code';
6
+ import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode';
7
+
8
+ const nodes = [
9
+ AutoLinkNode,
10
+ CodeHighlightNode,
11
+ CodeNode,
12
+ HeadingNode,
13
+ HorizontalRuleNode,
14
+ LinkNode,
15
+ ListItemNode,
16
+ ListNode,
17
+ QuoteNode,
18
+ TableCellNode,
19
+ TableNode,
20
+ TableRowNode
21
+ ];
22
+
23
+ export { nodes };