@mirrormedia/lilith-draft-editor 1.0.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/draft-js/block-renderer/background-image-block.tsx +113 -0
- package/lib/draft-js/block-renderer/background-video-block.tsx +120 -0
- package/lib/draft-js/block-renderer/color-box-block.tsx +85 -0
- package/lib/draft-js/block-renderer/divider-block.tsx +12 -0
- package/lib/draft-js/block-renderer/embedded-code-block.tsx +65 -0
- package/lib/draft-js/block-renderer/image-block.tsx +41 -0
- package/lib/draft-js/block-renderer/info-box-block.tsx +85 -0
- package/lib/draft-js/block-renderer/media-block.tsx +36 -0
- package/lib/draft-js/block-renderer/related-post-block.tsx +47 -0
- package/lib/draft-js/block-renderer/side-index-block.tsx +113 -0
- package/lib/draft-js/block-renderer/slideshow-block.tsx +62 -0
- package/lib/draft-js/block-renderer/table-block.tsx +488 -0
- package/lib/draft-js/buttons/annotation.tsx +113 -0
- package/lib/draft-js/buttons/background-color.tsx +125 -0
- package/lib/draft-js/buttons/background-image.tsx +276 -0
- package/lib/draft-js/buttons/background-video.tsx +275 -0
- package/lib/draft-js/buttons/color-box.tsx +207 -0
- package/lib/draft-js/buttons/divider.tsx +56 -0
- package/lib/draft-js/buttons/embedded-code.tsx +126 -0
- package/lib/draft-js/buttons/enlarge.tsx +11 -0
- package/lib/draft-js/buttons/font-color.tsx +113 -0
- package/lib/draft-js/buttons/image.tsx +71 -0
- package/lib/draft-js/buttons/info-box.tsx +170 -0
- package/lib/draft-js/buttons/link.tsx +103 -0
- package/lib/draft-js/buttons/media.tsx +120 -0
- package/lib/draft-js/buttons/related-post.tsx +81 -0
- package/lib/draft-js/buttons/selector/align-selector.tsx +65 -0
- package/lib/draft-js/buttons/selector/image-selector.tsx +485 -0
- package/lib/draft-js/buttons/selector/pagination.tsx +83 -0
- package/lib/draft-js/buttons/selector/post-selector.tsx +367 -0
- package/lib/draft-js/buttons/selector/search-box.tsx +39 -0
- package/lib/draft-js/buttons/selector/video-selector.tsx +312 -0
- package/lib/draft-js/buttons/side-index.tsx +257 -0
- package/lib/draft-js/buttons/slideshow.tsx +81 -0
- package/lib/draft-js/buttons/table.tsx +63 -0
- package/lib/draft-js/buttons/text-align.tsx +88 -0
- package/lib/draft-js/editor/basic-editor.tsx +384 -0
- package/lib/draft-js/editor/block-redender-fn.tsx +77 -0
- package/lib/draft-js/editor/draft-converter/api-data-instance.js +58 -0
- package/lib/draft-js/editor/draft-converter/atomic-block-processor.js +233 -0
- package/lib/draft-js/editor/draft-converter/entities.js +76 -0
- package/lib/draft-js/editor/draft-converter/index.js +201 -0
- package/lib/draft-js/editor/draft-converter/inline-styles-processor.js +238 -0
- package/lib/draft-js/editor/entity-decorator.tsx +7 -0
- package/lib/draft-js/editor/modifier.tsx +71 -0
- package/lib/draft-js/entity-decorator/annotation-decorator.tsx +81 -0
- package/lib/draft-js/entity-decorator/link-decorator.tsx +27 -0
- package/lib/index.js +31 -0
- package/lib/website/mirrormedia/custom/block-renderer/background-image-block.tsx +128 -0
- package/lib/website/mirrormedia/custom/block-renderer/background-video-block.tsx +135 -0
- package/lib/website/mirrormedia/custom/block-renderer/color-box-block.tsx +98 -0
- package/lib/website/mirrormedia/custom/block-renderer/divider-block.tsx +12 -0
- package/lib/website/mirrormedia/custom/block-renderer/embedded-code-block.tsx +65 -0
- package/lib/website/mirrormedia/custom/block-renderer/image-block.tsx +41 -0
- package/lib/website/mirrormedia/custom/block-renderer/info-box-block.tsx +98 -0
- package/lib/website/mirrormedia/custom/block-renderer/media-block.tsx +36 -0
- package/lib/website/mirrormedia/custom/block-renderer/related-post-block.tsx +47 -0
- package/lib/website/mirrormedia/custom/block-renderer/side-index-block.tsx +125 -0
- package/lib/website/mirrormedia/custom/block-renderer/slideshow-block.tsx +62 -0
- package/lib/website/mirrormedia/custom/block-renderer/table-block.tsx +537 -0
- package/lib/website/mirrormedia/custom/entity-decorator/annotation-decorator.tsx +81 -0
- package/lib/website/mirrormedia/custom/entity-decorator/link-decorator.tsx +27 -0
- package/lib/website/mirrormedia/custom/selector/align-selector.tsx +65 -0
- package/lib/website/mirrormedia/custom/selector/image-selector.tsx +485 -0
- package/lib/website/mirrormedia/custom/selector/pagination.tsx +83 -0
- package/lib/website/mirrormedia/custom/selector/post-selector.tsx +367 -0
- package/lib/website/mirrormedia/custom/selector/search-box.tsx +39 -0
- package/lib/website/mirrormedia/custom/selector/video-selector.tsx +310 -0
- package/lib/website/mirrormedia/draft-editor/block-redender-fn.tsx +77 -0
- package/lib/website/mirrormedia/draft-editor/entity-decorator.tsx +7 -0
- package/lib/website/mirrormedia/draft-editor/index.tsx +909 -0
- package/lib/website/mirrormedia/draft-renderer/block-redender-fn.tsx +77 -0
- package/lib/website/mirrormedia/draft-renderer/entity-decorator.tsx +7 -0
- package/lib/website/mirrormedia/draft-renderer/index-deprecated.tsx +43 -0
- package/lib/website/mirrormedia/draft-renderer/index.tsx +150 -0
- package/lib/website/mirrormedia/index.js +19 -0
- package/lib/website/readr/custom/block-renderer/background-image-block.tsx +128 -0
- package/lib/website/readr/custom/block-renderer/background-video-block.tsx +135 -0
- package/lib/website/readr/custom/block-renderer/color-box-block.tsx +98 -0
- package/lib/website/readr/custom/block-renderer/divider-block.tsx +12 -0
- package/lib/website/readr/custom/block-renderer/embedded-code-block.tsx +65 -0
- package/lib/website/readr/custom/block-renderer/image-block.tsx +41 -0
- package/lib/website/readr/custom/block-renderer/info-box-block.tsx +98 -0
- package/lib/website/readr/custom/block-renderer/media-block.tsx +36 -0
- package/lib/website/readr/custom/block-renderer/related-post-block.tsx +47 -0
- package/lib/website/readr/custom/block-renderer/side-index-block.tsx +125 -0
- package/lib/website/readr/custom/block-renderer/slideshow-block.tsx +62 -0
- package/lib/website/readr/custom/block-renderer/table-block.tsx +537 -0
- package/lib/website/readr/custom/entity-decorator/annotation-decorator.tsx +81 -0
- package/lib/website/readr/custom/entity-decorator/link-decorator.tsx +27 -0
- package/lib/website/readr/custom/selector/align-selector.tsx +65 -0
- package/lib/website/readr/custom/selector/image-selector.tsx +485 -0
- package/lib/website/readr/custom/selector/pagination.tsx +83 -0
- package/lib/website/readr/custom/selector/post-selector.tsx +367 -0
- package/lib/website/readr/custom/selector/search-box.tsx +39 -0
- package/lib/website/readr/custom/selector/video-selector.tsx +310 -0
- package/lib/website/readr/draft-editor/block-redender-fn.tsx +77 -0
- package/lib/website/readr/draft-editor/entity-decorator.tsx +7 -0
- package/lib/website/readr/draft-editor/index.tsx +909 -0
- package/lib/website/readr/draft-renderer/block-redender-fn.tsx +77 -0
- package/lib/website/readr/draft-renderer/entity-decorator.tsx +7 -0
- package/lib/website/readr/draft-renderer/index-deprecated.tsx +43 -0
- package/lib/website/readr/draft-renderer/index.tsx +150 -0
- package/lib/website/readr/index.js +19 -0
- package/package.json +39 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import styled from 'styled-components'
|
|
3
|
+
import {
|
|
4
|
+
ContentBlock,
|
|
5
|
+
ContentState,
|
|
6
|
+
Editor,
|
|
7
|
+
EditorState,
|
|
8
|
+
RawDraftContentState,
|
|
9
|
+
convertFromRaw,
|
|
10
|
+
convertToRaw,
|
|
11
|
+
} from 'draft-js'
|
|
12
|
+
import cloneDeep from 'lodash/cloneDeep'
|
|
13
|
+
|
|
14
|
+
const _ = {
|
|
15
|
+
cloneDeep,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
enum ActionType {
|
|
19
|
+
Insert = 'insert',
|
|
20
|
+
Delete = 'delete',
|
|
21
|
+
Update = 'update',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
enum TableEnum {
|
|
25
|
+
Row = 'row',
|
|
26
|
+
Column = 'column',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type RawTableData = RawDraftContentState[][]
|
|
30
|
+
type TableData = EditorState[][]
|
|
31
|
+
type UpdateAction = {
|
|
32
|
+
type: ActionType.Update
|
|
33
|
+
cIndex: number
|
|
34
|
+
rIndex: number
|
|
35
|
+
value: EditorState
|
|
36
|
+
}
|
|
37
|
+
type InsertAction = {
|
|
38
|
+
type: ActionType.Insert
|
|
39
|
+
target: TableEnum.Row | TableEnum.Column
|
|
40
|
+
index: number
|
|
41
|
+
}
|
|
42
|
+
type DeleteAction = {
|
|
43
|
+
type: ActionType.Delete
|
|
44
|
+
target: TableEnum.Row | TableEnum.Column
|
|
45
|
+
index: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type TableStyles = {
|
|
49
|
+
rows: Record<string, string>[]
|
|
50
|
+
columns?: Record<string, string>[]
|
|
51
|
+
cells?: Record<string, string>[][]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createEmptyRow(colLen = 0, emptyValue: EditorState): TableData {
|
|
55
|
+
const rtn: EditorState[] = []
|
|
56
|
+
for (let i = 0; i < colLen; i++) {
|
|
57
|
+
rtn.push(emptyValue)
|
|
58
|
+
}
|
|
59
|
+
return rtn
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveTableStyles(
|
|
63
|
+
action: InsertAction | DeleteAction,
|
|
64
|
+
tableStyles: TableStyles
|
|
65
|
+
) {
|
|
66
|
+
switch (action?.type) {
|
|
67
|
+
case ActionType.Insert: {
|
|
68
|
+
if (action.target === TableEnum.Row) {
|
|
69
|
+
const rows = [
|
|
70
|
+
...tableStyles.rows.slice(0, action.index),
|
|
71
|
+
{},
|
|
72
|
+
...tableStyles.rows.slice(action.index),
|
|
73
|
+
]
|
|
74
|
+
return Object.assign({}, tableStyles, { rows })
|
|
75
|
+
}
|
|
76
|
+
// TODO: handle target === TableEnum.Column if needed
|
|
77
|
+
return tableStyles
|
|
78
|
+
}
|
|
79
|
+
case ActionType.Delete: {
|
|
80
|
+
if (action.target === TableEnum.Row) {
|
|
81
|
+
const rows = [
|
|
82
|
+
...tableStyles.rows.slice(0, action.index),
|
|
83
|
+
...tableStyles.rows.slice(action.index + 1),
|
|
84
|
+
]
|
|
85
|
+
return Object.assign({}, tableStyles, { rows })
|
|
86
|
+
}
|
|
87
|
+
// TODO: handle target === TableEnum.Column if needed
|
|
88
|
+
return tableStyles
|
|
89
|
+
}
|
|
90
|
+
// TODO: handle action.type === ActionType.Update if needed
|
|
91
|
+
default: {
|
|
92
|
+
return tableStyles
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolveTableData(
|
|
98
|
+
action: UpdateAction | InsertAction | DeleteAction,
|
|
99
|
+
tableData: TableData
|
|
100
|
+
): TableData {
|
|
101
|
+
switch (action?.type) {
|
|
102
|
+
case ActionType.Insert: {
|
|
103
|
+
if (typeof action?.index !== 'number') {
|
|
104
|
+
return tableData
|
|
105
|
+
}
|
|
106
|
+
if (action?.target === TableEnum.Column) {
|
|
107
|
+
// add the new column at specific position in each row
|
|
108
|
+
return tableData.map((r) => [
|
|
109
|
+
...r.slice(0, action?.index),
|
|
110
|
+
EditorState.createEmpty(),
|
|
111
|
+
...r.slice(action?.index),
|
|
112
|
+
])
|
|
113
|
+
}
|
|
114
|
+
// add the new row
|
|
115
|
+
return [
|
|
116
|
+
...tableData.slice(0, action?.index),
|
|
117
|
+
createEmptyRow(tableData?.[0]?.length, EditorState.createEmpty()),
|
|
118
|
+
...tableData.slice(action?.index),
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
case ActionType.Delete: {
|
|
122
|
+
if (typeof action?.index !== 'number') {
|
|
123
|
+
return tableData
|
|
124
|
+
}
|
|
125
|
+
if (action?.target === 'column') {
|
|
126
|
+
// delete the column at specific position in each row
|
|
127
|
+
return tableData.map((r) => [
|
|
128
|
+
...r.slice(0, action.index),
|
|
129
|
+
...r.slice(action.index + 1),
|
|
130
|
+
])
|
|
131
|
+
}
|
|
132
|
+
// delete the column
|
|
133
|
+
return [
|
|
134
|
+
...tableData.slice(0, action.index),
|
|
135
|
+
...tableData.slice(action.index + 1),
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
case ActionType.Update: {
|
|
139
|
+
// The reason we copy the array is to make sure
|
|
140
|
+
// that React component re-renders.
|
|
141
|
+
const copiedData = [...tableData]
|
|
142
|
+
if (
|
|
143
|
+
typeof action?.rIndex !== 'number' ||
|
|
144
|
+
typeof action?.cIndex !== 'number'
|
|
145
|
+
) {
|
|
146
|
+
return copiedData
|
|
147
|
+
}
|
|
148
|
+
copiedData[action.rIndex][action.cIndex] = action?.value
|
|
149
|
+
return copiedData
|
|
150
|
+
}
|
|
151
|
+
default: {
|
|
152
|
+
return tableData
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
type TableBlockProps = {
|
|
158
|
+
block: ContentBlock
|
|
159
|
+
blockProps: {
|
|
160
|
+
onEditStart: () => void
|
|
161
|
+
onEditFinish: ({
|
|
162
|
+
entityKey,
|
|
163
|
+
entityData,
|
|
164
|
+
}: {
|
|
165
|
+
entityKey?: string
|
|
166
|
+
entityData?: Record<string, unknown>
|
|
167
|
+
}) => void
|
|
168
|
+
getMainEditorReadOnly: () => boolean
|
|
169
|
+
}
|
|
170
|
+
contentState: ContentState
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function convertTableDataFromRaw(rawTableData: RawTableData): TableData {
|
|
174
|
+
return rawTableData.map((rowData) => {
|
|
175
|
+
return rowData.map((colData) => {
|
|
176
|
+
const contentState = convertFromRaw(colData)
|
|
177
|
+
return EditorState.createWithContent(contentState)
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function convertTableDataToRaw(tableData: TableData): RawTableData {
|
|
183
|
+
return tableData.map((rowData) => {
|
|
184
|
+
return rowData.map((colData) => {
|
|
185
|
+
return convertToRaw(colData.getCurrentContent())
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const Table = styled.div`
|
|
191
|
+
display: table;
|
|
192
|
+
width: 95%;
|
|
193
|
+
border-collapse: collapse;
|
|
194
|
+
`
|
|
195
|
+
|
|
196
|
+
const Tr = styled.div`
|
|
197
|
+
display: table-row;
|
|
198
|
+
`
|
|
199
|
+
|
|
200
|
+
const Td = styled.div`
|
|
201
|
+
display: table-cell;
|
|
202
|
+
border-width: 1px;
|
|
203
|
+
min-width: 100px;
|
|
204
|
+
min-height: 40px;
|
|
205
|
+
padding: 10px;
|
|
206
|
+
`
|
|
207
|
+
|
|
208
|
+
const StyledFirstRow = styled.div`
|
|
209
|
+
display: table-row;
|
|
210
|
+
height: 10px;
|
|
211
|
+
|
|
212
|
+
div {
|
|
213
|
+
display: table-cell;
|
|
214
|
+
position: relative;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
span {
|
|
218
|
+
cursor: pointer;
|
|
219
|
+
line-height: 10px;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
span:first-child {
|
|
223
|
+
position: absolute;
|
|
224
|
+
right: 50%;
|
|
225
|
+
transform: translateX(50%);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
span:first-child:before {
|
|
229
|
+
content: '•';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
span:first-child:hover:before {
|
|
233
|
+
content: '➖';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
span:last-child {
|
|
237
|
+
position: absolute;
|
|
238
|
+
right: -5px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
span:last-child:before {
|
|
242
|
+
content: '•';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
span:last-child:hover:before {
|
|
246
|
+
content: '➕';
|
|
247
|
+
}
|
|
248
|
+
`
|
|
249
|
+
|
|
250
|
+
const StyledFirstColumn = styled.div`
|
|
251
|
+
display: table-cell;
|
|
252
|
+
width: 10px;
|
|
253
|
+
position: relative;
|
|
254
|
+
|
|
255
|
+
span {
|
|
256
|
+
cursor: pointer;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
span:first-child {
|
|
260
|
+
position: absolute;
|
|
261
|
+
bottom: 50%;
|
|
262
|
+
right: 0px;
|
|
263
|
+
transform: translateY(50%);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
span:first-child:before {
|
|
267
|
+
content: '•';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
span:first-child:hover:before {
|
|
271
|
+
content: '➖';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
span:last-child {
|
|
275
|
+
position: absolute;
|
|
276
|
+
bottom: -10px;
|
|
277
|
+
right: 0px;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
span:last-child:before {
|
|
281
|
+
content: '•';
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
span:last-child:hover:before {
|
|
285
|
+
content: '➕';
|
|
286
|
+
}
|
|
287
|
+
`
|
|
288
|
+
|
|
289
|
+
const TableBlockContainer = styled.div`
|
|
290
|
+
margin: 15px 0;
|
|
291
|
+
position: relative;
|
|
292
|
+
overflow: scroll;
|
|
293
|
+
padding: 15px;
|
|
294
|
+
`
|
|
295
|
+
|
|
296
|
+
const StyledTable = styled.div`
|
|
297
|
+
display: table;
|
|
298
|
+
width: 95%;
|
|
299
|
+
border-collapse: collapse;
|
|
300
|
+
`
|
|
301
|
+
|
|
302
|
+
const StyledTr = styled.div`
|
|
303
|
+
display: table-row;
|
|
304
|
+
`
|
|
305
|
+
|
|
306
|
+
const StyledTd = styled.div`
|
|
307
|
+
display: table-cell;
|
|
308
|
+
border: 1px solid #e1e5e9;
|
|
309
|
+
min-width: 100px;
|
|
310
|
+
min-height: 40px;
|
|
311
|
+
padding: 10px;
|
|
312
|
+
`
|
|
313
|
+
|
|
314
|
+
export const TableEditorBlock = (props: TableBlockProps) => {
|
|
315
|
+
const { block, blockProps, contentState } = props
|
|
316
|
+
const { onEditStart, onEditFinish, getMainEditorReadOnly } = blockProps
|
|
317
|
+
const entityKey = block.getEntityAt(0)
|
|
318
|
+
const entity = contentState.getEntity(entityKey)
|
|
319
|
+
const {
|
|
320
|
+
tableData: rawTableData,
|
|
321
|
+
tableStyles: _tableStyles,
|
|
322
|
+
}: { tableData: RawTableData; tableStyles: TableStyles } = entity.getData()
|
|
323
|
+
const [tableData, setTableData] = useState(
|
|
324
|
+
convertTableDataFromRaw(rawTableData)
|
|
325
|
+
)
|
|
326
|
+
// deep clone `_tableStyles` to prevent updating the entity data directly
|
|
327
|
+
const [tableStyles, setTableStyles] = useState(_.cloneDeep(_tableStyles))
|
|
328
|
+
const tableRef = useRef(null)
|
|
329
|
+
|
|
330
|
+
// `TableBlock` will render other inner/nested DraftJS Editors inside the main Editor.
|
|
331
|
+
// However, main Editor's `readOnly` needs to be mutually exclusive with nested Editors' `readOnly`.
|
|
332
|
+
// If the main Editor and nested Editor are editable (`readOnly={false}`) at the same time,
|
|
333
|
+
// there will be a DraftJS Edtior Selection bug.
|
|
334
|
+
const [cellEditorReadOnly, setCellEditorReadOnly] = useState(
|
|
335
|
+
!getMainEditorReadOnly()
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
// The user clicks the table for editing
|
|
339
|
+
const onTableClick = () => {
|
|
340
|
+
// call `onEditStart` function to tell the main DraftJS Editor
|
|
341
|
+
// that we are going to interact with the custom atomic block.
|
|
342
|
+
onEditStart()
|
|
343
|
+
|
|
344
|
+
// make nested DraftJS Editors editable
|
|
345
|
+
setCellEditorReadOnly(false)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
useEffect(
|
|
349
|
+
() => {
|
|
350
|
+
// The user clicks other places except the table,
|
|
351
|
+
// so we think he/she doesn't want to edit the table anymore.
|
|
352
|
+
// Therefore, we call `onEditFinish` function to pass modified table data
|
|
353
|
+
// back to the main DraftJS Edtior.
|
|
354
|
+
function handleClickOutside(event) {
|
|
355
|
+
// `!cellEditorReadOnly` condition is needed.
|
|
356
|
+
// If there are two tables in the main Editor,
|
|
357
|
+
// this `handleClickOutside` will only handle the just updated one.
|
|
358
|
+
if (
|
|
359
|
+
tableRef.current &&
|
|
360
|
+
!(tableRef.current as HTMLElement).contains(event.target) &&
|
|
361
|
+
!cellEditorReadOnly
|
|
362
|
+
) {
|
|
363
|
+
// make inner DraftJS Editors NOT editable
|
|
364
|
+
setCellEditorReadOnly(true)
|
|
365
|
+
|
|
366
|
+
// call `onEditFinish` function tell the main DraftJS Editor
|
|
367
|
+
// that we are finishing interacting with the custom atomic block.
|
|
368
|
+
onEditFinish({
|
|
369
|
+
entityKey,
|
|
370
|
+
entityData: {
|
|
371
|
+
tableData: convertTableDataToRaw(tableData),
|
|
372
|
+
tableStyles,
|
|
373
|
+
},
|
|
374
|
+
})
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
console.debug(
|
|
378
|
+
'(rich-text-editor/table): add click outside event listener'
|
|
379
|
+
)
|
|
380
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
381
|
+
return () => {
|
|
382
|
+
// Unbind the event listener on clean up
|
|
383
|
+
console.debug(
|
|
384
|
+
'(rich-text-editor/table): remove click outside event listener'
|
|
385
|
+
)
|
|
386
|
+
document.removeEventListener('mousedown', handleClickOutside)
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
// Skip running effect if `tableData` and `cellEditorReadOnly` are not changed.
|
|
390
|
+
[tableData, cellEditorReadOnly]
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
return (
|
|
394
|
+
<TableBlockContainer>
|
|
395
|
+
<Table key={entityKey} onClick={onTableClick} ref={tableRef}>
|
|
396
|
+
<StyledFirstRow>
|
|
397
|
+
{/* The following `div` is for the first empty column in rows */}
|
|
398
|
+
<div />
|
|
399
|
+
{tableData?.[0]?.map((colData, cIndex) => {
|
|
400
|
+
return (
|
|
401
|
+
<div key={`col_${cIndex + 1}`}>
|
|
402
|
+
<span
|
|
403
|
+
onClick={() => {
|
|
404
|
+
const deleteColumn: DeleteAction = {
|
|
405
|
+
type: ActionType.Delete,
|
|
406
|
+
target: TableEnum.Column,
|
|
407
|
+
index: cIndex,
|
|
408
|
+
}
|
|
409
|
+
const updatedTableData = resolveTableData(
|
|
410
|
+
deleteColumn,
|
|
411
|
+
tableData
|
|
412
|
+
)
|
|
413
|
+
setTableData(updatedTableData)
|
|
414
|
+
}}
|
|
415
|
+
/>
|
|
416
|
+
<span
|
|
417
|
+
onClick={() => {
|
|
418
|
+
const insertColumn: InsertAction = {
|
|
419
|
+
type: ActionType.Insert,
|
|
420
|
+
target: TableEnum.Column,
|
|
421
|
+
index: cIndex + 1,
|
|
422
|
+
}
|
|
423
|
+
const updatedTableData = resolveTableData(
|
|
424
|
+
insertColumn,
|
|
425
|
+
tableData
|
|
426
|
+
)
|
|
427
|
+
setTableData(updatedTableData)
|
|
428
|
+
}}
|
|
429
|
+
/>
|
|
430
|
+
</div>
|
|
431
|
+
)
|
|
432
|
+
})}
|
|
433
|
+
</StyledFirstRow>
|
|
434
|
+
{tableData.map((rowData, rIndex) => {
|
|
435
|
+
const colsJsx = rowData.map((colData, cIndex) => {
|
|
436
|
+
return (
|
|
437
|
+
<Td key={`col_${cIndex}`}>
|
|
438
|
+
{/* TODO: add editor buttons if needed */}
|
|
439
|
+
<Editor
|
|
440
|
+
onChange={(editorState) => {
|
|
441
|
+
const updateAction: UpdateAction = {
|
|
442
|
+
type: ActionType.Update,
|
|
443
|
+
cIndex,
|
|
444
|
+
rIndex,
|
|
445
|
+
value: editorState,
|
|
446
|
+
}
|
|
447
|
+
const updatedTableData = resolveTableData(
|
|
448
|
+
updateAction,
|
|
449
|
+
tableData
|
|
450
|
+
)
|
|
451
|
+
setTableData(updatedTableData)
|
|
452
|
+
}}
|
|
453
|
+
editorState={colData}
|
|
454
|
+
readOnly={cellEditorReadOnly}
|
|
455
|
+
/>
|
|
456
|
+
</Td>
|
|
457
|
+
)
|
|
458
|
+
})
|
|
459
|
+
return (
|
|
460
|
+
<React.Fragment key={`row_${rIndex}`}>
|
|
461
|
+
<Tr style={tableStyles?.rows?.[rIndex]}>
|
|
462
|
+
<StyledFirstColumn>
|
|
463
|
+
<span
|
|
464
|
+
onClick={() => {
|
|
465
|
+
const deleteRowAction: DeleteAction = {
|
|
466
|
+
type: ActionType.Delete,
|
|
467
|
+
target: TableEnum.Row,
|
|
468
|
+
index: rIndex,
|
|
469
|
+
}
|
|
470
|
+
const updatedTableData = resolveTableData(
|
|
471
|
+
deleteRowAction,
|
|
472
|
+
tableData
|
|
473
|
+
)
|
|
474
|
+
setTableData(updatedTableData)
|
|
475
|
+
setTableStyles(
|
|
476
|
+
resolveTableStyles(deleteRowAction, tableStyles)
|
|
477
|
+
)
|
|
478
|
+
}}
|
|
479
|
+
/>
|
|
480
|
+
<span
|
|
481
|
+
onClick={() => {
|
|
482
|
+
const addRowAction: InsertAction = {
|
|
483
|
+
type: ActionType.Insert,
|
|
484
|
+
target: TableEnum.Row,
|
|
485
|
+
index: rIndex + 1,
|
|
486
|
+
}
|
|
487
|
+
const updatedTableData = resolveTableData(
|
|
488
|
+
addRowAction,
|
|
489
|
+
tableData
|
|
490
|
+
)
|
|
491
|
+
setTableData(updatedTableData)
|
|
492
|
+
setTableStyles(
|
|
493
|
+
resolveTableStyles(addRowAction, tableStyles)
|
|
494
|
+
)
|
|
495
|
+
}}
|
|
496
|
+
/>
|
|
497
|
+
</StyledFirstColumn>
|
|
498
|
+
{colsJsx}
|
|
499
|
+
</Tr>
|
|
500
|
+
</React.Fragment>
|
|
501
|
+
)
|
|
502
|
+
})}
|
|
503
|
+
</Table>
|
|
504
|
+
</TableBlockContainer>
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export const TableBlock = (props) => {
|
|
509
|
+
const { block, contentState } = props
|
|
510
|
+
const entityKey = block.getEntityAt(0)
|
|
511
|
+
const entity = contentState.getEntity(entityKey)
|
|
512
|
+
const { tableData: rawTableData } = entity.getData()
|
|
513
|
+
const [tableData] = useState(convertTableDataFromRaw(rawTableData))
|
|
514
|
+
const tableRef = useRef(null)
|
|
515
|
+
|
|
516
|
+
return (
|
|
517
|
+
<TableBlockContainer>
|
|
518
|
+
<StyledTable key={entityKey} ref={tableRef}>
|
|
519
|
+
{tableData.map((rowData, rIndex) => {
|
|
520
|
+
const colsJsx = rowData.map((colData, cIndex) => {
|
|
521
|
+
return (
|
|
522
|
+
<StyledTd key={`col_${cIndex}`}>
|
|
523
|
+
{/* TODO: add editor buttons if needed */}
|
|
524
|
+
<Editor editorState={colData} readOnly />
|
|
525
|
+
</StyledTd>
|
|
526
|
+
)
|
|
527
|
+
})
|
|
528
|
+
return (
|
|
529
|
+
<React.Fragment key={`row_${rIndex}`}>
|
|
530
|
+
<StyledTr>{colsJsx}</StyledTr>
|
|
531
|
+
</React.Fragment>
|
|
532
|
+
)
|
|
533
|
+
})}
|
|
534
|
+
</StyledTable>
|
|
535
|
+
</TableBlockContainer>
|
|
536
|
+
)
|
|
537
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import styled from 'styled-components'
|
|
3
|
+
|
|
4
|
+
const AnnotatedText = styled.span`
|
|
5
|
+
vertical-align: middle;
|
|
6
|
+
color: #d0a67d;
|
|
7
|
+
|
|
8
|
+
svg {
|
|
9
|
+
vertical-align: middle;
|
|
10
|
+
margin-left: 5px;
|
|
11
|
+
}
|
|
12
|
+
`
|
|
13
|
+
|
|
14
|
+
const AnnotationBody = styled.div`
|
|
15
|
+
background-color: #f1f1f1;
|
|
16
|
+
padding: 10px;
|
|
17
|
+
margin-top: 5px;
|
|
18
|
+
margin-bottom: 10px;
|
|
19
|
+
`
|
|
20
|
+
|
|
21
|
+
function indicatorSvg(shouldRotate: boolean) {
|
|
22
|
+
const transform = shouldRotate ? 'rotate(180 10 10)' : ''
|
|
23
|
+
return (
|
|
24
|
+
<svg
|
|
25
|
+
width="20"
|
|
26
|
+
height="20"
|
|
27
|
+
viewBox="0 0 20 20"
|
|
28
|
+
fill="none"
|
|
29
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
30
|
+
>
|
|
31
|
+
<circle cx="10" cy="10" r="10" fill="#f1f1f1" />
|
|
32
|
+
<path
|
|
33
|
+
d="M10 15L5.66987 7.5L14.3301 7.5L10 15Z"
|
|
34
|
+
fill="#D0A67D"
|
|
35
|
+
transform={transform}
|
|
36
|
+
/>
|
|
37
|
+
</svg>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function AnnotationBlock(props) {
|
|
42
|
+
const { children: annotated } = props
|
|
43
|
+
const [toShowAnnotation, setToShowAnnotation] = useState(false)
|
|
44
|
+
const { bodyHTML } = props.contentState.getEntity(props.entityKey).getData()
|
|
45
|
+
return (
|
|
46
|
+
<React.Fragment>
|
|
47
|
+
<AnnotatedText>
|
|
48
|
+
{annotated}
|
|
49
|
+
<span
|
|
50
|
+
onClick={(e) => {
|
|
51
|
+
e.preventDefault()
|
|
52
|
+
setToShowAnnotation(!toShowAnnotation)
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
{toShowAnnotation ? indicatorSvg(false) : indicatorSvg(true)}
|
|
56
|
+
</span>
|
|
57
|
+
</AnnotatedText>
|
|
58
|
+
{toShowAnnotation ? (
|
|
59
|
+
<AnnotationBody
|
|
60
|
+
contentEditable={false}
|
|
61
|
+
dangerouslySetInnerHTML={{ __html: bodyHTML }}
|
|
62
|
+
></AnnotationBody>
|
|
63
|
+
) : null}
|
|
64
|
+
</React.Fragment>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function findAnnotationEntities(contentBlock, callback, contentState) {
|
|
69
|
+
contentBlock.findEntityRanges((character) => {
|
|
70
|
+
const entityKey = character.getEntity()
|
|
71
|
+
return (
|
|
72
|
+
entityKey !== null &&
|
|
73
|
+
contentState.getEntity(entityKey).getType() === 'ANNOTATION'
|
|
74
|
+
)
|
|
75
|
+
}, callback)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const annotationDecorator = {
|
|
79
|
+
strategy: findAnnotationEntities,
|
|
80
|
+
component: AnnotationBlock,
|
|
81
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import styled from 'styled-components'
|
|
3
|
+
|
|
4
|
+
const LinkWrapper = styled.a`
|
|
5
|
+
color: #3b5998;
|
|
6
|
+
text-decoration: underline;
|
|
7
|
+
`
|
|
8
|
+
|
|
9
|
+
function findLinkEntities(contentBlock, callback, contentState) {
|
|
10
|
+
contentBlock.findEntityRanges((character) => {
|
|
11
|
+
const entityKey = character.getEntity()
|
|
12
|
+
return (
|
|
13
|
+
entityKey !== null &&
|
|
14
|
+
contentState.getEntity(entityKey).getType() === 'LINK'
|
|
15
|
+
)
|
|
16
|
+
}, callback)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const linkDecorator = {
|
|
20
|
+
strategy: findLinkEntities,
|
|
21
|
+
component: Link,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function Link(props) {
|
|
25
|
+
const { url } = props.contentState.getEntity(props.entityKey).getData()
|
|
26
|
+
return <LinkWrapper href={url}>{props.children}</LinkWrapper>
|
|
27
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react'
|
|
2
|
+
import styled from 'styled-components'
|
|
3
|
+
import { Select } from '@keystone-ui/fields'
|
|
4
|
+
|
|
5
|
+
const Label = styled.label`
|
|
6
|
+
display: block;
|
|
7
|
+
margin: 10px 0;
|
|
8
|
+
font-weight: 600;
|
|
9
|
+
`
|
|
10
|
+
|
|
11
|
+
const AlignSelect = styled(Select)`
|
|
12
|
+
${({ menuHeight }) => {
|
|
13
|
+
return `margin-bottom: ${menuHeight}px;`
|
|
14
|
+
}}
|
|
15
|
+
`
|
|
16
|
+
|
|
17
|
+
type Option = { label: string; value: string; isDisabled?: boolean }
|
|
18
|
+
type AlignSelectorOnChangeFn = (param: string) => void
|
|
19
|
+
type Options = Option[]
|
|
20
|
+
|
|
21
|
+
export function AlignSelector(props: {
|
|
22
|
+
align: string
|
|
23
|
+
options: Options
|
|
24
|
+
onChange: AlignSelectorOnChangeFn
|
|
25
|
+
onOpen?: () => void
|
|
26
|
+
}) {
|
|
27
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
28
|
+
const [menuHeight, setMenuHeight] = useState(0)
|
|
29
|
+
const { align, options, onChange, onOpen } = props
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const selectMenu = document.querySelector(
|
|
33
|
+
'.css-nabggt-menu'
|
|
34
|
+
) as HTMLElement | null
|
|
35
|
+
|
|
36
|
+
if (selectMenu) {
|
|
37
|
+
const styles = window.getComputedStyle(selectMenu)
|
|
38
|
+
const margin =
|
|
39
|
+
parseFloat(styles['marginTop']) + parseFloat(styles['marginBottom'])
|
|
40
|
+
setMenuHeight(selectMenu.offsetHeight + margin)
|
|
41
|
+
} else {
|
|
42
|
+
setMenuHeight(0)
|
|
43
|
+
}
|
|
44
|
+
if (isOpen && onOpen) {
|
|
45
|
+
onOpen()
|
|
46
|
+
}
|
|
47
|
+
}, [isOpen])
|
|
48
|
+
return (
|
|
49
|
+
<React.Fragment>
|
|
50
|
+
<Label htmlFor="alignment">對齊</Label>
|
|
51
|
+
<AlignSelect
|
|
52
|
+
id="alignment"
|
|
53
|
+
// default align === undefined
|
|
54
|
+
value={options.find((option) => option.value === align)}
|
|
55
|
+
options={options}
|
|
56
|
+
onChange={(option) => {
|
|
57
|
+
onChange(option.value)
|
|
58
|
+
}}
|
|
59
|
+
onMenuOpen={() => setIsOpen(true)}
|
|
60
|
+
onMenuClose={() => setIsOpen(false)}
|
|
61
|
+
menuHeight={menuHeight}
|
|
62
|
+
/>
|
|
63
|
+
</React.Fragment>
|
|
64
|
+
)
|
|
65
|
+
}
|