@pie-lib/mask-markup 3.0.4-next.33 → 3.0.4-next.36
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/CHANGELOG.json +17 -0
- package/CHANGELOG.md +1256 -0
- package/LICENSE.md +5 -0
- package/lib/choices/choice.js +116 -0
- package/lib/choices/choice.js.map +1 -0
- package/lib/choices/index.js +103 -0
- package/lib/choices/index.js.map +1 -0
- package/lib/componentize.js +21 -0
- package/lib/componentize.js.map +1 -0
- package/lib/components/blank.js +371 -0
- package/lib/components/blank.js.map +1 -0
- package/lib/components/correct-input.js +94 -0
- package/lib/components/correct-input.js.map +1 -0
- package/lib/components/dropdown.js +483 -0
- package/lib/components/dropdown.js.map +1 -0
- package/lib/components/input.js +50 -0
- package/lib/components/input.js.map +1 -0
- package/lib/constructed-response.js +101 -0
- package/lib/constructed-response.js.map +1 -0
- package/lib/customizable.js +42 -0
- package/lib/customizable.js.map +1 -0
- package/lib/drag-in-the-blank.js +254 -0
- package/lib/drag-in-the-blank.js.map +1 -0
- package/lib/index.js +55 -0
- package/lib/index.js.map +1 -0
- package/lib/inline-dropdown.js +40 -0
- package/lib/inline-dropdown.js.map +1 -0
- package/lib/mask.js +198 -0
- package/lib/mask.js.map +1 -0
- package/lib/serialization.js +261 -0
- package/lib/serialization.js.map +1 -0
- package/lib/with-mask.js +97 -0
- package/lib/with-mask.js.map +1 -0
- package/package.json +20 -39
- package/src/__tests__/drag-in-the-blank.test.js +111 -0
- package/src/__tests__/index.test.js +38 -0
- package/src/__tests__/mask.test.js +381 -0
- package/src/__tests__/serialization.test.js +54 -0
- package/src/__tests__/utils.js +1 -0
- package/src/__tests__/with-mask.test.js +76 -0
- package/src/choices/__tests__/index.test.js +75 -0
- package/src/choices/choice.jsx +97 -0
- package/src/choices/index.jsx +64 -0
- package/src/componentize.js +13 -0
- package/src/components/__tests__/blank.test.js +199 -0
- package/src/components/__tests__/correct-input.test.js +90 -0
- package/src/components/__tests__/dropdown.test.js +129 -0
- package/src/components/__tests__/input.test.js +102 -0
- package/src/components/blank.jsx +386 -0
- package/src/components/correct-input.jsx +82 -0
- package/src/components/dropdown.jsx +423 -0
- package/src/components/input.jsx +48 -0
- package/src/constructed-response.jsx +87 -0
- package/src/customizable.jsx +34 -0
- package/src/drag-in-the-blank.jsx +241 -0
- package/src/index.js +16 -0
- package/src/inline-dropdown.jsx +29 -0
- package/src/mask.jsx +172 -0
- package/src/serialization.js +260 -0
- package/src/with-mask.jsx +75 -0
- package/dist/_virtual/_rolldown/runtime.js +0 -4
- package/dist/choices/choice.d.ts +0 -24
- package/dist/choices/choice.js +0 -77
- package/dist/choices/index.d.ts +0 -25
- package/dist/choices/index.js +0 -49
- package/dist/componentize.d.ts +0 -12
- package/dist/componentize.js +0 -4
- package/dist/components/blank.d.ts +0 -39
- package/dist/components/blank.js +0 -240
- package/dist/components/correct-input.d.ts +0 -11
- package/dist/components/dropdown.d.ts +0 -37
- package/dist/components/dropdown.js +0 -320
- package/dist/components/input.d.ts +0 -37
- package/dist/constructed-response.d.ts +0 -44
- package/dist/constructed-response.js +0 -55
- package/dist/customizable.d.ts +0 -43
- package/dist/customizable.js +0 -8
- package/dist/drag-in-the-blank.d.ts +0 -37
- package/dist/drag-in-the-blank.js +0 -164
- package/dist/index.d.ts +0 -15
- package/dist/index.js +0 -7
- package/dist/inline-dropdown.d.ts +0 -44
- package/dist/inline-dropdown.js +0 -24
- package/dist/mask.d.ts +0 -30
- package/dist/mask.js +0 -99
- package/dist/node_modules/.bun/clsx@2.1.1/node_modules/clsx/dist/clsx.js +0 -16
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/index.js +0 -17
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/cssPrefix.js +0 -9
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/cssUnitless.js +0 -26
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/hasOwn.js +0 -11
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/isFunction.js +0 -11
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/isObject.js +0 -11
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/prefixInfo.js +0 -24
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/prefixProperties.js +0 -32
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/prefixer.js +0 -29
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/stringUtils/camelize.js +0 -14
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/stringUtils/hyphenRe.js +0 -8
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/stringUtils/hyphenate.js +0 -12
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/stringUtils/separate.js +0 -11
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/stringUtils/toLowerFirst.js +0 -10
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/stringUtils/toUpperFirst.js +0 -10
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/toStyleObject.js +0 -55
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/toStyleString.js +0 -16
- package/dist/serialization.d.ts +0 -34
- package/dist/serialization.js +0 -132
- package/dist/with-mask.d.ts +0 -55
- package/dist/with-mask.js +0 -45
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { DragProvider } from '@pie-lib/drag';
|
|
4
|
+
import { DragOverlay, rectIntersection } from '@dnd-kit/core';
|
|
5
|
+
import Choices from './choices';
|
|
6
|
+
import Choice from './choices/choice';
|
|
7
|
+
import Blank from './components/blank';
|
|
8
|
+
import { withMask } from './with-mask';
|
|
9
|
+
|
|
10
|
+
const Masked = withMask('blank', (props) => (node, data, onChange) => {
|
|
11
|
+
const dataset = node.data?.dataset || {};
|
|
12
|
+
if (dataset.component === 'blank') {
|
|
13
|
+
// eslint-disable-next-line react/prop-types
|
|
14
|
+
const {
|
|
15
|
+
disabled,
|
|
16
|
+
duplicates,
|
|
17
|
+
correctResponse,
|
|
18
|
+
feedback,
|
|
19
|
+
showCorrectAnswer,
|
|
20
|
+
emptyResponseAreaWidth,
|
|
21
|
+
emptyResponseAreaHeight,
|
|
22
|
+
instanceId,
|
|
23
|
+
isDragging,
|
|
24
|
+
} = props;
|
|
25
|
+
const choiceId = showCorrectAnswer ? correctResponse[dataset.id] : data[dataset.id];
|
|
26
|
+
// eslint-disable-next-line react/prop-types
|
|
27
|
+
const choice = choiceId && props.choices.find((c) => c.id === choiceId);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Blank
|
|
31
|
+
key={`${node.type}-${dataset.id}`}
|
|
32
|
+
correct={showCorrectAnswer || (feedback && feedback[dataset.id])}
|
|
33
|
+
disabled={disabled}
|
|
34
|
+
duplicates={duplicates}
|
|
35
|
+
choice={choice}
|
|
36
|
+
id={dataset.id}
|
|
37
|
+
emptyResponseAreaWidth={emptyResponseAreaWidth}
|
|
38
|
+
emptyResponseAreaHeight={emptyResponseAreaHeight}
|
|
39
|
+
onChange={(id, choiceId) => {
|
|
40
|
+
const newData = { ...data };
|
|
41
|
+
if (choiceId === undefined) {
|
|
42
|
+
delete newData[id];
|
|
43
|
+
} else {
|
|
44
|
+
newData[id] = choiceId;
|
|
45
|
+
}
|
|
46
|
+
onChange(newData);
|
|
47
|
+
}}
|
|
48
|
+
instanceId={instanceId}
|
|
49
|
+
isDragging={isDragging}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export default class DragInTheBlank extends React.Component {
|
|
56
|
+
constructor(props) {
|
|
57
|
+
super(props);
|
|
58
|
+
this.state = {
|
|
59
|
+
activeDragItem: null,
|
|
60
|
+
dropAnimation: undefined,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static propTypes = {
|
|
65
|
+
markup: PropTypes.string,
|
|
66
|
+
layout: PropTypes.object,
|
|
67
|
+
choicesPosition: PropTypes.string,
|
|
68
|
+
choices: PropTypes.array,
|
|
69
|
+
value: PropTypes.object,
|
|
70
|
+
onChange: PropTypes.func,
|
|
71
|
+
duplicates: PropTypes.bool,
|
|
72
|
+
disabled: PropTypes.bool,
|
|
73
|
+
feedback: PropTypes.object,
|
|
74
|
+
correctResponse: PropTypes.object,
|
|
75
|
+
showCorrectAnswer: PropTypes.bool,
|
|
76
|
+
emptyResponseAreaWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
77
|
+
emptyResponseAreaHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
78
|
+
instanceId: PropTypes.string,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
static defaultProps = {
|
|
82
|
+
instanceId: 'drag-in-the-blank',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
handleDragStart = (event) => {
|
|
86
|
+
const { active } = event;
|
|
87
|
+
|
|
88
|
+
if (active?.data?.current) {
|
|
89
|
+
this.setState({
|
|
90
|
+
activeDragItem: active.data.current,
|
|
91
|
+
dropAnimation: undefined, // default during drag
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
renderDragOverlay = () => {
|
|
97
|
+
const { activeDragItem } = this.state;
|
|
98
|
+
if (!activeDragItem) return null;
|
|
99
|
+
|
|
100
|
+
if (activeDragItem.type === 'MaskBlank') {
|
|
101
|
+
return (
|
|
102
|
+
<Choice
|
|
103
|
+
disabled={activeDragItem.disabled}
|
|
104
|
+
choice={activeDragItem.choice}
|
|
105
|
+
instanceId={activeDragItem.instanceId}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return null;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
handleDragEnd = (event) => {
|
|
114
|
+
const { active, over } = event;
|
|
115
|
+
const { onChange, value } = this.props;
|
|
116
|
+
|
|
117
|
+
const draggedData = active?.data?.current;
|
|
118
|
+
const dropData = over?.data?.current;
|
|
119
|
+
|
|
120
|
+
const isValidDrop =
|
|
121
|
+
!!active && !!over && draggedData?.type === 'MaskBlank' && dropData?.accepts?.includes('MaskBlank');
|
|
122
|
+
|
|
123
|
+
// Only animate back when drop is invalid
|
|
124
|
+
this.setState({
|
|
125
|
+
activeDragItem: null,
|
|
126
|
+
dropAnimation: isValidDrop ? null : undefined,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (!isValidDrop || !onChange) return;
|
|
130
|
+
|
|
131
|
+
const draggedItem = draggedData;
|
|
132
|
+
const targetId = dropData.id;
|
|
133
|
+
|
|
134
|
+
if (dropData.toChoiceBoard === true) {
|
|
135
|
+
if (!draggedItem.fromChoice && draggedItem.id) {
|
|
136
|
+
const newValue = { ...value };
|
|
137
|
+
delete newValue[draggedItem.id];
|
|
138
|
+
onChange(newValue);
|
|
139
|
+
}
|
|
140
|
+
} else if (draggedItem.fromChoice === true) {
|
|
141
|
+
if (targetId && targetId !== 'drag-in-the-blank-droppable') {
|
|
142
|
+
const newValue = { ...value };
|
|
143
|
+
newValue[targetId] = draggedItem.choice.id;
|
|
144
|
+
onChange(newValue);
|
|
145
|
+
}
|
|
146
|
+
} else if (draggedItem.id && draggedItem.id !== targetId) {
|
|
147
|
+
const newValue = { ...value };
|
|
148
|
+
newValue[targetId] = draggedItem.choice.id;
|
|
149
|
+
delete newValue[draggedItem.id];
|
|
150
|
+
onChange(newValue);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
getPositionDirection = (choicePosition) => {
|
|
155
|
+
let flexDirection;
|
|
156
|
+
let justifyContent;
|
|
157
|
+
let alignItems;
|
|
158
|
+
|
|
159
|
+
switch (choicePosition) {
|
|
160
|
+
case 'left':
|
|
161
|
+
flexDirection = 'row';
|
|
162
|
+
alignItems = 'center';
|
|
163
|
+
break;
|
|
164
|
+
case 'right':
|
|
165
|
+
flexDirection = 'row-reverse';
|
|
166
|
+
justifyContent = 'flex-end';
|
|
167
|
+
alignItems = 'center';
|
|
168
|
+
break;
|
|
169
|
+
case 'below':
|
|
170
|
+
flexDirection = 'column-reverse';
|
|
171
|
+
break;
|
|
172
|
+
default:
|
|
173
|
+
// above
|
|
174
|
+
flexDirection = 'column';
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { flexDirection, justifyContent, alignItems };
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
render() {
|
|
182
|
+
const {
|
|
183
|
+
markup,
|
|
184
|
+
duplicates,
|
|
185
|
+
value,
|
|
186
|
+
onChange,
|
|
187
|
+
choicesPosition,
|
|
188
|
+
choices,
|
|
189
|
+
correctResponse,
|
|
190
|
+
disabled,
|
|
191
|
+
feedback,
|
|
192
|
+
showCorrectAnswer,
|
|
193
|
+
emptyResponseAreaWidth,
|
|
194
|
+
emptyResponseAreaHeight,
|
|
195
|
+
layout,
|
|
196
|
+
instanceId,
|
|
197
|
+
} = this.props;
|
|
198
|
+
|
|
199
|
+
const choicePosition = choicesPosition || 'below';
|
|
200
|
+
const style = { display: 'flex', minWidth: '100px', ...this.getPositionDirection(choicePosition) };
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<DragProvider
|
|
204
|
+
onDragStart={this.handleDragStart}
|
|
205
|
+
onDragEnd={this.handleDragEnd}
|
|
206
|
+
collisionDetection={rectIntersection}
|
|
207
|
+
>
|
|
208
|
+
<div ref={(ref) => (this.rootRef = ref)} style={style}>
|
|
209
|
+
<Choices
|
|
210
|
+
choicePosition={choicePosition}
|
|
211
|
+
choices={choices}
|
|
212
|
+
value={value}
|
|
213
|
+
duplicates={duplicates}
|
|
214
|
+
disabled={disabled}
|
|
215
|
+
instanceId={instanceId}
|
|
216
|
+
/>
|
|
217
|
+
<Masked
|
|
218
|
+
elementType="drag-in-the-blank"
|
|
219
|
+
markup={markup}
|
|
220
|
+
layout={layout}
|
|
221
|
+
value={value}
|
|
222
|
+
choices={choices}
|
|
223
|
+
onChange={onChange}
|
|
224
|
+
disabled={disabled}
|
|
225
|
+
duplicates={duplicates}
|
|
226
|
+
feedback={feedback}
|
|
227
|
+
correctResponse={correctResponse}
|
|
228
|
+
showCorrectAnswer={showCorrectAnswer}
|
|
229
|
+
emptyResponseAreaWidth={emptyResponseAreaWidth}
|
|
230
|
+
emptyResponseAreaHeight={emptyResponseAreaHeight}
|
|
231
|
+
instanceId={instanceId}
|
|
232
|
+
isDragging={!!this.state.activeDragItem}
|
|
233
|
+
/>
|
|
234
|
+
<DragOverlay style={{ pointerEvents: 'none' }} dropAnimation={this.state.dropAnimation}>
|
|
235
|
+
{this.renderDragOverlay()}
|
|
236
|
+
</DragOverlay>
|
|
237
|
+
</div>
|
|
238
|
+
</DragProvider>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { buildLayoutFromMarkup, withMask } from './with-mask';
|
|
2
|
+
import DragInTheBlank from './drag-in-the-blank';
|
|
3
|
+
import ConstructedResponse from './constructed-response';
|
|
4
|
+
import Customizable from './customizable';
|
|
5
|
+
import InlineDropdown from './inline-dropdown';
|
|
6
|
+
import componentize from './componentize';
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
withMask,
|
|
10
|
+
buildLayoutFromMarkup,
|
|
11
|
+
DragInTheBlank,
|
|
12
|
+
ConstructedResponse,
|
|
13
|
+
InlineDropdown,
|
|
14
|
+
componentize,
|
|
15
|
+
Customizable,
|
|
16
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Dropdown from './components/dropdown';
|
|
3
|
+
import { withMask } from './with-mask';
|
|
4
|
+
|
|
5
|
+
// eslint-disable-next-line react/display-name
|
|
6
|
+
export default withMask('dropdown', (props) => (node, data, onChange) => {
|
|
7
|
+
const dataset = node.data ? node.data.dataset || {} : {};
|
|
8
|
+
if (dataset.component === 'dropdown') {
|
|
9
|
+
// eslint-disable-next-line react/prop-types
|
|
10
|
+
const { choices, disabled, feedback, showCorrectAnswer } = props;
|
|
11
|
+
const correctAnswer = choices && choices[dataset.id] && choices[dataset.id].find((c) => c.correct);
|
|
12
|
+
const finalChoice = showCorrectAnswer ? correctAnswer && correctAnswer.value : data[dataset.id];
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Dropdown
|
|
16
|
+
key={`${node.type}-dropdown-${dataset.id}`}
|
|
17
|
+
correct={feedback && feedback[dataset.id] && feedback[dataset.id] === 'correct'}
|
|
18
|
+
disabled={disabled || showCorrectAnswer}
|
|
19
|
+
value={finalChoice}
|
|
20
|
+
correctValue={showCorrectAnswer ? correctAnswer && correctAnswer.label : undefined}
|
|
21
|
+
id={dataset.id}
|
|
22
|
+
onChange={onChange}
|
|
23
|
+
choices={choices[dataset.id]}
|
|
24
|
+
showCorrectAnswer={showCorrectAnswer}
|
|
25
|
+
singleQuery={Object.keys(choices).length == 1}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
});
|
package/src/mask.jsx
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { get } from 'lodash-es';
|
|
4
|
+
import { styled } from '@mui/material/styles';
|
|
5
|
+
import { renderMath } from '@pie-lib/math-rendering';
|
|
6
|
+
import { MARK_TAGS } from './serialization';
|
|
7
|
+
|
|
8
|
+
const Paragraph = styled('div')(({ theme }) => ({
|
|
9
|
+
paddingTop: theme.spacing(0.5),
|
|
10
|
+
paddingBottom: theme.spacing(0.5),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const Spacer = styled('span')(() => ({
|
|
14
|
+
display: 'inline-block',
|
|
15
|
+
width: '.75em',
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
const restrictWhitespaceTypes = ['tbody', 'tr'];
|
|
19
|
+
|
|
20
|
+
const addText = (parentNode, text) => {
|
|
21
|
+
const isWhitespace = text.trim() === '';
|
|
22
|
+
const parentType = parentNode && parentNode.type;
|
|
23
|
+
|
|
24
|
+
if (isWhitespace && restrictWhitespaceTypes.includes(parentType)) {
|
|
25
|
+
return undefined;
|
|
26
|
+
} else {
|
|
27
|
+
return text;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getMark = (n) => {
|
|
32
|
+
const mark = n.leaves.find((leave) => get(leave, 'marks', []).length);
|
|
33
|
+
|
|
34
|
+
if (mark) {
|
|
35
|
+
return mark.marks[0];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const renderChildren = (layout, value, onChange, rootRenderChildren, parentNode, elementType) => {
|
|
42
|
+
if (!value) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const children = [];
|
|
47
|
+
|
|
48
|
+
(layout.nodes || []).forEach((n, index) => {
|
|
49
|
+
const key = n.type ? `${n.type}-${index}` : `${index}`;
|
|
50
|
+
|
|
51
|
+
if (n.isMath) {
|
|
52
|
+
children.push(
|
|
53
|
+
<span
|
|
54
|
+
dangerouslySetInnerHTML={{
|
|
55
|
+
__html: `<math displaystyle="true">${n.nodes[0].innerHTML}</math>`,
|
|
56
|
+
}}
|
|
57
|
+
/>,
|
|
58
|
+
);
|
|
59
|
+
return children;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (rootRenderChildren) {
|
|
63
|
+
const c = rootRenderChildren(n, value, onChange);
|
|
64
|
+
if (c) {
|
|
65
|
+
const isDndComponent = n.data?.dataset?.component === 'blank';
|
|
66
|
+
|
|
67
|
+
if (isDndComponent) {
|
|
68
|
+
children.push(<Spacer key={`spacer-${index}`} />);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
children.push(c);
|
|
72
|
+
|
|
73
|
+
if (isDndComponent) {
|
|
74
|
+
children.push(<Spacer key={`spacer-${index}`} />);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (n.object === 'text') {
|
|
81
|
+
const content = n.leaves.reduce((acc, l) => {
|
|
82
|
+
const t = l.text;
|
|
83
|
+
const extraText = addText(parentNode, t);
|
|
84
|
+
return extraText ? acc + extraText : acc;
|
|
85
|
+
}, '');
|
|
86
|
+
const mark = getMark(n);
|
|
87
|
+
|
|
88
|
+
if (mark) {
|
|
89
|
+
let markKey;
|
|
90
|
+
|
|
91
|
+
for (markKey in MARK_TAGS) {
|
|
92
|
+
if (MARK_TAGS[markKey] === mark.type) {
|
|
93
|
+
const Tag = markKey;
|
|
94
|
+
|
|
95
|
+
children.push(<Tag key={key}>{content}</Tag>);
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} else if (content.length > 0) {
|
|
100
|
+
children.push(content);
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
const subNodes = renderChildren(n, value, onChange, rootRenderChildren, n, elementType);
|
|
104
|
+
if (n.type === 'p' || n.type === 'paragraph') {
|
|
105
|
+
children.push(<Paragraph key={key}>{subNodes}</Paragraph>);
|
|
106
|
+
} else {
|
|
107
|
+
const Tag = n.type;
|
|
108
|
+
if (n.nodes && n.nodes.length > 0) {
|
|
109
|
+
children.push(
|
|
110
|
+
<Tag key={key} {...n.data.attributes}>
|
|
111
|
+
{subNodes}
|
|
112
|
+
</Tag>,
|
|
113
|
+
);
|
|
114
|
+
} else {
|
|
115
|
+
children.push(<Tag key={key} {...n.data.attributes} />);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
return children;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const MaskContainer = styled('div')(() => ({
|
|
124
|
+
display: 'initial',
|
|
125
|
+
'&:not(.MathJax) table': {
|
|
126
|
+
borderCollapse: 'collapse',
|
|
127
|
+
},
|
|
128
|
+
// align table content to left as per STAR requirement PD-3687
|
|
129
|
+
'&:not(.MathJax) table td, &:not(.MathJax) table th': {
|
|
130
|
+
padding: '8px 12px',
|
|
131
|
+
textAlign: 'left',
|
|
132
|
+
},
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Renders a layout that uses the slate.js Value model structure.
|
|
137
|
+
*/
|
|
138
|
+
export default class Mask extends React.Component {
|
|
139
|
+
constructor(props) {
|
|
140
|
+
super(props);
|
|
141
|
+
this.internalContainerRef = React.createRef();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static propTypes = {
|
|
145
|
+
renderChildren: PropTypes.func,
|
|
146
|
+
layout: PropTypes.object,
|
|
147
|
+
value: PropTypes.object,
|
|
148
|
+
onChange: PropTypes.func,
|
|
149
|
+
elementType: PropTypes.string,
|
|
150
|
+
containerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.instanceOf(Element) })]),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
componentDidMount() {
|
|
154
|
+
const containerRef = this.props.containerRef || this.internalContainerRef;
|
|
155
|
+
if (containerRef.current && typeof renderMath === 'function') {
|
|
156
|
+
renderMath(containerRef.current);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
handleChange = (id, value) => {
|
|
161
|
+
const data = { ...this.props.value, [id]: value };
|
|
162
|
+
this.props.onChange(data);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
render() {
|
|
166
|
+
const { value, layout, elementType, containerRef } = this.props;
|
|
167
|
+
const children = renderChildren(layout, value, this.handleChange, this.props.renderChildren, null, elementType);
|
|
168
|
+
const ref = containerRef || this.internalContainerRef;
|
|
169
|
+
|
|
170
|
+
return <MaskContainer ref={ref}>{children}</MaskContainer>;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { object as toStyleObject } from 'to-style';
|
|
2
|
+
import debug from 'debug';
|
|
3
|
+
|
|
4
|
+
const log = debug('@pie-lib:mask-markup:serialization');
|
|
5
|
+
|
|
6
|
+
const INLINE = ['span'];
|
|
7
|
+
const MARK = ['em', 'strong', 'u'];
|
|
8
|
+
const TEXT_NODE = 3;
|
|
9
|
+
const COMMENT_NODE = 8;
|
|
10
|
+
const ELEMENT_NODE = 1;
|
|
11
|
+
|
|
12
|
+
const attr = (el) => {
|
|
13
|
+
if (!el.attributes || el.attributes.length <= 0) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const out = {};
|
|
18
|
+
let i;
|
|
19
|
+
|
|
20
|
+
for (i = 0; i < el.attributes.length; i++) {
|
|
21
|
+
const a = el.attributes[i];
|
|
22
|
+
|
|
23
|
+
out[a.name] = a.value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return out;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const getObject = (type) => {
|
|
30
|
+
if (INLINE.includes(type)) {
|
|
31
|
+
return 'inline';
|
|
32
|
+
} else if (MARK.includes(type)) {
|
|
33
|
+
return 'mark';
|
|
34
|
+
}
|
|
35
|
+
return 'block';
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const parseStyleString = (s) => {
|
|
39
|
+
const regex = /([\w-]*)\s*:\s*([^;]*)/g;
|
|
40
|
+
let match;
|
|
41
|
+
const result = {};
|
|
42
|
+
while ((match = regex.exec(s))) {
|
|
43
|
+
result[match[1]] = match[2].trim();
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const reactAttributes = (o) => toStyleObject(o, { camelize: true, addUnits: false });
|
|
49
|
+
|
|
50
|
+
const handleStyles = (el, attribute) => {
|
|
51
|
+
const styleString = el.getAttribute(attribute);
|
|
52
|
+
|
|
53
|
+
return reactAttributes(parseStyleString(styleString));
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleClass = (el, acc, attribute) => {
|
|
57
|
+
const classNames = el.getAttribute(attribute);
|
|
58
|
+
|
|
59
|
+
delete acc.class;
|
|
60
|
+
|
|
61
|
+
return classNames;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const attributesToMap = (el) => (acc, attribute) => {
|
|
65
|
+
if (!el.getAttribute) {
|
|
66
|
+
return acc;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const value = el.getAttribute(attribute);
|
|
70
|
+
|
|
71
|
+
if (value) {
|
|
72
|
+
switch (attribute) {
|
|
73
|
+
case 'style':
|
|
74
|
+
acc.style = handleStyles(el, attribute);
|
|
75
|
+
break;
|
|
76
|
+
case 'class':
|
|
77
|
+
acc.className = handleClass(el, acc, attribute);
|
|
78
|
+
break;
|
|
79
|
+
default:
|
|
80
|
+
acc[attribute] = el.getAttribute(attribute);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return acc;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const attributes = ['border', 'class', 'style'];
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Tags to marks.
|
|
92
|
+
*
|
|
93
|
+
* @type {Object}
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
export const MARK_TAGS = {
|
|
97
|
+
b: 'bold',
|
|
98
|
+
em: 'italic',
|
|
99
|
+
u: 'underline',
|
|
100
|
+
s: 'strikethrough',
|
|
101
|
+
code: 'code',
|
|
102
|
+
strong: 'strong',
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Recursively process DOM nodes and convert them to Slate JSON format
|
|
107
|
+
*/
|
|
108
|
+
const processNode = (node, marks = []) => {
|
|
109
|
+
// Skip comment nodes
|
|
110
|
+
if (node.nodeType === COMMENT_NODE) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Handle text nodes
|
|
115
|
+
if (node.nodeType === TEXT_NODE) {
|
|
116
|
+
const text = node.textContent;
|
|
117
|
+
const leaf = { text };
|
|
118
|
+
|
|
119
|
+
if (marks.length > 0) {
|
|
120
|
+
leaf.marks = marks.map((m) => ({ type: m, data: undefined }));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
object: 'text',
|
|
125
|
+
leaves: [leaf],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Handle element nodes
|
|
130
|
+
if (node.nodeType === ELEMENT_NODE) {
|
|
131
|
+
const type = node.tagName.toLowerCase();
|
|
132
|
+
|
|
133
|
+
// Check if this is a mark tag
|
|
134
|
+
const markType = MARK_TAGS[type];
|
|
135
|
+
if (markType) {
|
|
136
|
+
log('[deserialize] mark: ', markType);
|
|
137
|
+
// Process children with this mark added and return them flattened
|
|
138
|
+
const childNodes = processNodes(node.childNodes, [...marks, markType]);
|
|
139
|
+
// Return an array indicator with the nodes (will be flattened by parent)
|
|
140
|
+
return { _flatten: true, nodes: childNodes };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Handle math elements specially
|
|
144
|
+
if (type === 'math') {
|
|
145
|
+
return {
|
|
146
|
+
isMath: true,
|
|
147
|
+
nodes: [node],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Process regular elements
|
|
152
|
+
const normalAttrs = attr(node) || {};
|
|
153
|
+
|
|
154
|
+
if (type === 'audio' && normalAttrs.controls === '') {
|
|
155
|
+
normalAttrs.controls = true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const allAttrs = attributes.reduce(attributesToMap(node), { ...normalAttrs });
|
|
159
|
+
const object = getObject(type);
|
|
160
|
+
|
|
161
|
+
const childNodes = processNodes(node.childNodes, marks);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
object,
|
|
165
|
+
type,
|
|
166
|
+
data: { dataset: { ...node.dataset }, attributes: { ...allAttrs } },
|
|
167
|
+
nodes: childNodes,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return null;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Process a NodeList and convert to array of Slate nodes
|
|
176
|
+
*/
|
|
177
|
+
const processNodes = (nodeList, marks = []) => {
|
|
178
|
+
const nodes = [];
|
|
179
|
+
for (let i = 0; i < nodeList.length; i++) {
|
|
180
|
+
const result = processNode(nodeList[i], marks);
|
|
181
|
+
if (result !== null) {
|
|
182
|
+
// Handle flattening for mark nodes
|
|
183
|
+
if (result._flatten && result.nodes) {
|
|
184
|
+
nodes.push(...result.nodes);
|
|
185
|
+
} else {
|
|
186
|
+
nodes.push(result);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return nodes;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Deserialize HTML string to Slate JSON format
|
|
195
|
+
*/
|
|
196
|
+
export const deserialize = (htmlString) => {
|
|
197
|
+
// Handle empty or whitespace-only strings
|
|
198
|
+
if (!htmlString || !htmlString.trim()) {
|
|
199
|
+
return {
|
|
200
|
+
object: 'value',
|
|
201
|
+
document: {
|
|
202
|
+
object: 'document',
|
|
203
|
+
data: {},
|
|
204
|
+
nodes: [
|
|
205
|
+
{
|
|
206
|
+
object: 'block',
|
|
207
|
+
type: 'span',
|
|
208
|
+
data: {},
|
|
209
|
+
isVoid: false,
|
|
210
|
+
nodes: [],
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Use DOMParser to parse the HTML
|
|
218
|
+
const parser = new DOMParser();
|
|
219
|
+
const doc = parser.parseFromString(htmlString, 'text/html');
|
|
220
|
+
|
|
221
|
+
// Process all nodes in the body
|
|
222
|
+
let nodes = processNodes(doc.body.childNodes);
|
|
223
|
+
|
|
224
|
+
// If we only have text nodes (no block elements), wrap in default span block
|
|
225
|
+
const hasBlockElements = nodes.some((node) => node.object === 'block' || node.object === 'inline');
|
|
226
|
+
|
|
227
|
+
if (!hasBlockElements && nodes.length > 0) {
|
|
228
|
+
nodes = [
|
|
229
|
+
{
|
|
230
|
+
object: 'block',
|
|
231
|
+
type: 'span',
|
|
232
|
+
data: {},
|
|
233
|
+
isVoid: false,
|
|
234
|
+
nodes: nodes,
|
|
235
|
+
},
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// If no nodes were produced, add a default span block
|
|
240
|
+
if (nodes.length === 0) {
|
|
241
|
+
nodes = [
|
|
242
|
+
{
|
|
243
|
+
object: 'block',
|
|
244
|
+
type: 'span',
|
|
245
|
+
data: {},
|
|
246
|
+
isVoid: false,
|
|
247
|
+
nodes: [],
|
|
248
|
+
},
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
object: 'value',
|
|
254
|
+
document: {
|
|
255
|
+
object: 'document',
|
|
256
|
+
data: {},
|
|
257
|
+
nodes,
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
};
|