@pie-lib/editable-html-tip-tap 1.2.0-next.8 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +178 -0
- package/lib/components/CharacterPicker.js +1 -0
- package/lib/components/CharacterPicker.js.map +1 -1
- package/lib/components/EditableHtml.js +84 -43
- package/lib/components/EditableHtml.js.map +1 -1
- package/lib/components/MenuBar.js +74 -43
- package/lib/components/MenuBar.js.map +1 -1
- package/lib/components/TiptapContainer.js +9 -8
- package/lib/components/TiptapContainer.js.map +1 -1
- package/lib/components/icons/TextAlign.js +2 -2
- package/lib/components/icons/TextAlign.js.map +1 -1
- package/lib/components/image/InsertImageHandler.js +10 -13
- package/lib/components/image/InsertImageHandler.js.map +1 -1
- package/lib/components/media/MediaDialog.js.map +1 -1
- package/lib/components/respArea/DragInTheBlank/DragInTheBlank.js +6 -1
- package/lib/components/respArea/DragInTheBlank/DragInTheBlank.js.map +1 -1
- package/lib/components/respArea/DragInTheBlank/choice.js +15 -7
- package/lib/components/respArea/DragInTheBlank/choice.js.map +1 -1
- package/lib/components/respArea/ExplicitConstructedResponse.js +29 -11
- package/lib/components/respArea/ExplicitConstructedResponse.js.map +1 -1
- package/lib/components/respArea/InlineDropdown.js +35 -6
- package/lib/components/respArea/InlineDropdown.js.map +1 -1
- package/lib/components/respArea/MathTemplated.js +130 -0
- package/lib/components/respArea/MathTemplated.js.map +1 -0
- package/lib/extensions/custom-toolbar-wrapper.js +3 -2
- package/lib/extensions/custom-toolbar-wrapper.js.map +1 -1
- package/lib/extensions/div-node.js +83 -0
- package/lib/extensions/div-node.js.map +1 -0
- package/lib/extensions/ensure-empty-root-div.js +48 -0
- package/lib/extensions/ensure-empty-root-div.js.map +1 -0
- package/lib/extensions/ensure-list-item-content-is-div.js +64 -0
- package/lib/extensions/ensure-list-item-content-is-div.js.map +1 -0
- package/lib/extensions/extended-list-item.js +15 -0
- package/lib/extensions/extended-list-item.js.map +1 -0
- package/lib/extensions/extended-table-cell.js +22 -0
- package/lib/extensions/extended-table-cell.js.map +1 -0
- package/lib/extensions/extended-table.js +50 -1
- package/lib/extensions/extended-table.js.map +1 -1
- package/lib/extensions/image-component.js +102 -51
- package/lib/extensions/image-component.js.map +1 -1
- package/lib/extensions/image.js +51 -2
- package/lib/extensions/image.js.map +1 -1
- package/lib/extensions/math.js +50 -9
- package/lib/extensions/math.js.map +1 -1
- package/lib/extensions/media.js +3 -1
- package/lib/extensions/media.js.map +1 -1
- package/lib/extensions/responseArea.js +22 -13
- package/lib/extensions/responseArea.js.map +1 -1
- package/lib/styles/editorContainerStyles.js +5 -4
- package/lib/styles/editorContainerStyles.js.map +1 -1
- package/lib/utils/helper.js +17 -0
- package/lib/utils/helper.js.map +1 -0
- package/package.json +8 -8
- package/src/__tests__/EditableHtml.test.jsx +93 -7
- package/src/__tests__/index.test.jsx +11 -3
- package/src/components/CharacterPicker.jsx +1 -0
- package/src/components/EditableHtml.jsx +93 -41
- package/src/components/MenuBar.jsx +57 -24
- package/src/components/TiptapContainer.jsx +10 -8
- package/src/components/__tests__/CharacterPicker.test.jsx +22 -0
- package/src/components/__tests__/ExplicitConstructedResponse.test.jsx +55 -12
- package/src/components/__tests__/InlineDropdown.test.jsx +203 -10
- package/src/components/__tests__/InsertImageHandler.test.js +28 -21
- package/src/components/__tests__/MenuBar.test.jsx +32 -0
- package/src/components/icons/TextAlign.jsx +1 -1
- package/src/components/image/InsertImageHandler.js +9 -13
- package/src/components/respArea/DragInTheBlank/DragInTheBlank.jsx +6 -1
- package/src/components/respArea/DragInTheBlank/choice.jsx +32 -4
- package/src/components/respArea/ExplicitConstructedResponse.jsx +33 -10
- package/src/components/respArea/InlineDropdown.jsx +45 -10
- package/src/components/respArea/MathTemplated.jsx +124 -0
- package/src/components/respArea/__tests__/MathTemplated.test.jsx +210 -0
- package/src/extensions/__tests__/divNode.test.js +87 -0
- package/src/extensions/__tests__/ensure-empty-root-div.test.js +57 -0
- package/src/extensions/__tests__/ensure-list-item-content-is-div.test.js +44 -0
- package/src/extensions/__tests__/extended-list-item.test.js +13 -0
- package/src/extensions/__tests__/extended-table-cell.test.js +22 -0
- package/src/extensions/__tests__/extended-table.test.js +98 -1
- package/src/extensions/__tests__/image-component.test.jsx +105 -9
- package/src/extensions/__tests__/image.test.js +109 -8
- package/src/extensions/__tests__/math.test.js +348 -0
- package/src/extensions/__tests__/media-node-view.test.jsx +10 -8
- package/src/extensions/__tests__/responseArea.test.js +291 -0
- package/src/extensions/custom-toolbar-wrapper.jsx +2 -2
- package/src/extensions/div-node.js +86 -0
- package/src/extensions/ensure-empty-root-div.js +47 -0
- package/src/extensions/ensure-list-item-content-is-div.js +62 -0
- package/src/extensions/extended-list-item.js +10 -0
- package/src/extensions/extended-table-cell.js +19 -0
- package/src/extensions/extended-table.js +37 -1
- package/src/extensions/image-component.jsx +114 -69
- package/src/extensions/image.js +56 -1
- package/src/extensions/math.js +62 -10
- package/src/extensions/media.js +1 -1
- package/src/extensions/responseArea.js +15 -12
- package/src/styles/editorContainerStyles.js +5 -4
- package/src/utils/helper.js +17 -0
- /package/src/components/media/{MediaDialog.js → MediaDialog.jsx} +0 -0
|
@@ -6,13 +6,14 @@ describe('InsertImageHandler', () => {
|
|
|
6
6
|
nodeSize: 1,
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
+
const NODE_POS = 5;
|
|
10
|
+
|
|
11
|
+
const nodeInfo = (node = mockNode, pos = NODE_POS) => [node, pos];
|
|
12
|
+
|
|
9
13
|
const createMockEditor = () => ({
|
|
14
|
+
_insertingImage: true,
|
|
10
15
|
state: {
|
|
11
|
-
doc: {
|
|
12
|
-
descendants: jest.fn((callback) => {
|
|
13
|
-
callback(mockNode, 5);
|
|
14
|
-
}),
|
|
15
|
-
},
|
|
16
|
+
doc: {},
|
|
16
17
|
tr: {
|
|
17
18
|
setNodeMarkup: jest.fn((pos, type, attrs) => ({ setNodeMarkup: jest.fn() })),
|
|
18
19
|
delete: jest.fn((from, to) => ({ delete: jest.fn() })),
|
|
@@ -31,36 +32,39 @@ describe('InsertImageHandler', () => {
|
|
|
31
32
|
|
|
32
33
|
it('creates handler instance', () => {
|
|
33
34
|
const editor = createMockEditor();
|
|
34
|
-
const
|
|
35
|
+
const info = nodeInfo();
|
|
36
|
+
const handler = new InsertImageHandler(editor, info, mockOnFinish);
|
|
35
37
|
expect(handler).toBeDefined();
|
|
36
38
|
expect(handler.editor).toBe(editor);
|
|
39
|
+
expect(handler.nodeInfo).toBe(info);
|
|
37
40
|
expect(handler.node).toBe(mockNode);
|
|
38
41
|
expect(handler.onFinish).toBe(mockOnFinish);
|
|
39
42
|
});
|
|
40
43
|
|
|
41
|
-
it('stores nodePos from
|
|
44
|
+
it('stores nodePos from nodeInfo tuple', () => {
|
|
42
45
|
const editor = createMockEditor();
|
|
43
|
-
const handler = new InsertImageHandler(editor, mockNode, mockOnFinish);
|
|
44
|
-
expect(handler.nodePos).toBe(
|
|
46
|
+
const handler = new InsertImageHandler(editor, nodeInfo(mockNode, 42), mockOnFinish);
|
|
47
|
+
expect(handler.nodePos).toBe(42);
|
|
45
48
|
});
|
|
46
49
|
|
|
47
50
|
it('stores isPasted parameter', () => {
|
|
48
51
|
const editor = createMockEditor();
|
|
49
|
-
const handler = new InsertImageHandler(editor,
|
|
52
|
+
const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish, true);
|
|
50
53
|
expect(handler.isPasted).toBe(true);
|
|
51
54
|
});
|
|
52
55
|
|
|
53
56
|
it('defaults isPasted to false', () => {
|
|
54
57
|
const editor = createMockEditor();
|
|
55
|
-
const handler = new InsertImageHandler(editor,
|
|
58
|
+
const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
|
|
56
59
|
expect(handler.isPasted).toBe(false);
|
|
57
60
|
});
|
|
58
61
|
|
|
59
62
|
it('cancel deletes node and calls onFinish', () => {
|
|
60
63
|
const editor = createMockEditor();
|
|
61
|
-
const handler = new InsertImageHandler(editor,
|
|
64
|
+
const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
|
|
62
65
|
handler.cancel();
|
|
63
66
|
expect(mockOnFinish).toHaveBeenCalledWith(false);
|
|
67
|
+
expect(editor._insertingImage).toBe(false);
|
|
64
68
|
});
|
|
65
69
|
|
|
66
70
|
it('updateNode dispatches transaction with new attributes', () => {
|
|
@@ -68,22 +72,23 @@ describe('InsertImageHandler', () => {
|
|
|
68
72
|
const mockNodeAt = jest.fn(() => ({ attrs: { existing: 'value' } }));
|
|
69
73
|
editor.state.doc.nodeAt = mockNodeAt;
|
|
70
74
|
|
|
71
|
-
const handler = new InsertImageHandler(editor,
|
|
75
|
+
const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
|
|
72
76
|
handler.updateNode({ newAttr: 'newValue' });
|
|
73
77
|
|
|
74
|
-
expect(mockNodeAt).toHaveBeenCalledWith(
|
|
78
|
+
expect(mockNodeAt).toHaveBeenCalledWith(NODE_POS);
|
|
75
79
|
expect(editor.view.dispatch).toHaveBeenCalled();
|
|
76
80
|
});
|
|
77
81
|
|
|
78
82
|
it('done calls onFinish with false on error', () => {
|
|
79
83
|
const editor = createMockEditor();
|
|
80
|
-
const handler = new InsertImageHandler(editor,
|
|
84
|
+
const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
|
|
81
85
|
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
82
86
|
|
|
83
87
|
handler.done('error', null);
|
|
84
88
|
|
|
85
89
|
expect(consoleLogSpy).toHaveBeenCalledWith('error');
|
|
86
90
|
expect(mockOnFinish).toHaveBeenCalledWith(false);
|
|
91
|
+
expect(editor._insertingImage).toBe(false);
|
|
87
92
|
consoleLogSpy.mockRestore();
|
|
88
93
|
});
|
|
89
94
|
|
|
@@ -92,16 +97,17 @@ describe('InsertImageHandler', () => {
|
|
|
92
97
|
const mockNodeAt = jest.fn(() => ({ attrs: {} }));
|
|
93
98
|
editor.state.doc.nodeAt = mockNodeAt;
|
|
94
99
|
|
|
95
|
-
const handler = new InsertImageHandler(editor,
|
|
100
|
+
const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
|
|
96
101
|
handler.done(null, 'http://example.com/image.jpg');
|
|
97
102
|
|
|
98
103
|
expect(editor.view.dispatch).toHaveBeenCalled();
|
|
99
104
|
expect(mockOnFinish).toHaveBeenCalledWith(true);
|
|
105
|
+
expect(editor._insertingImage).toBe(false);
|
|
100
106
|
});
|
|
101
107
|
|
|
102
108
|
it('fileChosen returns early when no file provided', () => {
|
|
103
109
|
const editor = createMockEditor();
|
|
104
|
-
const handler = new InsertImageHandler(editor,
|
|
110
|
+
const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
|
|
105
111
|
handler.fileChosen(null);
|
|
106
112
|
expect(editor.view.dispatch).not.toHaveBeenCalled();
|
|
107
113
|
});
|
|
@@ -111,7 +117,7 @@ describe('InsertImageHandler', () => {
|
|
|
111
117
|
const mockNodeAt = jest.fn(() => ({ attrs: {} }));
|
|
112
118
|
editor.state.doc.nodeAt = mockNodeAt;
|
|
113
119
|
|
|
114
|
-
const handler = new InsertImageHandler(editor,
|
|
120
|
+
const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
|
|
115
121
|
const mockFile = new File(['content'], 'test.jpg', { type: 'image/jpeg' });
|
|
116
122
|
|
|
117
123
|
global.FileReader = jest.fn(function () {
|
|
@@ -124,6 +130,7 @@ describe('InsertImageHandler', () => {
|
|
|
124
130
|
handler.fileChosen(mockFile);
|
|
125
131
|
|
|
126
132
|
expect(handler.chosenFile).toBe(mockFile);
|
|
133
|
+
expect(editor._insertingImage).toBe(false);
|
|
127
134
|
});
|
|
128
135
|
|
|
129
136
|
it('progress updates node with percent', () => {
|
|
@@ -131,7 +138,7 @@ describe('InsertImageHandler', () => {
|
|
|
131
138
|
const mockNodeAt = jest.fn(() => ({ attrs: {} }));
|
|
132
139
|
editor.state.doc.nodeAt = mockNodeAt;
|
|
133
140
|
|
|
134
|
-
const handler = new InsertImageHandler(editor,
|
|
141
|
+
const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
|
|
135
142
|
handler.progress(50, 500, 1000);
|
|
136
143
|
|
|
137
144
|
expect(editor.view.dispatch).toHaveBeenCalled();
|
|
@@ -139,7 +146,7 @@ describe('InsertImageHandler', () => {
|
|
|
139
146
|
|
|
140
147
|
it('getChosenFile returns the chosen file', () => {
|
|
141
148
|
const editor = createMockEditor();
|
|
142
|
-
const handler = new InsertImageHandler(editor,
|
|
149
|
+
const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
|
|
143
150
|
const mockFile = new File(['content'], 'test.jpg', { type: 'image/jpeg' });
|
|
144
151
|
handler.chosenFile = mockFile;
|
|
145
152
|
|
|
@@ -148,7 +155,7 @@ describe('InsertImageHandler', () => {
|
|
|
148
155
|
|
|
149
156
|
it('getChosenFile returns null initially', () => {
|
|
150
157
|
const editor = createMockEditor();
|
|
151
|
-
const handler = new InsertImageHandler(editor,
|
|
158
|
+
const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
|
|
152
159
|
expect(handler.getChosenFile()).toBeNull();
|
|
153
160
|
});
|
|
154
161
|
});
|
|
@@ -214,4 +214,36 @@ describe('StyledMenuBar', () => {
|
|
|
214
214
|
toolbar?.dispatchEvent(event);
|
|
215
215
|
expect(preventDefaultSpy).toHaveBeenCalled();
|
|
216
216
|
});
|
|
217
|
+
|
|
218
|
+
it('calculates hasTextSelectionInTable correctly when selection is not empty in table', () => {
|
|
219
|
+
// This test verifies the hasTextSelectionInTable state computation
|
|
220
|
+
const { container } = render(<StyledMenuBar {...defaultProps} activePlugins={['table', 'bold', 'italic']} />);
|
|
221
|
+
expect(container).toBeInTheDocument();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('hides table manipulation buttons when text is selected in table', () => {
|
|
225
|
+
// When hasTextSelectionInTable is true, table row/column buttons should be hidden
|
|
226
|
+
const { container } = render(<StyledMenuBar {...defaultProps} activePlugins={['table']} />);
|
|
227
|
+
// The component should render but table manipulation buttons should be conditional
|
|
228
|
+
expect(container).toBeInTheDocument();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('shows table manipulation buttons when no text is selected in table', () => {
|
|
232
|
+
// When hasTextSelectionInTable is false, table row/column buttons should be visible
|
|
233
|
+
const { container } = render(<StyledMenuBar {...defaultProps} activePlugins={['table']} />);
|
|
234
|
+
expect(container).toBeInTheDocument();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('shows text formatting buttons regardless of table state', () => {
|
|
238
|
+
// Bold, italic, etc. should always be visible when their plugin is active
|
|
239
|
+
const { container } = render(<StyledMenuBar {...defaultProps} activePlugins={['bold', 'italic', 'underline']} />);
|
|
240
|
+
const buttons = container.querySelectorAll('button');
|
|
241
|
+
expect(buttons.length).toBeGreaterThan(0);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('does not hide text formatting buttons when in table', () => {
|
|
245
|
+
// Verify that the removal of "|| state.isTable" condition works correctly
|
|
246
|
+
const { container } = render(<StyledMenuBar {...defaultProps} activePlugins={['table', 'bold', 'italic']} />);
|
|
247
|
+
expect(container).toBeInTheDocument();
|
|
248
|
+
});
|
|
217
249
|
});
|
|
@@ -60,7 +60,7 @@ export default ({ editor, onChange }) => {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
const applyAlignment = (event) => {
|
|
63
|
-
const alignType = event.
|
|
63
|
+
const alignType = event.currentTarget?.getAttribute('value');
|
|
64
64
|
|
|
65
65
|
if (alignType) {
|
|
66
66
|
editor.commands.setTextAlign(alignType);
|
|
@@ -11,20 +11,11 @@ const log = debug('@pie-lib:editable-html:image:insert-image-handler');
|
|
|
11
11
|
* @param {Boolean} isPasted - a boolean that keeps track if the file is pasted
|
|
12
12
|
*/
|
|
13
13
|
class InsertImageHandler {
|
|
14
|
-
constructor(editor,
|
|
14
|
+
constructor(editor, nodeInfo, onFinish, isPasted = false) {
|
|
15
15
|
this.editor = editor;
|
|
16
|
-
this.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
editor.state.doc.descendants((node, pos) => {
|
|
21
|
-
if (node === this.node) {
|
|
22
|
-
nodePos = pos;
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
this.nodePos = nodePos;
|
|
16
|
+
this.nodeInfo = nodeInfo;
|
|
17
|
+
this.node = nodeInfo[0];
|
|
18
|
+
this.nodePos = nodeInfo[1];
|
|
28
19
|
this.onFinish = onFinish;
|
|
29
20
|
this.isPasted = isPasted;
|
|
30
21
|
this.chosenFile = null;
|
|
@@ -38,6 +29,8 @@ class InsertImageHandler {
|
|
|
38
29
|
this.onFinish(false);
|
|
39
30
|
} catch (err) {
|
|
40
31
|
//
|
|
32
|
+
} finally {
|
|
33
|
+
this.editor._insertingImage = false;
|
|
41
34
|
}
|
|
42
35
|
}
|
|
43
36
|
|
|
@@ -72,6 +65,8 @@ class InsertImageHandler {
|
|
|
72
65
|
this.updateNode({ loaded: true, src, percent: 100 });
|
|
73
66
|
this.onFinish(true);
|
|
74
67
|
}
|
|
68
|
+
|
|
69
|
+
this.editor._insertingImage = false;
|
|
75
70
|
}
|
|
76
71
|
|
|
77
72
|
/**
|
|
@@ -86,6 +81,7 @@ class InsertImageHandler {
|
|
|
86
81
|
|
|
87
82
|
// Save the chosen file to this.chosenFile
|
|
88
83
|
this.chosenFile = file;
|
|
84
|
+
this.editor._insertingImage = false;
|
|
89
85
|
|
|
90
86
|
log('[fileChosen] file: ', file);
|
|
91
87
|
const reader = new FileReader();
|
|
@@ -33,7 +33,11 @@ const DragDrop = (props) => {
|
|
|
33
33
|
|
|
34
34
|
// console.log({nodeProps.children})
|
|
35
35
|
return (
|
|
36
|
-
<NodeViewWrapper
|
|
36
|
+
<NodeViewWrapper
|
|
37
|
+
className="drag-in-the-blank"
|
|
38
|
+
data-selected={selected}
|
|
39
|
+
style={{ display: 'inline', whiteSpace: 'normal' }}
|
|
40
|
+
>
|
|
37
41
|
<span
|
|
38
42
|
{...attributes}
|
|
39
43
|
style={{
|
|
@@ -52,6 +56,7 @@ const DragDrop = (props) => {
|
|
|
52
56
|
pos={pos}
|
|
53
57
|
value={attributes}
|
|
54
58
|
duplicates={options.duplicates}
|
|
59
|
+
selected={selected}
|
|
55
60
|
onChange={(choice) => onValueChange(editor, node, pos, choice)}
|
|
56
61
|
removeResponse={(choice) => onRemoveResponse(editor, node, choice)}
|
|
57
62
|
></DragDropTile>
|
|
@@ -15,7 +15,7 @@ const StyledContent = styled('span')(({ theme }) => ({
|
|
|
15
15
|
},
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
|
-
export function BlankContent({ n, children, isDragging, isOver, dragItem, value }) {
|
|
18
|
+
export function BlankContent({ n, children, isDragging, isOver, dragItem, value, selected }) {
|
|
19
19
|
const [hoveredElementSize, setHoveredElementSize] = useState(null);
|
|
20
20
|
const elementRef = useRef(null);
|
|
21
21
|
|
|
@@ -56,15 +56,22 @@ export function BlankContent({ n, children, isDragging, isOver, dragItem, value
|
|
|
56
56
|
const hasGrip = finalLabel !== '\u00A0';
|
|
57
57
|
const isPreview = dragItem && isOver;
|
|
58
58
|
|
|
59
|
+
const borderStyle = selected
|
|
60
|
+
? `2px solid ${color.primaryDark()}`
|
|
61
|
+
: isPreview
|
|
62
|
+
? `1px solid ${color.defaults.BORDER_DARK}`
|
|
63
|
+
: `1px solid ${color.defaults.BORDER_LIGHT}`;
|
|
64
|
+
|
|
59
65
|
return (
|
|
60
66
|
<div
|
|
61
67
|
ref={elementRef}
|
|
68
|
+
className={selected ? 'selected' : undefined}
|
|
62
69
|
style={{
|
|
63
70
|
display: 'inline-flex',
|
|
64
71
|
minWidth: '178px',
|
|
65
72
|
minHeight: '36px',
|
|
66
73
|
background: isPreview ? `${color.defaults.BORDER_LIGHT}` : `${color.defaults.WHITE}`,
|
|
67
|
-
border:
|
|
74
|
+
border: borderStyle,
|
|
68
75
|
boxSizing: 'border-box',
|
|
69
76
|
borderRadius: '3px',
|
|
70
77
|
overflow: 'hidden',
|
|
@@ -72,6 +79,7 @@ export function BlankContent({ n, children, isDragging, isOver, dragItem, value
|
|
|
72
79
|
padding: '8px 8px 8px 35px',
|
|
73
80
|
width: hoveredElementSize ? hoveredElementSize.width : undefined,
|
|
74
81
|
height: hoveredElementSize ? hoveredElementSize.height : undefined,
|
|
82
|
+
touchAction: 'none',
|
|
75
83
|
}}
|
|
76
84
|
data-key={n.index}
|
|
77
85
|
contentEditable={false}
|
|
@@ -104,9 +112,21 @@ BlankContent.propTypes = {
|
|
|
104
112
|
isOver: PropTypes.bool,
|
|
105
113
|
dragItem: PropTypes.object,
|
|
106
114
|
value: PropTypes.object,
|
|
115
|
+
selected: PropTypes.bool,
|
|
107
116
|
};
|
|
108
117
|
|
|
109
|
-
function DragDropChoice({
|
|
118
|
+
function DragDropChoice({
|
|
119
|
+
value,
|
|
120
|
+
disabled,
|
|
121
|
+
instanceId,
|
|
122
|
+
children,
|
|
123
|
+
n,
|
|
124
|
+
onChange,
|
|
125
|
+
removeResponse,
|
|
126
|
+
duplicates,
|
|
127
|
+
pos,
|
|
128
|
+
selected,
|
|
129
|
+
}) {
|
|
110
130
|
const {
|
|
111
131
|
attributes: dragAttributes,
|
|
112
132
|
listeners: dragListeners,
|
|
@@ -196,7 +216,14 @@ function DragDropChoice({ value, disabled, instanceId, children, n, onChange, re
|
|
|
196
216
|
};
|
|
197
217
|
|
|
198
218
|
const dragContent = (
|
|
199
|
-
<BlankContent
|
|
219
|
+
<BlankContent
|
|
220
|
+
n={n}
|
|
221
|
+
isDragging={isDragging}
|
|
222
|
+
isOver={isOver}
|
|
223
|
+
dragItem={dragItem?.data?.current}
|
|
224
|
+
value={value}
|
|
225
|
+
selected={selected}
|
|
226
|
+
>
|
|
200
227
|
{children}
|
|
201
228
|
</BlankContent>
|
|
202
229
|
);
|
|
@@ -223,6 +250,7 @@ DragDropChoice.propTypes = {
|
|
|
223
250
|
onChange: PropTypes.func.isRequired,
|
|
224
251
|
removeResponse: PropTypes.func.isRequired,
|
|
225
252
|
duplicates: PropTypes.bool,
|
|
253
|
+
selected: PropTypes.bool,
|
|
226
254
|
};
|
|
227
255
|
|
|
228
256
|
export default DragDropChoice;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { NodeViewWrapper } from '@tiptap/react';
|
|
3
|
+
import ReactDOM from 'react-dom';
|
|
3
4
|
import PropTypes from 'prop-types';
|
|
5
|
+
import CustomToolbarWrapper from '../../extensions/custom-toolbar-wrapper';
|
|
4
6
|
|
|
5
7
|
const ExplicitConstructedResponse = (props) => {
|
|
6
8
|
const { editor, node, getPos, options, selected } = props;
|
|
@@ -9,7 +11,7 @@ const ExplicitConstructedResponse = (props) => {
|
|
|
9
11
|
const { respAreaToolbar, error: errorFn } = options;
|
|
10
12
|
const pos = getPos();
|
|
11
13
|
const [showToolbar, setShowToolbar] = useState(false);
|
|
12
|
-
const EcrToolbar = respAreaToolbar(node, editor, () => {});
|
|
14
|
+
const EcrToolbar = respAreaToolbar([node, pos], editor, () => {});
|
|
13
15
|
const toolbarRef = useRef(null);
|
|
14
16
|
|
|
15
17
|
let error;
|
|
@@ -21,12 +23,6 @@ const ExplicitConstructedResponse = (props) => {
|
|
|
21
23
|
error = !!errorValue?.[respIndex]?.[0];
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
const handleDone = (newLatex) => {
|
|
25
|
-
updateAttributes({ latex: newLatex });
|
|
26
|
-
setShowToolbar(false);
|
|
27
|
-
editor.commands.focus();
|
|
28
|
-
};
|
|
29
|
-
|
|
30
26
|
useEffect(() => {
|
|
31
27
|
const { selection } = editor.state;
|
|
32
28
|
const onlyThisNodeSelected = selection.from + node.nodeSize === selection.to;
|
|
@@ -42,7 +38,11 @@ const ExplicitConstructedResponse = (props) => {
|
|
|
42
38
|
|
|
43
39
|
useEffect(() => {
|
|
44
40
|
const handleClickOutside = (event) => {
|
|
41
|
+
const insideCharacterPicker =
|
|
42
|
+
event.target.closest('.insert-character-dialog') || event.target.closest('[data-toolbar-for]');
|
|
43
|
+
|
|
45
44
|
if (
|
|
45
|
+
!insideCharacterPicker &&
|
|
46
46
|
toolbarRef.current &&
|
|
47
47
|
!toolbarRef.current.contains(event.target) &&
|
|
48
48
|
!event.target.closest('[data-inline-node]')
|
|
@@ -94,9 +94,32 @@ const ExplicitConstructedResponse = (props) => {
|
|
|
94
94
|
}}
|
|
95
95
|
/>
|
|
96
96
|
{showToolbar && (
|
|
97
|
-
<
|
|
98
|
-
<
|
|
99
|
-
|
|
97
|
+
<React.Fragment>
|
|
98
|
+
<div ref={toolbarRef} className="absolute z-50 bg-white shadow-lg rounded p-2" style={{ zIndex: 1 }}>
|
|
99
|
+
<EcrToolbar />
|
|
100
|
+
</div>
|
|
101
|
+
{editor._tiptapContainerEl &&
|
|
102
|
+
ReactDOM.createPortal(
|
|
103
|
+
<CustomToolbarWrapper
|
|
104
|
+
deletable
|
|
105
|
+
toolbarOpts={{ minWidth: 'auto' }}
|
|
106
|
+
autoWidth
|
|
107
|
+
style={{ top: -40, left: 0, right: 0 }}
|
|
108
|
+
onDelete={() => {
|
|
109
|
+
const { tr } = editor.state;
|
|
110
|
+
tr.delete(pos, pos + node.nodeSize);
|
|
111
|
+
// Prevent the debounced onBlur/onDone from firing into the
|
|
112
|
+
// now-deleted node's stale position
|
|
113
|
+
editor._toolbarOpened = false;
|
|
114
|
+
editor.view.dispatch(tr);
|
|
115
|
+
setShowToolbar(false);
|
|
116
|
+
editor.commands.focus();
|
|
117
|
+
}}
|
|
118
|
+
showDone={false}
|
|
119
|
+
/>,
|
|
120
|
+
editor._tiptapContainerEl,
|
|
121
|
+
)}
|
|
122
|
+
</React.Fragment>
|
|
100
123
|
)}
|
|
101
124
|
</NodeViewWrapper>
|
|
102
125
|
);
|
|
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|
|
3
3
|
import { NodeViewWrapper } from '@tiptap/react';
|
|
4
4
|
import { Chevron } from '../icons/RespArea';
|
|
5
5
|
import ReactDOM from 'react-dom';
|
|
6
|
+
import CustomToolbarWrapper from '../../extensions/custom-toolbar-wrapper';
|
|
6
7
|
|
|
7
8
|
const InlineDropdown = (props) => {
|
|
8
9
|
const { editor, node, getPos, options, selected } = props;
|
|
@@ -11,10 +12,12 @@ const InlineDropdown = (props) => {
|
|
|
11
12
|
// TODO: Investigate
|
|
12
13
|
// Needed because items with values inside have different positioning for some reason
|
|
13
14
|
const html = value || '<div> </div>';
|
|
15
|
+
const pos = getPos();
|
|
14
16
|
const toolbarRef = useRef(null);
|
|
17
|
+
const toolbarEditor = useRef(null);
|
|
15
18
|
const [showToolbar, setShowToolbar] = useState(false);
|
|
16
19
|
const [position, setPosition] = useState({ top: 0, left: 0 });
|
|
17
|
-
const InlineDropdownToolbar = options.respAreaToolbar(node, editor, () => {});
|
|
20
|
+
const InlineDropdownToolbar = options.respAreaToolbar([node, pos], editor, () => {});
|
|
18
21
|
|
|
19
22
|
useEffect(() => {
|
|
20
23
|
const { selection } = editor.state;
|
|
@@ -41,7 +44,11 @@ const InlineDropdown = (props) => {
|
|
|
41
44
|
});
|
|
42
45
|
|
|
43
46
|
const handleClickOutside = (event) => {
|
|
47
|
+
const insideSomeEditor = event.target.closest('[data-toolbar-for]');
|
|
48
|
+
|
|
44
49
|
if (
|
|
50
|
+
(!insideSomeEditor || insideSomeEditor.dataset.toolbarFor !== toolbarEditor.current.instanceId) &&
|
|
51
|
+
!editor._toolbarOpened &&
|
|
45
52
|
toolbarRef.current &&
|
|
46
53
|
!toolbarRef.current.contains(event.target) &&
|
|
47
54
|
!event.target.closest('[data-inline-node]')
|
|
@@ -66,7 +73,6 @@ const InlineDropdown = (props) => {
|
|
|
66
73
|
style={{
|
|
67
74
|
display: 'inline-flex',
|
|
68
75
|
height: '50px',
|
|
69
|
-
margin: '0 5px',
|
|
70
76
|
cursor: 'pointer',
|
|
71
77
|
}}
|
|
72
78
|
>
|
|
@@ -79,7 +85,7 @@ const InlineDropdown = (props) => {
|
|
|
79
85
|
border: '1px solid #C0C3CF',
|
|
80
86
|
boxSizing: 'border-box',
|
|
81
87
|
borderRadius: '3px',
|
|
82
|
-
margin: '0
|
|
88
|
+
margin: '0 2px',
|
|
83
89
|
position: 'relative',
|
|
84
90
|
alignItems: 'center',
|
|
85
91
|
}}
|
|
@@ -113,13 +119,42 @@ const InlineDropdown = (props) => {
|
|
|
113
119
|
}}
|
|
114
120
|
/>
|
|
115
121
|
</div>
|
|
116
|
-
{showToolbar &&
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
{showToolbar && (
|
|
123
|
+
<React.Fragment>
|
|
124
|
+
{ReactDOM.createPortal(
|
|
125
|
+
<div ref={toolbarRef} style={{ zIndex: 1 }}>
|
|
126
|
+
<InlineDropdownToolbar
|
|
127
|
+
editorCallback={(instance) => {
|
|
128
|
+
toolbarEditor.current = instance;
|
|
129
|
+
}}
|
|
130
|
+
/>
|
|
131
|
+
</div>,
|
|
132
|
+
document.body,
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{editor._tiptapContainerEl &&
|
|
136
|
+
ReactDOM.createPortal(
|
|
137
|
+
<CustomToolbarWrapper
|
|
138
|
+
deletable
|
|
139
|
+
toolbarOpts={{ minWidth: 'auto' }}
|
|
140
|
+
autoWidth
|
|
141
|
+
style={{ top: -40, left: 0, right: 0 }}
|
|
142
|
+
onDelete={() => {
|
|
143
|
+
const { tr } = editor.state;
|
|
144
|
+
tr.delete(pos, pos + node.nodeSize);
|
|
145
|
+
// Prevent the debounced onBlur/onDone from firing into the
|
|
146
|
+
// now-deleted node's stale position
|
|
147
|
+
editor._toolbarOpened = false;
|
|
148
|
+
editor.view.dispatch(tr);
|
|
149
|
+
setShowToolbar(false);
|
|
150
|
+
editor.commands.focus();
|
|
151
|
+
}}
|
|
152
|
+
showDone={false}
|
|
153
|
+
/>,
|
|
154
|
+
editor._tiptapContainerEl,
|
|
155
|
+
)}
|
|
156
|
+
</React.Fragment>
|
|
157
|
+
)}
|
|
123
158
|
</NodeViewWrapper>
|
|
124
159
|
);
|
|
125
160
|
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { NodeViewWrapper } from '@tiptap/react';
|
|
4
|
+
import { mq } from '@pie-lib/math-input';
|
|
5
|
+
import { styled } from '@mui/material/styles';
|
|
6
|
+
|
|
7
|
+
const StyledSpanContainer = styled('span')(() => ({
|
|
8
|
+
display: 'inline-flex',
|
|
9
|
+
border: '1px solid #C0C3CF',
|
|
10
|
+
margin: '1px 5px',
|
|
11
|
+
cursor: 'pointer',
|
|
12
|
+
alignItems: 'center',
|
|
13
|
+
justifyContent: 'center',
|
|
14
|
+
minWidth: '50px',
|
|
15
|
+
minHeight: '36px',
|
|
16
|
+
height: 'fit-content',
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const StyledResponseBox = styled('div')(({ theme }) => ({
|
|
20
|
+
background: theme.palette.grey['A100'],
|
|
21
|
+
color: theme.palette.grey['A700'],
|
|
22
|
+
display: 'inline-flex',
|
|
23
|
+
borderRight: '2px solid #C0C3CF',
|
|
24
|
+
boxSizing: 'border-box',
|
|
25
|
+
overflow: 'hidden',
|
|
26
|
+
fontSize: '12px',
|
|
27
|
+
minHeight: '36px',
|
|
28
|
+
height: '100%',
|
|
29
|
+
alignItems: 'center',
|
|
30
|
+
fontFamily: 'Symbola, Times New Roman, serif',
|
|
31
|
+
padding: '0 2px',
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
const StyledMathBlock = styled('div')(() => ({
|
|
35
|
+
flex: 8,
|
|
36
|
+
color: 'var(--pie-text, black)',
|
|
37
|
+
padding: '4px !important',
|
|
38
|
+
display: 'flex',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
justifyContent: 'center',
|
|
41
|
+
backgroundColor: 'var(--pie-background, rgba(255, 255, 255, 0))',
|
|
42
|
+
'& > .mq-math-mode sup.mq-nthroot': {
|
|
43
|
+
fontSize: '70% !important',
|
|
44
|
+
verticalAlign: '1em !important',
|
|
45
|
+
},
|
|
46
|
+
'& > .mq-math-mode .mq-sqrt-stem': {
|
|
47
|
+
borderTop: '0.07em solid',
|
|
48
|
+
marginLeft: '-1.5px',
|
|
49
|
+
marginTop: '-2px !important',
|
|
50
|
+
paddingTop: '5px !important',
|
|
51
|
+
},
|
|
52
|
+
'& .mq-overarrow-inner': {
|
|
53
|
+
paddingTop: '0 !important',
|
|
54
|
+
border: 'none !important',
|
|
55
|
+
},
|
|
56
|
+
'& .mq-overarrow.mq-arrow-both': {
|
|
57
|
+
marginTop: '0px',
|
|
58
|
+
minWidth: '1.23em',
|
|
59
|
+
'& *': {
|
|
60
|
+
lineHeight: '1 !important',
|
|
61
|
+
},
|
|
62
|
+
'&:before': {
|
|
63
|
+
top: '-0.4em',
|
|
64
|
+
left: '-1px',
|
|
65
|
+
},
|
|
66
|
+
'&:after': {
|
|
67
|
+
top: '0px !important',
|
|
68
|
+
position: 'absolute !important',
|
|
69
|
+
right: '-2px',
|
|
70
|
+
},
|
|
71
|
+
'&.mq-empty:after': {
|
|
72
|
+
top: '-0.45em',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
'& .mq-overarrow.mq-arrow-right': {
|
|
76
|
+
'&:before': {
|
|
77
|
+
top: '-0.4em',
|
|
78
|
+
right: '-1px',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
'& .mq-overarrow-inner-right': {
|
|
82
|
+
display: 'none !important',
|
|
83
|
+
},
|
|
84
|
+
'& .mq-overarrow-inner-left': {
|
|
85
|
+
display: 'none !important',
|
|
86
|
+
},
|
|
87
|
+
}));
|
|
88
|
+
const MathTemplated = (props) => {
|
|
89
|
+
const { node, options, selected } = props;
|
|
90
|
+
const { attrs: attributes } = node;
|
|
91
|
+
const { value, index } = attributes;
|
|
92
|
+
|
|
93
|
+
// add 1 to index to display R 1 instead of R 0
|
|
94
|
+
const keyToDisplay = `R ${parseInt(index) + 1}`;
|
|
95
|
+
|
|
96
|
+
// console.log({nodeProps.children})
|
|
97
|
+
return (
|
|
98
|
+
<NodeViewWrapper
|
|
99
|
+
className="math-templated"
|
|
100
|
+
data-selected={selected}
|
|
101
|
+
style={{
|
|
102
|
+
display: 'inline-flex',
|
|
103
|
+
minHeight: '36px',
|
|
104
|
+
minWidth: '50px',
|
|
105
|
+
cursor: 'pointer',
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<StyledSpanContainer {...attributes}>
|
|
109
|
+
<StyledResponseBox>{keyToDisplay}</StyledResponseBox>
|
|
110
|
+
<StyledMathBlock>
|
|
111
|
+
<mq.Static latex={value} />
|
|
112
|
+
</StyledMathBlock>
|
|
113
|
+
</StyledSpanContainer>
|
|
114
|
+
</NodeViewWrapper>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
MathTemplated.propTypes = {
|
|
119
|
+
attributes: PropTypes.object,
|
|
120
|
+
value: PropTypes.string,
|
|
121
|
+
keyToDisplay: PropTypes.string,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export default MathTemplated;
|