@pie-lib/mask-markup 1.29.1-next.0 → 1.30.1
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/esm/index.js +1782 -0
- package/esm/index.js.map +1 -0
- package/package.json +13 -6
package/esm/index.js
ADDED
|
@@ -0,0 +1,1782 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom';
|
|
3
|
+
import PropTypes from 'prop-types';
|
|
4
|
+
import get from 'lodash/get';
|
|
5
|
+
import { withStyles } from '@material-ui/core/styles';
|
|
6
|
+
import Html from 'slate-html-serializer';
|
|
7
|
+
import { object } from 'to-style';
|
|
8
|
+
import debug from 'debug';
|
|
9
|
+
import classnames from 'classnames';
|
|
10
|
+
import { renderMath } from '@pie-lib/math-rendering';
|
|
11
|
+
import findKey from 'lodash/findKey';
|
|
12
|
+
import Chip from '@material-ui/core/Chip';
|
|
13
|
+
import { color } from '@pie-lib/render-ui';
|
|
14
|
+
import { DragSource, DragDroppablePlaceholder, DropTarget } from '@pie-lib/drag';
|
|
15
|
+
import grey from '@material-ui/core/colors/grey';
|
|
16
|
+
import EditableHtml from '@pie-lib/editable-html';
|
|
17
|
+
import Button from '@material-ui/core/Button';
|
|
18
|
+
import InputLabel from '@material-ui/core/InputLabel';
|
|
19
|
+
import Menu from '@material-ui/core/Menu';
|
|
20
|
+
import MenuItem from '@material-ui/core/MenuItem';
|
|
21
|
+
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
|
|
22
|
+
import ArrowDropUpIcon from '@material-ui/icons/ArrowDropUp';
|
|
23
|
+
import Close from '@material-ui/icons/Close';
|
|
24
|
+
import Check from '@material-ui/icons/Check';
|
|
25
|
+
|
|
26
|
+
function _extends() {
|
|
27
|
+
_extends = Object.assign || function (target) {
|
|
28
|
+
for (var i = 1; i < arguments.length; i++) {
|
|
29
|
+
var source = arguments[i];
|
|
30
|
+
|
|
31
|
+
for (var key in source) {
|
|
32
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
33
|
+
target[key] = source[key];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return target;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return _extends.apply(this, arguments);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function _objectWithoutPropertiesLoose(source, excluded) {
|
|
45
|
+
if (source == null) return {};
|
|
46
|
+
var target = {};
|
|
47
|
+
var sourceKeys = Object.keys(source);
|
|
48
|
+
var key, i;
|
|
49
|
+
|
|
50
|
+
for (i = 0; i < sourceKeys.length; i++) {
|
|
51
|
+
key = sourceKeys[i];
|
|
52
|
+
if (excluded.indexOf(key) >= 0) continue;
|
|
53
|
+
target[key] = source[key];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return target;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const log$1 = debug('@pie-lib:mask-markup:serialization');
|
|
60
|
+
const INLINE = ['span'];
|
|
61
|
+
const MARK = ['em', 'strong', 'u'];
|
|
62
|
+
const TEXT_NODE = 3;
|
|
63
|
+
const COMMENT_NODE = 8;
|
|
64
|
+
|
|
65
|
+
const attr = el => {
|
|
66
|
+
if (!el.attributes || el.attributes.length <= 0) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const out = {};
|
|
71
|
+
let i;
|
|
72
|
+
|
|
73
|
+
for (i = 0; i < el.attributes.length; i++) {
|
|
74
|
+
const a = el.attributes[i];
|
|
75
|
+
out[a.name] = a.value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return out;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const getObject = type => {
|
|
82
|
+
if (INLINE.includes(type)) {
|
|
83
|
+
return 'inline';
|
|
84
|
+
} else if (MARK.includes(type)) {
|
|
85
|
+
return 'mark';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return 'block';
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const parseStyleString = s => {
|
|
92
|
+
const regex = /([\w-]*)\s*:\s*([^;]*)/g;
|
|
93
|
+
let match;
|
|
94
|
+
const result = {};
|
|
95
|
+
|
|
96
|
+
while (match = regex.exec(s)) {
|
|
97
|
+
result[match[1]] = match[2].trim();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return result;
|
|
101
|
+
};
|
|
102
|
+
const reactAttributes = o => object(o, {
|
|
103
|
+
camelize: true,
|
|
104
|
+
addUnits: false
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const handleStyles = (el, attribute) => {
|
|
108
|
+
const styleString = el.getAttribute(attribute);
|
|
109
|
+
return reactAttributes(parseStyleString(styleString));
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const handleClass = (el, acc, attribute) => {
|
|
113
|
+
const classNames = el.getAttribute(attribute);
|
|
114
|
+
delete acc.class;
|
|
115
|
+
return classNames;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const attributesToMap = el => (acc, attribute) => {
|
|
119
|
+
if (!el.getAttribute) {
|
|
120
|
+
return acc;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const value = el.getAttribute(attribute);
|
|
124
|
+
|
|
125
|
+
if (value) {
|
|
126
|
+
switch (attribute) {
|
|
127
|
+
case 'style':
|
|
128
|
+
acc.style = handleStyles(el, attribute);
|
|
129
|
+
break;
|
|
130
|
+
|
|
131
|
+
case 'class':
|
|
132
|
+
acc.className = handleClass(el, acc, attribute);
|
|
133
|
+
break;
|
|
134
|
+
|
|
135
|
+
default:
|
|
136
|
+
acc[attribute] = el.getAttribute(attribute);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return acc;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const attributes = ['border', 'class', 'style'];
|
|
145
|
+
/**
|
|
146
|
+
* Tags to marks.
|
|
147
|
+
*
|
|
148
|
+
* @type {Object}
|
|
149
|
+
*/
|
|
150
|
+
|
|
151
|
+
const MARK_TAGS = {
|
|
152
|
+
b: 'bold',
|
|
153
|
+
em: 'italic',
|
|
154
|
+
u: 'underline',
|
|
155
|
+
s: 'strikethrough',
|
|
156
|
+
code: 'code',
|
|
157
|
+
strong: 'strong'
|
|
158
|
+
};
|
|
159
|
+
const marks = {
|
|
160
|
+
deserialize(el, next) {
|
|
161
|
+
const mark = MARK_TAGS[el.tagName.toLowerCase()];
|
|
162
|
+
if (!mark) return;
|
|
163
|
+
log$1('[deserialize] mark: ', mark);
|
|
164
|
+
return {
|
|
165
|
+
object: 'mark',
|
|
166
|
+
type: mark,
|
|
167
|
+
nodes: next(el.childNodes)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
};
|
|
172
|
+
const rules = [marks, {
|
|
173
|
+
/**
|
|
174
|
+
* deserialize everything, we're not fussy about the dom structure for now.
|
|
175
|
+
*/
|
|
176
|
+
deserialize: (el, next) => {
|
|
177
|
+
if (el.nodeType === COMMENT_NODE) {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (el.nodeType === TEXT_NODE) {
|
|
182
|
+
return {
|
|
183
|
+
object: 'text',
|
|
184
|
+
leaves: [{
|
|
185
|
+
text: el.textContent
|
|
186
|
+
}]
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const type = el.tagName.toLowerCase();
|
|
191
|
+
const normalAttrs = attr(el) || {};
|
|
192
|
+
|
|
193
|
+
if (type == 'audio' && normalAttrs.controls == '') {
|
|
194
|
+
normalAttrs.controls = true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const allAttrs = attributes.reduce(attributesToMap(el), _extends({}, normalAttrs));
|
|
198
|
+
const object = getObject(type);
|
|
199
|
+
|
|
200
|
+
if (el.tagName.toLowerCase() === 'math') {
|
|
201
|
+
return {
|
|
202
|
+
isMath: true,
|
|
203
|
+
nodes: [el]
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
object,
|
|
209
|
+
type,
|
|
210
|
+
data: {
|
|
211
|
+
dataset: _extends({}, el.dataset),
|
|
212
|
+
attributes: _extends({}, allAttrs)
|
|
213
|
+
},
|
|
214
|
+
nodes: next(el.childNodes)
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}];
|
|
218
|
+
/**
|
|
219
|
+
* Create a new serializer instance with our `rules` from above.
|
|
220
|
+
* Having a default div block will just put every div on it's own line, which is not ideal.
|
|
221
|
+
*/
|
|
222
|
+
|
|
223
|
+
const html = new Html({
|
|
224
|
+
rules,
|
|
225
|
+
defaultBlock: 'span'
|
|
226
|
+
});
|
|
227
|
+
const deserialize = s => html.deserialize(s, {
|
|
228
|
+
toJSON: true
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const Paragraph = withStyles(theme => ({
|
|
232
|
+
para: {
|
|
233
|
+
paddingTop: 2 * theme.spacing.unit,
|
|
234
|
+
paddingBottom: 2 * theme.spacing.unit
|
|
235
|
+
}
|
|
236
|
+
}))(props => /*#__PURE__*/React.createElement("div", {
|
|
237
|
+
className: props.classes.para
|
|
238
|
+
}, props.children));
|
|
239
|
+
const Spacer = withStyles(() => ({
|
|
240
|
+
spacer: {
|
|
241
|
+
display: 'inline-block',
|
|
242
|
+
width: '.75em'
|
|
243
|
+
}
|
|
244
|
+
}))(props => /*#__PURE__*/React.createElement("span", {
|
|
245
|
+
className: props.classes.spacer
|
|
246
|
+
}));
|
|
247
|
+
const restrictWhitespaceTypes = ['tbody', 'tr'];
|
|
248
|
+
|
|
249
|
+
const addText = (parentNode, text) => {
|
|
250
|
+
const isWhitespace = text.trim() === '';
|
|
251
|
+
const parentType = parentNode && parentNode.type;
|
|
252
|
+
|
|
253
|
+
if (isWhitespace && restrictWhitespaceTypes.includes(parentType)) {
|
|
254
|
+
return undefined;
|
|
255
|
+
} else {
|
|
256
|
+
return text;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const getMark = n => {
|
|
261
|
+
const mark = n.leaves.find(leave => get(leave, 'marks', []).length);
|
|
262
|
+
|
|
263
|
+
if (mark) {
|
|
264
|
+
return mark.marks[0];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return null;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const renderChildren = (layout, value, onChange, rootRenderChildren, parentNode, elementType) => {
|
|
271
|
+
if (!value) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const children = [];
|
|
276
|
+
(layout.nodes || []).forEach((n, index) => {
|
|
277
|
+
const key = n.type ? `${n.type}-${index}` : `${index}`;
|
|
278
|
+
|
|
279
|
+
if (n.isMath) {
|
|
280
|
+
children.push( /*#__PURE__*/React.createElement("span", {
|
|
281
|
+
dangerouslySetInnerHTML: {
|
|
282
|
+
__html: `<math displaystyle="true">${n.nodes[0].innerHTML}</math>`
|
|
283
|
+
}
|
|
284
|
+
}));
|
|
285
|
+
return children;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (rootRenderChildren) {
|
|
289
|
+
const c = rootRenderChildren(n, value, onChange);
|
|
290
|
+
|
|
291
|
+
if (c) {
|
|
292
|
+
children.push(c);
|
|
293
|
+
|
|
294
|
+
if ((parentNode == null ? void 0 : parentNode.type) !== 'td' && elementType === 'drag-in-the-blank') {
|
|
295
|
+
children.push( /*#__PURE__*/React.createElement(Spacer, {
|
|
296
|
+
key: `spacer-${index}`
|
|
297
|
+
}));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (n.object === 'text') {
|
|
305
|
+
const content = n.leaves.reduce((acc, l) => {
|
|
306
|
+
const t = l.text;
|
|
307
|
+
const extraText = addText(parentNode, t);
|
|
308
|
+
return extraText ? acc + extraText : acc;
|
|
309
|
+
}, '');
|
|
310
|
+
const mark = getMark(n);
|
|
311
|
+
|
|
312
|
+
if (mark) {
|
|
313
|
+
let markKey;
|
|
314
|
+
|
|
315
|
+
for (markKey in MARK_TAGS) {
|
|
316
|
+
if (MARK_TAGS[markKey] === mark.type) {
|
|
317
|
+
const Tag = markKey;
|
|
318
|
+
children.push( /*#__PURE__*/React.createElement(Tag, {
|
|
319
|
+
key: key
|
|
320
|
+
}, content));
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} else if (content.length > 0) {
|
|
325
|
+
children.push(content);
|
|
326
|
+
|
|
327
|
+
if ((parentNode == null ? void 0 : parentNode.type) !== 'td' && elementType === 'drag-in-the-blank') {
|
|
328
|
+
children.push( /*#__PURE__*/React.createElement(Spacer, {
|
|
329
|
+
key: `spacer-${index}`
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
const subNodes = renderChildren(n, value, onChange, rootRenderChildren, n, elementType);
|
|
335
|
+
|
|
336
|
+
if (n.type === 'p' || n.type === 'paragraph') {
|
|
337
|
+
children.push( /*#__PURE__*/React.createElement(Paragraph, {
|
|
338
|
+
key: key
|
|
339
|
+
}, subNodes));
|
|
340
|
+
} else {
|
|
341
|
+
const Tag = n.type;
|
|
342
|
+
|
|
343
|
+
if (n.nodes && n.nodes.length > 0) {
|
|
344
|
+
children.push( /*#__PURE__*/React.createElement(Tag, _extends({
|
|
345
|
+
key: key
|
|
346
|
+
}, n.data.attributes), subNodes));
|
|
347
|
+
} else {
|
|
348
|
+
children.push( /*#__PURE__*/React.createElement(Tag, _extends({
|
|
349
|
+
key: key
|
|
350
|
+
}, n.data.attributes)));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
return children;
|
|
356
|
+
};
|
|
357
|
+
const MaskContainer = withStyles(() => ({
|
|
358
|
+
main: {
|
|
359
|
+
display: 'initial'
|
|
360
|
+
},
|
|
361
|
+
tableStyle: {
|
|
362
|
+
'&:not(.MathJax) table': {
|
|
363
|
+
borderCollapse: 'collapse'
|
|
364
|
+
},
|
|
365
|
+
// align table content to left as per STAR requirement PD-3687
|
|
366
|
+
'&:not(.MathJax) table td, &:not(.MathJax) table th': {
|
|
367
|
+
padding: '8px 12px',
|
|
368
|
+
textAlign: 'left'
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}))(props => /*#__PURE__*/React.createElement("div", {
|
|
372
|
+
className: classnames(props.classes.main, props.classes.tableStyle)
|
|
373
|
+
}, props.children));
|
|
374
|
+
/**
|
|
375
|
+
* Renders a layout that uses the slate.js Value model structure.
|
|
376
|
+
*/
|
|
377
|
+
|
|
378
|
+
class Mask extends React.Component {
|
|
379
|
+
constructor(...args) {
|
|
380
|
+
super(...args);
|
|
381
|
+
|
|
382
|
+
this.handleChange = (id, value) => {
|
|
383
|
+
const data = _extends({}, this.props.value, {
|
|
384
|
+
[id]: value
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
this.props.onChange(data);
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
render() {
|
|
392
|
+
const {
|
|
393
|
+
value,
|
|
394
|
+
layout,
|
|
395
|
+
elementType
|
|
396
|
+
} = this.props;
|
|
397
|
+
const children = renderChildren(layout, value, this.handleChange, this.props.renderChildren, null, elementType);
|
|
398
|
+
return /*#__PURE__*/React.createElement(MaskContainer, null, children);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
}
|
|
402
|
+
Mask.propTypes = {
|
|
403
|
+
renderChildren: PropTypes.func,
|
|
404
|
+
layout: PropTypes.object,
|
|
405
|
+
value: PropTypes.object,
|
|
406
|
+
onChange: PropTypes.func,
|
|
407
|
+
elementType: PropTypes.string
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const REGEX = /\{\{(\d+)\}\}/g;
|
|
411
|
+
var componentize = ((s, t) => {
|
|
412
|
+
if (!s) {
|
|
413
|
+
return {
|
|
414
|
+
markup: ''
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const markup = s.replace(REGEX, (match, g) => {
|
|
419
|
+
return `<span data-component="${t}" data-id="${g}"></span>`;
|
|
420
|
+
});
|
|
421
|
+
return {
|
|
422
|
+
markup
|
|
423
|
+
};
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const buildLayoutFromMarkup = (markup, type) => {
|
|
427
|
+
const {
|
|
428
|
+
markup: processed
|
|
429
|
+
} = componentize(markup, type);
|
|
430
|
+
const value = deserialize(processed);
|
|
431
|
+
return value.document;
|
|
432
|
+
};
|
|
433
|
+
const withMask = (type, renderChildren) => {
|
|
434
|
+
var _class;
|
|
435
|
+
|
|
436
|
+
return _class = class WithMask extends React.Component {
|
|
437
|
+
componentDidUpdate(prevProps) {
|
|
438
|
+
if (this.props.markup !== prevProps.markup) {
|
|
439
|
+
// eslint-disable-next-line
|
|
440
|
+
const domNode = ReactDOM.findDOMNode(this); // Query all elements that may contain outdated MathJax renderings
|
|
441
|
+
|
|
442
|
+
const mathElements = domNode && domNode.querySelectorAll('[data-latex][data-math-handled="true"]'); // Clean up for fresh MathJax processing
|
|
443
|
+
|
|
444
|
+
(mathElements || []).forEach(el => {
|
|
445
|
+
// Remove the MathJax container to allow for clean updates
|
|
446
|
+
const mjxContainer = el.querySelector('mjx-container');
|
|
447
|
+
|
|
448
|
+
if (mjxContainer) {
|
|
449
|
+
el.removeChild(mjxContainer);
|
|
450
|
+
} // Update the innerHTML to match the raw LaTeX data, ensuring it is reprocessed correctly
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
const latexCode = el.getAttribute('data-raw');
|
|
454
|
+
el.innerHTML = latexCode; // Remove the attribute to signal that MathJax should reprocess this element
|
|
455
|
+
|
|
456
|
+
el.removeAttribute('data-math-handled');
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
render() {
|
|
462
|
+
const {
|
|
463
|
+
markup,
|
|
464
|
+
layout,
|
|
465
|
+
value,
|
|
466
|
+
onChange,
|
|
467
|
+
elementType
|
|
468
|
+
} = this.props;
|
|
469
|
+
const maskLayout = layout ? layout : buildLayoutFromMarkup(markup, type);
|
|
470
|
+
return /*#__PURE__*/React.createElement(Mask, {
|
|
471
|
+
elementType: elementType,
|
|
472
|
+
layout: maskLayout,
|
|
473
|
+
value: value,
|
|
474
|
+
onChange: onChange,
|
|
475
|
+
renderChildren: renderChildren(this.props)
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
}, _class.propTypes = {
|
|
480
|
+
/**
|
|
481
|
+
* At the start we'll probably work with markup
|
|
482
|
+
*/
|
|
483
|
+
markup: PropTypes.string,
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Once we start authoring, it may make sense for use to us layout, which will be a simple js object that maps to `slate.Value`.
|
|
487
|
+
*/
|
|
488
|
+
layout: PropTypes.object,
|
|
489
|
+
value: PropTypes.object,
|
|
490
|
+
onChange: PropTypes.func,
|
|
491
|
+
customMarkMarkupComponent: PropTypes.func,
|
|
492
|
+
elementType: PropTypes.string
|
|
493
|
+
}, _class;
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const DRAG_TYPE$1 = 'MaskBlank';
|
|
497
|
+
|
|
498
|
+
class BlankContentComp extends React.Component {
|
|
499
|
+
constructor(...args) {
|
|
500
|
+
super(...args);
|
|
501
|
+
|
|
502
|
+
this.startDrag = () => {
|
|
503
|
+
const {
|
|
504
|
+
connectDragSource,
|
|
505
|
+
disabled
|
|
506
|
+
} = this.props;
|
|
507
|
+
|
|
508
|
+
if (!disabled) {
|
|
509
|
+
connectDragSource(this.dragContainerRef);
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
this.handleTouchStart = e => {
|
|
514
|
+
e.preventDefault();
|
|
515
|
+
this.longPressTimer = setTimeout(() => {
|
|
516
|
+
this.startDrag(e);
|
|
517
|
+
}, 500);
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
this.handleTouchEnd = () => {
|
|
521
|
+
clearTimeout(this.longPressTimer);
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
this.handleTouchMove = () => {
|
|
525
|
+
clearTimeout(this.longPressTimer);
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
componentDidMount() {
|
|
530
|
+
if (this.dragContainerRef) {
|
|
531
|
+
this.dragContainerRef.addEventListener('touchstart', this.handleTouchStart, {
|
|
532
|
+
passive: false
|
|
533
|
+
});
|
|
534
|
+
this.dragContainerRef.addEventListener('touchend', this.handleTouchEnd);
|
|
535
|
+
this.dragContainerRef.addEventListener('touchmove', this.handleTouchMove);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
componentWillUnmount() {
|
|
540
|
+
if (this.dragContainerRef) {
|
|
541
|
+
this.dragContainerRef.removeEventListener('touchstart', this.handleTouchStart);
|
|
542
|
+
this.dragContainerRef.removeEventListener('touchend', this.handleTouchEnd);
|
|
543
|
+
this.dragContainerRef.removeEventListener('touchmove', this.handleTouchMove);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
componentDidUpdate() {
|
|
548
|
+
renderMath(this.rootRef);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
render() {
|
|
552
|
+
const {
|
|
553
|
+
connectDragSource,
|
|
554
|
+
choice,
|
|
555
|
+
classes,
|
|
556
|
+
disabled
|
|
557
|
+
} = this.props; // TODO the Chip element is causing drag problems on touch devices. Avoid using Chip and consider refactoring the code. Keep in mind that Chip is a span with a button role, which interferes with seamless touch device dragging.
|
|
558
|
+
|
|
559
|
+
return connectDragSource( /*#__PURE__*/React.createElement("span", {
|
|
560
|
+
className: classnames(classes.choice, disabled && classes.disabled),
|
|
561
|
+
ref: ref => {
|
|
562
|
+
//eslint-disable-next-line
|
|
563
|
+
this.dragContainerRef = ReactDOM.findDOMNode(ref);
|
|
564
|
+
}
|
|
565
|
+
}, /*#__PURE__*/React.createElement(Chip, {
|
|
566
|
+
clickable: false,
|
|
567
|
+
disabled: true,
|
|
568
|
+
ref: ref => {
|
|
569
|
+
//eslint-disable-next-line
|
|
570
|
+
this.rootRef = ReactDOM.findDOMNode(ref);
|
|
571
|
+
},
|
|
572
|
+
className: classes.chip,
|
|
573
|
+
label: /*#__PURE__*/React.createElement("span", {
|
|
574
|
+
className: classes.chipLabel,
|
|
575
|
+
ref: ref => {
|
|
576
|
+
if (ref) {
|
|
577
|
+
ref.innerHTML = choice.value || ' ';
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}, ' '),
|
|
581
|
+
variant: disabled ? 'outlined' : undefined
|
|
582
|
+
})), {});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
BlankContentComp.propTypes = {
|
|
588
|
+
disabled: PropTypes.bool,
|
|
589
|
+
choice: PropTypes.object,
|
|
590
|
+
classes: PropTypes.object,
|
|
591
|
+
connectDragSource: PropTypes.func
|
|
592
|
+
};
|
|
593
|
+
const BlankContent$1 = withStyles(theme => ({
|
|
594
|
+
choice: {
|
|
595
|
+
border: `solid 0px ${theme.palette.primary.main}`,
|
|
596
|
+
borderRadius: theme.spacing.unit * 2,
|
|
597
|
+
margin: theme.spacing.unit / 2,
|
|
598
|
+
transform: 'translate(0, 0)'
|
|
599
|
+
},
|
|
600
|
+
chip: {
|
|
601
|
+
backgroundColor: color.white(),
|
|
602
|
+
border: `1px solid ${color.text()}`,
|
|
603
|
+
color: color.text(),
|
|
604
|
+
alignItems: 'center',
|
|
605
|
+
display: 'inline-flex',
|
|
606
|
+
height: 'initial',
|
|
607
|
+
minHeight: '32px',
|
|
608
|
+
fontSize: 'inherit',
|
|
609
|
+
whiteSpace: 'pre-wrap',
|
|
610
|
+
maxWidth: '374px',
|
|
611
|
+
// Added for touch devices, for image content.
|
|
612
|
+
// This will prevent the context menu from appearing and not allowing other interactions with the image.
|
|
613
|
+
// If interactions with the image in the token will be requested we should handle only the context Menu.
|
|
614
|
+
pointerEvents: 'none',
|
|
615
|
+
borderRadius: '3px',
|
|
616
|
+
paddingTop: '12px',
|
|
617
|
+
paddingBottom: '12px'
|
|
618
|
+
},
|
|
619
|
+
chipLabel: {
|
|
620
|
+
whiteSpace: 'normal',
|
|
621
|
+
'& img': {
|
|
622
|
+
display: 'block',
|
|
623
|
+
padding: '2px 0'
|
|
624
|
+
},
|
|
625
|
+
'& mjx-frac': {
|
|
626
|
+
fontSize: '120% !important'
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
disabled: {
|
|
630
|
+
opacity: 0.6
|
|
631
|
+
}
|
|
632
|
+
}))(BlankContentComp);
|
|
633
|
+
const tileSource$1 = {
|
|
634
|
+
canDrag(props) {
|
|
635
|
+
return !props.disabled;
|
|
636
|
+
},
|
|
637
|
+
|
|
638
|
+
beginDrag(props) {
|
|
639
|
+
return {
|
|
640
|
+
choice: props.choice,
|
|
641
|
+
instanceId: props.instanceId
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
};
|
|
646
|
+
const DragDropTile$1 = DragSource(DRAG_TYPE$1, tileSource$1, (connect, monitor) => ({
|
|
647
|
+
connectDragSource: connect.dragSource(),
|
|
648
|
+
isDragging: monitor.isDragging()
|
|
649
|
+
}))(BlankContent$1);
|
|
650
|
+
|
|
651
|
+
class Choices extends React.Component {
|
|
652
|
+
constructor(...args) {
|
|
653
|
+
super(...args);
|
|
654
|
+
|
|
655
|
+
this.getStyleForWrapper = () => {
|
|
656
|
+
const {
|
|
657
|
+
choicePosition
|
|
658
|
+
} = this.props;
|
|
659
|
+
|
|
660
|
+
switch (choicePosition) {
|
|
661
|
+
case 'above':
|
|
662
|
+
return {
|
|
663
|
+
margin: '0 0 40px 0'
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
case 'below':
|
|
667
|
+
return {
|
|
668
|
+
margin: '40px 0 0 0'
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
case 'right':
|
|
672
|
+
return {
|
|
673
|
+
margin: '0 0 0 40px'
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
default:
|
|
677
|
+
return {
|
|
678
|
+
margin: '0 40px 0 0'
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
render() {
|
|
685
|
+
const {
|
|
686
|
+
disabled,
|
|
687
|
+
duplicates,
|
|
688
|
+
choices,
|
|
689
|
+
value
|
|
690
|
+
} = this.props;
|
|
691
|
+
const filteredChoices = choices.filter(c => {
|
|
692
|
+
if (duplicates === true) {
|
|
693
|
+
return true;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const foundChoice = findKey(value, v => v === c.id);
|
|
697
|
+
return foundChoice === undefined;
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
const elementStyle = _extends({}, this.getStyleForWrapper(), {
|
|
701
|
+
minWidth: '100px'
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
705
|
+
style: elementStyle
|
|
706
|
+
}, /*#__PURE__*/React.createElement(DragDroppablePlaceholder, {
|
|
707
|
+
disabled: disabled
|
|
708
|
+
}, filteredChoices.map((c, index) => /*#__PURE__*/React.createElement(DragDropTile$1, {
|
|
709
|
+
key: `${c.value}-${index}`,
|
|
710
|
+
disabled: disabled,
|
|
711
|
+
choice: c
|
|
712
|
+
}))));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
}
|
|
716
|
+
Choices.propTypes = {
|
|
717
|
+
disabled: PropTypes.bool,
|
|
718
|
+
duplicates: PropTypes.bool,
|
|
719
|
+
choices: PropTypes.arrayOf(PropTypes.shape({
|
|
720
|
+
label: PropTypes.string,
|
|
721
|
+
value: PropTypes.string
|
|
722
|
+
})),
|
|
723
|
+
value: PropTypes.object,
|
|
724
|
+
choicePosition: PropTypes.string.isRequired
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
const _excluded = ["connectDragSource", "connectDropTarget"];
|
|
728
|
+
const log = debug('pie-lib:mask-markup:blank');
|
|
729
|
+
const DRAG_TYPE = 'MaskBlank';
|
|
730
|
+
const useStyles = withStyles(() => ({
|
|
731
|
+
content: {
|
|
732
|
+
border: `solid 0px ${color.primary()}`,
|
|
733
|
+
minWidth: '200px',
|
|
734
|
+
touchAction: 'none',
|
|
735
|
+
overflow: 'hidden',
|
|
736
|
+
whiteSpace: 'nowrap' // Prevent line wrapping
|
|
737
|
+
|
|
738
|
+
},
|
|
739
|
+
chip: {
|
|
740
|
+
backgroundColor: color.background(),
|
|
741
|
+
border: `2px dashed ${color.text()}`,
|
|
742
|
+
color: color.text(),
|
|
743
|
+
fontSize: 'inherit',
|
|
744
|
+
maxWidth: '374px',
|
|
745
|
+
position: 'relative',
|
|
746
|
+
borderRadius: '3px'
|
|
747
|
+
},
|
|
748
|
+
chipLabel: {
|
|
749
|
+
whiteSpace: 'normal',
|
|
750
|
+
// Added for touch devices, for image content.
|
|
751
|
+
// This will prevent the context menu from appearing and not allowing other interactions with the image.
|
|
752
|
+
// If interactions with the image in the token will be requested we should handle only the context Menu.
|
|
753
|
+
pointerEvents: 'none',
|
|
754
|
+
'& img': {
|
|
755
|
+
display: 'block',
|
|
756
|
+
padding: '2px 0'
|
|
757
|
+
},
|
|
758
|
+
// Remove default <p> margins to ensure consistent spacing across all wrapped content (p, span, div, math)
|
|
759
|
+
// Padding for top and bottom will instead be controlled by the container for consistent layout
|
|
760
|
+
// Ensures consistent behavior with pie-api-browser, where marginTop is already removed by a Bootstrap stylesheet
|
|
761
|
+
'& p': {
|
|
762
|
+
marginTop: '0',
|
|
763
|
+
marginBottom: '0'
|
|
764
|
+
},
|
|
765
|
+
'& mjx-frac': {
|
|
766
|
+
fontSize: '120% !important'
|
|
767
|
+
}
|
|
768
|
+
},
|
|
769
|
+
hidden: {
|
|
770
|
+
color: 'transparent',
|
|
771
|
+
opacity: 0
|
|
772
|
+
},
|
|
773
|
+
dragged: {
|
|
774
|
+
position: 'absolute',
|
|
775
|
+
left: 16,
|
|
776
|
+
maxWidth: '60px'
|
|
777
|
+
},
|
|
778
|
+
correct: {
|
|
779
|
+
border: `solid 1px ${color.correct()}`
|
|
780
|
+
},
|
|
781
|
+
incorrect: {
|
|
782
|
+
border: `solid 1px ${color.incorrect()}`
|
|
783
|
+
},
|
|
784
|
+
over: {
|
|
785
|
+
whiteSpace: 'nowrap',
|
|
786
|
+
overflow: 'hidden'
|
|
787
|
+
},
|
|
788
|
+
parentOver: {
|
|
789
|
+
border: `1px solid ${grey[500]}`,
|
|
790
|
+
backgroundColor: `${grey[300]}`
|
|
791
|
+
}
|
|
792
|
+
}));
|
|
793
|
+
class BlankContent extends React.Component {
|
|
794
|
+
constructor() {
|
|
795
|
+
super();
|
|
796
|
+
|
|
797
|
+
this.handleImageLoad = () => {
|
|
798
|
+
this.updateDimensions();
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
this.handleTouchStart = e => {
|
|
802
|
+
e.preventDefault();
|
|
803
|
+
this.touchStartTimer = setTimeout(() => {
|
|
804
|
+
this.startDrag();
|
|
805
|
+
}, 300); // Start drag after 300ms (touch and hold duration)
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
this.startDrag = () => {
|
|
809
|
+
const {
|
|
810
|
+
connectDragSource,
|
|
811
|
+
disabled
|
|
812
|
+
} = this.props;
|
|
813
|
+
|
|
814
|
+
if (!disabled) {
|
|
815
|
+
connectDragSource(this.rootRef);
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
this.state = {
|
|
820
|
+
height: 0,
|
|
821
|
+
width: 0
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
handleElements() {
|
|
826
|
+
var _this$spanRef;
|
|
827
|
+
|
|
828
|
+
const imageElement = (_this$spanRef = this.spanRef) == null ? void 0 : _this$spanRef.querySelector('img');
|
|
829
|
+
|
|
830
|
+
if (imageElement) {
|
|
831
|
+
imageElement.onload = this.handleImageLoad;
|
|
832
|
+
} else {
|
|
833
|
+
setTimeout(() => {
|
|
834
|
+
this.updateDimensions();
|
|
835
|
+
}, 300);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
componentDidMount() {
|
|
840
|
+
this.handleElements();
|
|
841
|
+
|
|
842
|
+
if (this.rootRef) {
|
|
843
|
+
this.rootRef.addEventListener('touchstart', this.handleTouchStart, {
|
|
844
|
+
passive: false
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
componentDidUpdate(prevProps) {
|
|
850
|
+
renderMath(this.rootRef);
|
|
851
|
+
const {
|
|
852
|
+
choice: currentChoice
|
|
853
|
+
} = this.props;
|
|
854
|
+
const {
|
|
855
|
+
choice: prevChoice
|
|
856
|
+
} = prevProps;
|
|
857
|
+
|
|
858
|
+
if (JSON.stringify(currentChoice) !== JSON.stringify(prevChoice)) {
|
|
859
|
+
if (!currentChoice) {
|
|
860
|
+
this.setState({
|
|
861
|
+
height: 0,
|
|
862
|
+
width: 0
|
|
863
|
+
});
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
this.handleElements();
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
componentWillUnmount() {
|
|
872
|
+
if (this.rootRef) {
|
|
873
|
+
this.rootRef.removeEventListener('touchstart', this.handleTouchStart);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
updateDimensions() {
|
|
878
|
+
if (this.spanRef && this.rootRef) {
|
|
879
|
+
// Temporarily set rootRef width to 'auto' for natural measurement
|
|
880
|
+
this.rootRef.style.width = 'auto'; // Get the natural dimensions of the content
|
|
881
|
+
|
|
882
|
+
const width = this.spanRef.offsetWidth || 0;
|
|
883
|
+
const height = this.spanRef.offsetHeight || 0;
|
|
884
|
+
const widthWithPadding = width + 24; // 12px padding on each side
|
|
885
|
+
|
|
886
|
+
const heightWithPadding = height + 24; // 12px padding on top and bottom
|
|
887
|
+
|
|
888
|
+
const responseAreaWidth = parseFloat(this.props.emptyResponseAreaWidth) || 0;
|
|
889
|
+
const responseAreaHeight = parseFloat(this.props.emptyResponseAreaHeight) || 0;
|
|
890
|
+
const adjustedWidth = widthWithPadding <= responseAreaWidth ? responseAreaWidth : widthWithPadding;
|
|
891
|
+
const adjustedHeight = heightWithPadding <= responseAreaHeight ? responseAreaHeight : heightWithPadding;
|
|
892
|
+
this.setState(prevState => ({
|
|
893
|
+
width: adjustedWidth > responseAreaWidth ? adjustedWidth : prevState.width,
|
|
894
|
+
height: adjustedHeight > responseAreaHeight ? adjustedHeight : prevState.height
|
|
895
|
+
}));
|
|
896
|
+
this.rootRef.style.width = `${adjustedWidth}px`;
|
|
897
|
+
this.rootRef.style.height = `${adjustedHeight}px`;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
addDraggableFalseAttributes(parent) {
|
|
902
|
+
parent.childNodes.forEach(elem => {
|
|
903
|
+
if (elem instanceof Element || elem instanceof HTMLDocument) {
|
|
904
|
+
elem.setAttribute('draggable', false);
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
getRootDimensions() {
|
|
910
|
+
// Handle potential non-numeric values
|
|
911
|
+
const responseAreaWidth = !isNaN(parseFloat(this.props.emptyResponseAreaWidth)) ? parseFloat(this.props.emptyResponseAreaWidth) : 0;
|
|
912
|
+
const responseAreaHeight = !isNaN(parseFloat(this.props.emptyResponseAreaHeight)) ? parseFloat(this.props.emptyResponseAreaHeight) : 0;
|
|
913
|
+
const rootStyle = {
|
|
914
|
+
height: this.state.height || responseAreaHeight,
|
|
915
|
+
width: this.state.width || responseAreaWidth
|
|
916
|
+
}; // add minWidth, minHeight if width and height are not defined
|
|
917
|
+
// minWidth, minHeight will be also in model in the future
|
|
918
|
+
|
|
919
|
+
return _extends({}, rootStyle, responseAreaWidth ? {} : {
|
|
920
|
+
minWidth: 90
|
|
921
|
+
}, responseAreaHeight ? {} : {
|
|
922
|
+
minHeight: 32
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
render() {
|
|
927
|
+
const {
|
|
928
|
+
disabled,
|
|
929
|
+
choice,
|
|
930
|
+
classes,
|
|
931
|
+
isOver,
|
|
932
|
+
dragItem,
|
|
933
|
+
correct
|
|
934
|
+
} = this.props;
|
|
935
|
+
const draggedLabel = dragItem && isOver && dragItem.choice.value;
|
|
936
|
+
const label = choice && choice.value;
|
|
937
|
+
return (
|
|
938
|
+
/*#__PURE__*/
|
|
939
|
+
// TODO the Chip element is causing drag problems on touch devices. Avoid using Chip and consider refactoring the code. Keep in mind that Chip is a span with a button role, which interferes with seamless touch device dragging.
|
|
940
|
+
React.createElement(Chip, {
|
|
941
|
+
clickable: false,
|
|
942
|
+
disabled: true,
|
|
943
|
+
ref: ref => {
|
|
944
|
+
//eslint-disable-next-line
|
|
945
|
+
this.rootRef = ReactDOM.findDOMNode(ref);
|
|
946
|
+
},
|
|
947
|
+
component: "span",
|
|
948
|
+
label: /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
|
|
949
|
+
className: classnames(classes.chipLabel, isOver && classes.over, {
|
|
950
|
+
[classes.hidden]: draggedLabel
|
|
951
|
+
}),
|
|
952
|
+
ref: ref => {
|
|
953
|
+
if (ref) {
|
|
954
|
+
//eslint-disable-next-line
|
|
955
|
+
this.spanRef = ReactDOM.findDOMNode(ref);
|
|
956
|
+
ref.innerHTML = label || '';
|
|
957
|
+
this.addDraggableFalseAttributes(ref);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}, ' '), draggedLabel && /*#__PURE__*/React.createElement("span", {
|
|
961
|
+
className: classnames(classes.chipLabel, isOver && classes.over, classes.dragged),
|
|
962
|
+
ref: ref => {
|
|
963
|
+
if (ref) {
|
|
964
|
+
//eslint-disable-next-line
|
|
965
|
+
this.spanRef = ReactDOM.findDOMNode(ref);
|
|
966
|
+
ref.innerHTML = draggedLabel || '';
|
|
967
|
+
this.addDraggableFalseAttributes(ref);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}, ' ')),
|
|
971
|
+
className: classnames(classes.chip, isOver && classes.over, isOver && classes.parentOver, {
|
|
972
|
+
[classes.correct]: correct !== undefined && correct,
|
|
973
|
+
[classes.incorrect]: correct !== undefined && !correct
|
|
974
|
+
}),
|
|
975
|
+
variant: disabled ? 'outlined' : undefined,
|
|
976
|
+
style: _extends({}, this.getRootDimensions())
|
|
977
|
+
})
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
}
|
|
982
|
+
BlankContent.defaultProps = {
|
|
983
|
+
emptyResponseAreaWidth: 0,
|
|
984
|
+
emptyResponseAreaHeight: 0
|
|
985
|
+
};
|
|
986
|
+
BlankContent.propTypes = {
|
|
987
|
+
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
988
|
+
disabled: PropTypes.bool,
|
|
989
|
+
duplicates: PropTypes.bool,
|
|
990
|
+
choice: PropTypes.object,
|
|
991
|
+
classes: PropTypes.object,
|
|
992
|
+
isOver: PropTypes.bool,
|
|
993
|
+
dragItem: PropTypes.object,
|
|
994
|
+
correct: PropTypes.bool,
|
|
995
|
+
onChange: PropTypes.func,
|
|
996
|
+
emptyResponseAreaWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
997
|
+
emptyResponseAreaHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
|
998
|
+
};
|
|
999
|
+
const StyledBlankContent = useStyles(BlankContent);
|
|
1000
|
+
const connectedBlankContent = useStyles(_ref => {
|
|
1001
|
+
let {
|
|
1002
|
+
connectDragSource,
|
|
1003
|
+
connectDropTarget
|
|
1004
|
+
} = _ref,
|
|
1005
|
+
props = _objectWithoutPropertiesLoose(_ref, _excluded);
|
|
1006
|
+
|
|
1007
|
+
const {
|
|
1008
|
+
classes,
|
|
1009
|
+
isOver
|
|
1010
|
+
} = props;
|
|
1011
|
+
return connectDropTarget(connectDragSource( /*#__PURE__*/React.createElement("span", {
|
|
1012
|
+
className: classnames(classes.content, isOver && classes.over)
|
|
1013
|
+
}, /*#__PURE__*/React.createElement(StyledBlankContent, props))));
|
|
1014
|
+
});
|
|
1015
|
+
const tileTarget = {
|
|
1016
|
+
drop(props, monitor) {
|
|
1017
|
+
const draggedItem = monitor.getItem();
|
|
1018
|
+
log('props.instanceId', props.instanceId, 'draggedItem.instanceId:', draggedItem.instanceId);
|
|
1019
|
+
|
|
1020
|
+
if (draggedItem.id !== props.id) {
|
|
1021
|
+
props.onChange(props.id, draggedItem.choice.id);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return {
|
|
1025
|
+
dropped: draggedItem.id !== props.id
|
|
1026
|
+
};
|
|
1027
|
+
},
|
|
1028
|
+
|
|
1029
|
+
canDrop(props, monitor) {
|
|
1030
|
+
const draggedItem = monitor.getItem();
|
|
1031
|
+
return draggedItem.instanceId === props.instanceId;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
};
|
|
1035
|
+
const DropTile = DropTarget(DRAG_TYPE, tileTarget, (connect, monitor) => ({
|
|
1036
|
+
connectDropTarget: connect.dropTarget(),
|
|
1037
|
+
isOver: monitor.isOver(),
|
|
1038
|
+
dragItem: monitor.getItem()
|
|
1039
|
+
}))(connectedBlankContent);
|
|
1040
|
+
const tileSource = {
|
|
1041
|
+
canDrag(props) {
|
|
1042
|
+
return !props.disabled && !!props.choice;
|
|
1043
|
+
},
|
|
1044
|
+
|
|
1045
|
+
beginDrag(props) {
|
|
1046
|
+
return {
|
|
1047
|
+
id: props.id,
|
|
1048
|
+
choice: props.choice,
|
|
1049
|
+
instanceId: props.instanceId,
|
|
1050
|
+
fromChoice: true
|
|
1051
|
+
};
|
|
1052
|
+
},
|
|
1053
|
+
|
|
1054
|
+
endDrag(props, monitor) {
|
|
1055
|
+
// this will be null if it did not drop
|
|
1056
|
+
const dropResult = monitor.getDropResult();
|
|
1057
|
+
|
|
1058
|
+
if (!dropResult || dropResult.dropped) {
|
|
1059
|
+
const draggedItem = monitor.getItem();
|
|
1060
|
+
|
|
1061
|
+
if (draggedItem.fromChoice) {
|
|
1062
|
+
props.onChange(props.id, undefined);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
};
|
|
1068
|
+
const DragDropTile = DragSource(DRAG_TYPE, tileSource, (connect, monitor) => ({
|
|
1069
|
+
connectDragSource: connect.dragSource(),
|
|
1070
|
+
isDragging: monitor.isDragging()
|
|
1071
|
+
}))(DropTile);
|
|
1072
|
+
|
|
1073
|
+
const Masked = withMask('blank', props => (node, data, onChange) => {
|
|
1074
|
+
const dataset = node.data ? node.data.dataset || {} : {};
|
|
1075
|
+
|
|
1076
|
+
if (dataset.component === 'blank') {
|
|
1077
|
+
// eslint-disable-next-line react/prop-types
|
|
1078
|
+
const {
|
|
1079
|
+
disabled,
|
|
1080
|
+
duplicates,
|
|
1081
|
+
correctResponse,
|
|
1082
|
+
feedback,
|
|
1083
|
+
showCorrectAnswer,
|
|
1084
|
+
emptyResponseAreaWidth,
|
|
1085
|
+
emptyResponseAreaHeight
|
|
1086
|
+
} = props;
|
|
1087
|
+
const choiceId = showCorrectAnswer ? correctResponse[dataset.id] : data[dataset.id]; // eslint-disable-next-line react/prop-types
|
|
1088
|
+
|
|
1089
|
+
const choice = choiceId && props.choices.find(c => c.id === choiceId);
|
|
1090
|
+
return /*#__PURE__*/React.createElement(DragDropTile, {
|
|
1091
|
+
key: `${node.type}-${dataset.id}`,
|
|
1092
|
+
correct: showCorrectAnswer || feedback && feedback[dataset.id],
|
|
1093
|
+
disabled: disabled,
|
|
1094
|
+
duplicates: duplicates,
|
|
1095
|
+
choice: choice,
|
|
1096
|
+
id: dataset.id,
|
|
1097
|
+
emptyResponseAreaWidth: emptyResponseAreaWidth,
|
|
1098
|
+
emptyResponseAreaHeight: emptyResponseAreaHeight,
|
|
1099
|
+
onChange: onChange
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
class DragInTheBlank extends React.Component {
|
|
1104
|
+
constructor(...args) {
|
|
1105
|
+
super(...args);
|
|
1106
|
+
|
|
1107
|
+
this.getPositionDirection = choicePosition => {
|
|
1108
|
+
let flexDirection;
|
|
1109
|
+
let justifyContent;
|
|
1110
|
+
let alignItems;
|
|
1111
|
+
|
|
1112
|
+
switch (choicePosition) {
|
|
1113
|
+
case 'left':
|
|
1114
|
+
flexDirection = 'row';
|
|
1115
|
+
alignItems = 'center';
|
|
1116
|
+
break;
|
|
1117
|
+
|
|
1118
|
+
case 'right':
|
|
1119
|
+
flexDirection = 'row-reverse';
|
|
1120
|
+
justifyContent = 'flex-end';
|
|
1121
|
+
alignItems = 'center';
|
|
1122
|
+
break;
|
|
1123
|
+
|
|
1124
|
+
case 'below':
|
|
1125
|
+
flexDirection = 'column-reverse';
|
|
1126
|
+
break;
|
|
1127
|
+
|
|
1128
|
+
default:
|
|
1129
|
+
// above
|
|
1130
|
+
flexDirection = 'column';
|
|
1131
|
+
break;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
return {
|
|
1135
|
+
flexDirection,
|
|
1136
|
+
justifyContent,
|
|
1137
|
+
alignItems
|
|
1138
|
+
};
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
UNSAFE_componentWillReceiveProps() {
|
|
1143
|
+
if (this.rootRef) {
|
|
1144
|
+
renderMath(this.rootRef);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
componentDidUpdate() {
|
|
1149
|
+
renderMath(this.rootRef);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
render() {
|
|
1153
|
+
const {
|
|
1154
|
+
markup,
|
|
1155
|
+
duplicates,
|
|
1156
|
+
layout,
|
|
1157
|
+
value,
|
|
1158
|
+
onChange,
|
|
1159
|
+
choicesPosition,
|
|
1160
|
+
choices,
|
|
1161
|
+
correctResponse,
|
|
1162
|
+
disabled,
|
|
1163
|
+
feedback,
|
|
1164
|
+
showCorrectAnswer,
|
|
1165
|
+
emptyResponseAreaWidth,
|
|
1166
|
+
emptyResponseAreaHeight
|
|
1167
|
+
} = this.props;
|
|
1168
|
+
const choicePosition = choicesPosition || 'below';
|
|
1169
|
+
|
|
1170
|
+
const style = _extends({
|
|
1171
|
+
display: 'flex',
|
|
1172
|
+
minWidth: '100px'
|
|
1173
|
+
}, this.getPositionDirection(choicePosition));
|
|
1174
|
+
|
|
1175
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
1176
|
+
ref: ref => ref && (this.rootRef = ref),
|
|
1177
|
+
style: style
|
|
1178
|
+
}, /*#__PURE__*/React.createElement(Choices, {
|
|
1179
|
+
choicePosition: choicePosition,
|
|
1180
|
+
duplicates: duplicates,
|
|
1181
|
+
choices: choices,
|
|
1182
|
+
value: value,
|
|
1183
|
+
disabled: disabled
|
|
1184
|
+
}), /*#__PURE__*/React.createElement(Masked, {
|
|
1185
|
+
elementType: 'drag-in-the-blank',
|
|
1186
|
+
markup: markup,
|
|
1187
|
+
layout: layout,
|
|
1188
|
+
value: value,
|
|
1189
|
+
choices: choices,
|
|
1190
|
+
onChange: onChange,
|
|
1191
|
+
disabled: disabled,
|
|
1192
|
+
duplicates: duplicates,
|
|
1193
|
+
feedback: feedback,
|
|
1194
|
+
correctResponse: correctResponse,
|
|
1195
|
+
showCorrectAnswer: showCorrectAnswer,
|
|
1196
|
+
emptyResponseAreaWidth: emptyResponseAreaWidth,
|
|
1197
|
+
emptyResponseAreaHeight: emptyResponseAreaHeight
|
|
1198
|
+
}));
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
}
|
|
1202
|
+
DragInTheBlank.propTypes = {
|
|
1203
|
+
markup: PropTypes.string,
|
|
1204
|
+
layout: PropTypes.object,
|
|
1205
|
+
choicesPosition: PropTypes.string,
|
|
1206
|
+
choices: PropTypes.arrayOf(PropTypes.shape({
|
|
1207
|
+
label: PropTypes.string,
|
|
1208
|
+
value: PropTypes.string
|
|
1209
|
+
})),
|
|
1210
|
+
value: PropTypes.object,
|
|
1211
|
+
onChange: PropTypes.func,
|
|
1212
|
+
duplicates: PropTypes.bool,
|
|
1213
|
+
disabled: PropTypes.bool,
|
|
1214
|
+
feedback: PropTypes.object,
|
|
1215
|
+
correctResponse: PropTypes.object,
|
|
1216
|
+
showCorrectAnswer: PropTypes.bool,
|
|
1217
|
+
emptyResponseAreaWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
1218
|
+
emptyResponseAreaHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
const styles$1 = () => ({
|
|
1222
|
+
editableHtmlCustom: {
|
|
1223
|
+
display: 'inline-block',
|
|
1224
|
+
verticalAlign: 'middle',
|
|
1225
|
+
margin: '4px',
|
|
1226
|
+
borderRadius: '4px',
|
|
1227
|
+
border: `1px solid ${color.black()}`
|
|
1228
|
+
},
|
|
1229
|
+
correct: {
|
|
1230
|
+
border: `1px solid ${color.correct()}`
|
|
1231
|
+
},
|
|
1232
|
+
incorrect: {
|
|
1233
|
+
border: `1px solid ${color.incorrect()}`
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
const MaskedInput = props => (node, data) => {
|
|
1238
|
+
var _node$data;
|
|
1239
|
+
|
|
1240
|
+
const {
|
|
1241
|
+
adjustedLimit,
|
|
1242
|
+
disabled,
|
|
1243
|
+
feedback,
|
|
1244
|
+
showCorrectAnswer,
|
|
1245
|
+
maxLength,
|
|
1246
|
+
spellCheck,
|
|
1247
|
+
classes,
|
|
1248
|
+
pluginProps,
|
|
1249
|
+
onChange
|
|
1250
|
+
} = props;
|
|
1251
|
+
const dataset = ((_node$data = node.data) == null ? void 0 : _node$data.dataset) || {};
|
|
1252
|
+
|
|
1253
|
+
if (dataset.component === 'input') {
|
|
1254
|
+
var _pluginProps$characte;
|
|
1255
|
+
|
|
1256
|
+
const correctAnswer = (props.choices && dataset && props.choices[dataset.id] || [])[0];
|
|
1257
|
+
const finalValue = showCorrectAnswer ? correctAnswer && correctAnswer.label : data[dataset.id] || '';
|
|
1258
|
+
const width = maxLength && maxLength[dataset.id];
|
|
1259
|
+
const feedbackStatus = feedback && feedback[dataset.id];
|
|
1260
|
+
const isCorrect = showCorrectAnswer || feedbackStatus === 'correct';
|
|
1261
|
+
const isIncorrect = !showCorrectAnswer && feedbackStatus === 'incorrect';
|
|
1262
|
+
|
|
1263
|
+
const handleInputChange = newValue => {
|
|
1264
|
+
const updatedValue = _extends({}, data, {
|
|
1265
|
+
[dataset.id]: newValue
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
onChange(updatedValue);
|
|
1269
|
+
};
|
|
1270
|
+
|
|
1271
|
+
const handleKeyDown = event => {
|
|
1272
|
+
// the keyCode value for the Enter/Return key is 13
|
|
1273
|
+
if (event.key === 'Enter' || event.keyCode === 13) {
|
|
1274
|
+
return false;
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1278
|
+
return /*#__PURE__*/React.createElement(EditableHtml, {
|
|
1279
|
+
id: dataset.id,
|
|
1280
|
+
key: `${node.type}-input-${dataset.id}`,
|
|
1281
|
+
disabled: showCorrectAnswer || disabled,
|
|
1282
|
+
disableUnderline: true,
|
|
1283
|
+
onChange: handleInputChange,
|
|
1284
|
+
markup: finalValue || '',
|
|
1285
|
+
charactersLimit: adjustedLimit ? width : 25,
|
|
1286
|
+
activePlugins: ['languageCharacters'],
|
|
1287
|
+
pluginProps: pluginProps,
|
|
1288
|
+
languageCharactersProps: [{
|
|
1289
|
+
language: 'spanish'
|
|
1290
|
+
}],
|
|
1291
|
+
spellCheck: spellCheck,
|
|
1292
|
+
width: `calc(${width}em + 42px)` // added 42px for left and right padding of editable-html
|
|
1293
|
+
,
|
|
1294
|
+
onKeyDown: handleKeyDown,
|
|
1295
|
+
autoWidthToolbar: true,
|
|
1296
|
+
toolbarOpts: {
|
|
1297
|
+
minWidth: 'auto',
|
|
1298
|
+
noBorder: true,
|
|
1299
|
+
isHidden: !!(pluginProps != null && (_pluginProps$characte = pluginProps.characters) != null && _pluginProps$characte.disabled)
|
|
1300
|
+
},
|
|
1301
|
+
className: classnames(classes.editableHtmlCustom, {
|
|
1302
|
+
[classes.correct]: isCorrect,
|
|
1303
|
+
[classes.incorrect]: isIncorrect
|
|
1304
|
+
})
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
|
|
1309
|
+
var constructedResponse = withStyles(styles$1)(withMask('input', MaskedInput));
|
|
1310
|
+
|
|
1311
|
+
var customizable = withMask('input', props => (node, data, onChange) => {
|
|
1312
|
+
const dataset = node.data ? node.data.dataset || {} : {};
|
|
1313
|
+
|
|
1314
|
+
if (dataset.component === 'input') {
|
|
1315
|
+
// eslint-disable-next-line react/prop-types
|
|
1316
|
+
// const { adjustedLimit, disabled, feedback, showCorrectAnswer, maxLength, spellCheck } = props;
|
|
1317
|
+
// the first answer is the correct one
|
|
1318
|
+
// eslint-disable-next-line react/prop-types
|
|
1319
|
+
// const correctAnswer = ((props.choices && dataset && props.choices[dataset.id]) || [])[0];
|
|
1320
|
+
// const finalValue = showCorrectAnswer ? correctAnswer && correctAnswer.label : data[dataset.id] || '';
|
|
1321
|
+
// const width = maxLength && maxLength[dataset.id];
|
|
1322
|
+
return props.customMarkMarkupComponent(dataset.id); // return (
|
|
1323
|
+
// <Input
|
|
1324
|
+
// key={`${node.type}-input-${dataset.id}`}
|
|
1325
|
+
// correct={feedback && feedback[dataset.id] && feedback[dataset.id] === 'correct'}
|
|
1326
|
+
// disabled={showCorrectAnswer || disabled}
|
|
1327
|
+
// value={finalValue}
|
|
1328
|
+
// id={dataset.id}
|
|
1329
|
+
// onChange={onChange}
|
|
1330
|
+
// showCorrectAnswer={showCorrectAnswer}
|
|
1331
|
+
// width={width}
|
|
1332
|
+
// charactersLimit={adjustedLimit ? width : 25}
|
|
1333
|
+
// isConstructedResponse={true}
|
|
1334
|
+
// spellCheck={spellCheck}
|
|
1335
|
+
// />
|
|
1336
|
+
// );
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
class Dropdown extends React.Component {
|
|
1341
|
+
constructor(props) {
|
|
1342
|
+
super(props);
|
|
1343
|
+
|
|
1344
|
+
this.handleClick = event => this.setState({
|
|
1345
|
+
anchorEl: event.currentTarget
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
this.handleClose = () => {
|
|
1349
|
+
const {
|
|
1350
|
+
value
|
|
1351
|
+
} = this.props;
|
|
1352
|
+
this.setState({
|
|
1353
|
+
anchorEl: null,
|
|
1354
|
+
previewValue: null,
|
|
1355
|
+
highlightedOptionId: null
|
|
1356
|
+
}); // clear displayed preview if no selection
|
|
1357
|
+
|
|
1358
|
+
if (!value && this.previewRef.current) {
|
|
1359
|
+
this.previewRef.current.innerHTML = '';
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
|
|
1363
|
+
this.handleHighlight = index => {
|
|
1364
|
+
const highlightedOptionId = `dropdown-option-${this.props.id}-${index}`; // preview on hover if nothing selected
|
|
1365
|
+
|
|
1366
|
+
const stateUpdate = {
|
|
1367
|
+
highlightedOptionId
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1370
|
+
if (!this.props.value) {
|
|
1371
|
+
stateUpdate.previewValue = this.props.choices[index].value;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
this.setState(stateUpdate);
|
|
1375
|
+
};
|
|
1376
|
+
|
|
1377
|
+
this.handleSelect = (value, index) => {
|
|
1378
|
+
this.props.onChange(this.props.id, value);
|
|
1379
|
+
this.handleHighlight(index);
|
|
1380
|
+
this.handleClose();
|
|
1381
|
+
};
|
|
1382
|
+
|
|
1383
|
+
this.handleHover = index => {
|
|
1384
|
+
const selectedValue = this.props.value;
|
|
1385
|
+
if (selectedValue) return;
|
|
1386
|
+
const highlightedOptionId = `dropdown-option-${this.props.id}-${index}`;
|
|
1387
|
+
const previewValue = this.state.previewValue;
|
|
1388
|
+
this.setState({
|
|
1389
|
+
highlightedOptionId,
|
|
1390
|
+
previewValue
|
|
1391
|
+
}, () => {
|
|
1392
|
+
// On hover, preview the math-rendered content inside the button if no value is selected.
|
|
1393
|
+
const ref = this.elementRefs[index];
|
|
1394
|
+
const preview = this.previewRef.current;
|
|
1395
|
+
|
|
1396
|
+
if (ref && preview) {
|
|
1397
|
+
preview.innerHTML = ref.innerHTML;
|
|
1398
|
+
}
|
|
1399
|
+
});
|
|
1400
|
+
};
|
|
1401
|
+
|
|
1402
|
+
this.state = {
|
|
1403
|
+
anchorEl: null,
|
|
1404
|
+
highlightedOptionId: null,
|
|
1405
|
+
menuWidth: null,
|
|
1406
|
+
previewValue: null
|
|
1407
|
+
};
|
|
1408
|
+
this.hiddenRef = /*#__PURE__*/React.createRef();
|
|
1409
|
+
this.buttonRef = /*#__PURE__*/React.createRef();
|
|
1410
|
+
this.previewRef = /*#__PURE__*/React.createRef();
|
|
1411
|
+
this.elementRefs = [];
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
componentDidMount() {
|
|
1415
|
+
// measure hidden menu width once
|
|
1416
|
+
if (this.hiddenRef.current && this.state.menuWidth === null) {
|
|
1417
|
+
this.setState({
|
|
1418
|
+
menuWidth: this.hiddenRef.current.clientWidth
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
componentDidUpdate(prevProps, prevState) {
|
|
1424
|
+
const hiddenEl = this.hiddenRef.current;
|
|
1425
|
+
const dropdownJustOpened = !prevState.anchorEl && this.state.anchorEl;
|
|
1426
|
+
|
|
1427
|
+
if (dropdownJustOpened) {
|
|
1428
|
+
this.elementRefs.forEach(ref => {
|
|
1429
|
+
if (!ref) return;
|
|
1430
|
+
const containsLatex = ref.querySelector('[data-latex], [data-raw]');
|
|
1431
|
+
const hasMathJax = ref.querySelector('mjx-container');
|
|
1432
|
+
const mathHandled = ref.querySelector('[data-math-handled="true"]');
|
|
1433
|
+
|
|
1434
|
+
if (containsLatex && (!mathHandled || !hasMathJax)) {
|
|
1435
|
+
renderMath(ref);
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
if (hiddenEl) {
|
|
1441
|
+
const newWidth = hiddenEl.clientWidth;
|
|
1442
|
+
|
|
1443
|
+
if (newWidth !== this.state.menuWidth) {
|
|
1444
|
+
this.elementRefs.forEach(ref => {
|
|
1445
|
+
if (ref) renderMath(ref);
|
|
1446
|
+
});
|
|
1447
|
+
renderMath(hiddenEl);
|
|
1448
|
+
this.setState({
|
|
1449
|
+
menuWidth: newWidth
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
getLabel(choices, value) {
|
|
1456
|
+
const found = (choices || []).find(choice => choice.value === value);
|
|
1457
|
+
return found ? found.label.trim() : undefined;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
render() {
|
|
1461
|
+
const {
|
|
1462
|
+
classes,
|
|
1463
|
+
id,
|
|
1464
|
+
correct,
|
|
1465
|
+
disabled,
|
|
1466
|
+
value,
|
|
1467
|
+
choices,
|
|
1468
|
+
showCorrectAnswer,
|
|
1469
|
+
singleQuery,
|
|
1470
|
+
correctValue
|
|
1471
|
+
} = this.props;
|
|
1472
|
+
const {
|
|
1473
|
+
anchorEl
|
|
1474
|
+
} = this.state;
|
|
1475
|
+
const open = Boolean(anchorEl);
|
|
1476
|
+
const buttonId = `dropdown-button-${id}`;
|
|
1477
|
+
const menuId = `dropdown-menu-${id}`;
|
|
1478
|
+
const valueDisplayId = `dropdown-value-${id}`; // Determine the class for disabled state, view mode and evaluate mode
|
|
1479
|
+
|
|
1480
|
+
let disabledClass; // Reset elementRefs before each render to avoid stale references
|
|
1481
|
+
|
|
1482
|
+
this.elementRefs = [];
|
|
1483
|
+
|
|
1484
|
+
if (disabled && correct !== undefined) {
|
|
1485
|
+
disabledClass = correct || showCorrectAnswer ? classes.disabledCorrect : classes.disabledIncorrect;
|
|
1486
|
+
} // Create distinct, visually hidden labels for each dropdown
|
|
1487
|
+
|
|
1488
|
+
|
|
1489
|
+
const incrementedId = parseInt(id, 10) + 1;
|
|
1490
|
+
const labelId = singleQuery ? 'Query-label' : `Query-label-${incrementedId}`;
|
|
1491
|
+
const labelText = singleQuery ? 'Query' : `Query ${incrementedId}`; // Changed from Select to Button for dropdown to enhance accessibility. This modification offers explicit control over aria attributes and focuses management, ensuring the dropdown is compliant with accessibility standards. The use of Button and Menu components allows for better handling of keyboard interactions and provides accessible labels and menus, aligning with WCAG guidelines and improving usability for assistive technology users.
|
|
1492
|
+
|
|
1493
|
+
let correctnessIcon = null;
|
|
1494
|
+
|
|
1495
|
+
if (disabled && correct !== undefined) {
|
|
1496
|
+
correctnessIcon = correct || showCorrectAnswer ? /*#__PURE__*/React.createElement(Check, {
|
|
1497
|
+
className: classnames(classes.correctnessIndicatorIcon, classes.correctIcon)
|
|
1498
|
+
}) : /*#__PURE__*/React.createElement(Close, {
|
|
1499
|
+
className: classnames(classes.correctnessIndicatorIcon, classes.incorrectIcon)
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
|
|
1504
|
+
ref: this.hiddenRef,
|
|
1505
|
+
style: {
|
|
1506
|
+
position: 'absolute',
|
|
1507
|
+
visibility: 'hidden',
|
|
1508
|
+
top: 0,
|
|
1509
|
+
left: 0
|
|
1510
|
+
},
|
|
1511
|
+
tabIndex: -1,
|
|
1512
|
+
"aria-hidden": "true"
|
|
1513
|
+
}, (choices || []).map((c, index) => /*#__PURE__*/React.createElement(MenuItem, {
|
|
1514
|
+
key: index,
|
|
1515
|
+
classes: {
|
|
1516
|
+
root: classes.menuRoot,
|
|
1517
|
+
selected: classes.selected
|
|
1518
|
+
},
|
|
1519
|
+
tabIndex: -1,
|
|
1520
|
+
"aria-hidden": "true"
|
|
1521
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
1522
|
+
className: classes.label,
|
|
1523
|
+
dangerouslySetInnerHTML: {
|
|
1524
|
+
__html: c.label
|
|
1525
|
+
}
|
|
1526
|
+
})))), /*#__PURE__*/React.createElement(InputLabel, {
|
|
1527
|
+
className: classes.srOnly,
|
|
1528
|
+
id: labelId,
|
|
1529
|
+
tabIndex: -1,
|
|
1530
|
+
"aria-hidden": "true"
|
|
1531
|
+
}, labelText), /*#__PURE__*/React.createElement(Button, {
|
|
1532
|
+
ref: this.buttonRef,
|
|
1533
|
+
style: _extends({}, this.state.menuWidth && {
|
|
1534
|
+
minWidth: `calc(${this.state.menuWidth}px + 8px)`
|
|
1535
|
+
}, {
|
|
1536
|
+
borderWidth: open ? '2px' : '1px',
|
|
1537
|
+
transition: 'border-width 0.2s ease-in-out'
|
|
1538
|
+
}),
|
|
1539
|
+
"aria-controls": open ? menuId : undefined,
|
|
1540
|
+
"aria-haspopup": "listbox",
|
|
1541
|
+
"aria-expanded": open ? 'true' : undefined,
|
|
1542
|
+
"aria-activedescendant": this.state.highlightedOptionId,
|
|
1543
|
+
onClick: this.handleClick,
|
|
1544
|
+
classes: {
|
|
1545
|
+
root: classes.root,
|
|
1546
|
+
disabled: disabledClass
|
|
1547
|
+
},
|
|
1548
|
+
disabled: disabled,
|
|
1549
|
+
id: buttonId,
|
|
1550
|
+
role: "combobox",
|
|
1551
|
+
"aria-label": `Select an option for ${labelText}`,
|
|
1552
|
+
"aria-labelledby": valueDisplayId
|
|
1553
|
+
}, correctnessIcon, /*#__PURE__*/React.createElement("span", {
|
|
1554
|
+
id: valueDisplayId,
|
|
1555
|
+
ref: this.previewRef,
|
|
1556
|
+
className: classes.label,
|
|
1557
|
+
dangerouslySetInnerHTML: {
|
|
1558
|
+
__html: correctValue ? correctValue : open && this.state.previewValue ? this.getLabel(choices, this.state.previewValue) : this.getLabel(choices, value) || ''
|
|
1559
|
+
}
|
|
1560
|
+
}), open ? /*#__PURE__*/React.createElement(ArrowDropUpIcon, null) : /*#__PURE__*/React.createElement(ArrowDropDownIcon, null)), /*#__PURE__*/React.createElement(Menu, {
|
|
1561
|
+
id: menuId,
|
|
1562
|
+
anchorEl: anchorEl,
|
|
1563
|
+
className: classes.selectMenu,
|
|
1564
|
+
keepMounted: true,
|
|
1565
|
+
open: open,
|
|
1566
|
+
onClose: this.handleClose,
|
|
1567
|
+
getContentAnchorEl: null,
|
|
1568
|
+
anchorOrigin: {
|
|
1569
|
+
vertical: 'bottom',
|
|
1570
|
+
horizontal: 'left'
|
|
1571
|
+
},
|
|
1572
|
+
transformOrigin: {
|
|
1573
|
+
vertical: 'top',
|
|
1574
|
+
horizontal: 'left'
|
|
1575
|
+
},
|
|
1576
|
+
PaperProps: this.state.menuWidth ? {
|
|
1577
|
+
style: {
|
|
1578
|
+
minWidth: this.state.menuWidth,
|
|
1579
|
+
padding: '4px'
|
|
1580
|
+
}
|
|
1581
|
+
} : undefined,
|
|
1582
|
+
MenuListProps: {
|
|
1583
|
+
'aria-labelledby': buttonId,
|
|
1584
|
+
role: 'listbox',
|
|
1585
|
+
disablePadding: true
|
|
1586
|
+
}
|
|
1587
|
+
}, (choices || []).map((c, index) => {
|
|
1588
|
+
const optionId = `dropdown-option-${id}-${index}`;
|
|
1589
|
+
return /*#__PURE__*/React.createElement(MenuItem, {
|
|
1590
|
+
id: optionId,
|
|
1591
|
+
classes: {
|
|
1592
|
+
root: classes.menuRoot,
|
|
1593
|
+
selected: classes.selected
|
|
1594
|
+
},
|
|
1595
|
+
key: `${c.label}-${index}`,
|
|
1596
|
+
value: c.value,
|
|
1597
|
+
onClick: () => this.handleSelect(c.value, index),
|
|
1598
|
+
role: "option",
|
|
1599
|
+
"aria-selected": this.state.highlightedOptionId === optionId ? 'true' : undefined,
|
|
1600
|
+
onMouseOver: () => this.handleHover(index)
|
|
1601
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
1602
|
+
ref: ref => this.elementRefs[index] = ref,
|
|
1603
|
+
className: classes.label,
|
|
1604
|
+
dangerouslySetInnerHTML: {
|
|
1605
|
+
__html: c.label
|
|
1606
|
+
}
|
|
1607
|
+
}), /*#__PURE__*/React.createElement("span", {
|
|
1608
|
+
className: classes.selectedIndicator,
|
|
1609
|
+
dangerouslySetInnerHTML: {
|
|
1610
|
+
__html: c.value === value ? ' ✓' : ''
|
|
1611
|
+
}
|
|
1612
|
+
}));
|
|
1613
|
+
})));
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
Dropdown.propTypes = {
|
|
1619
|
+
id: PropTypes.string,
|
|
1620
|
+
value: PropTypes.string,
|
|
1621
|
+
disabled: PropTypes.bool,
|
|
1622
|
+
onChange: PropTypes.func,
|
|
1623
|
+
classes: PropTypes.object,
|
|
1624
|
+
correct: PropTypes.bool,
|
|
1625
|
+
choices: PropTypes.arrayOf(PropTypes.shape({
|
|
1626
|
+
value: PropTypes.string,
|
|
1627
|
+
label: PropTypes.string
|
|
1628
|
+
})),
|
|
1629
|
+
showCorrectAnswer: PropTypes.bool,
|
|
1630
|
+
singleQuery: PropTypes.bool,
|
|
1631
|
+
correctValue: PropTypes.string
|
|
1632
|
+
};
|
|
1633
|
+
|
|
1634
|
+
const styles = () => ({
|
|
1635
|
+
root: {
|
|
1636
|
+
color: color.text(),
|
|
1637
|
+
border: `1px solid ${color.borderGray()}`,
|
|
1638
|
+
borderRadius: '4px',
|
|
1639
|
+
justifyContent: 'space-between',
|
|
1640
|
+
backgroundColor: color.background(),
|
|
1641
|
+
position: 'relative',
|
|
1642
|
+
height: '45px',
|
|
1643
|
+
width: 'fit-content',
|
|
1644
|
+
margin: '2px',
|
|
1645
|
+
textTransform: 'none',
|
|
1646
|
+
'& span': {
|
|
1647
|
+
paddingRight: '5px'
|
|
1648
|
+
},
|
|
1649
|
+
'& svg': {
|
|
1650
|
+
position: 'absolute',
|
|
1651
|
+
right: 0,
|
|
1652
|
+
top: 'calc(50% - 12px)',
|
|
1653
|
+
pointerEvents: 'none',
|
|
1654
|
+
color: color.text(),
|
|
1655
|
+
marginLeft: '5px'
|
|
1656
|
+
},
|
|
1657
|
+
'&:focus, &:focus-visible': {
|
|
1658
|
+
outline: `3px solid ${color.tertiary()}`,
|
|
1659
|
+
outlineOffset: '2px',
|
|
1660
|
+
borderWidth: '3px'
|
|
1661
|
+
}
|
|
1662
|
+
},
|
|
1663
|
+
disabledCorrect: {
|
|
1664
|
+
borderWidth: '2px',
|
|
1665
|
+
borderColor: color.correct(),
|
|
1666
|
+
color: `${color.text()} !important`
|
|
1667
|
+
},
|
|
1668
|
+
disabledIncorrect: {
|
|
1669
|
+
borderWidth: '2px',
|
|
1670
|
+
borderColor: color.incorrectWithIcon(),
|
|
1671
|
+
color: `${color.text()} !important`
|
|
1672
|
+
},
|
|
1673
|
+
selectMenu: {
|
|
1674
|
+
backgroundColor: color.background(),
|
|
1675
|
+
border: `1px solid ${color.correct()} !important`,
|
|
1676
|
+
'&:hover': {
|
|
1677
|
+
border: `1px solid ${color.text()} `,
|
|
1678
|
+
borderColor: 'initial'
|
|
1679
|
+
},
|
|
1680
|
+
'&:focus': {
|
|
1681
|
+
border: `1px solid ${color.text()}`,
|
|
1682
|
+
borderColor: 'initial'
|
|
1683
|
+
},
|
|
1684
|
+
// remove default padding on the inner list
|
|
1685
|
+
'& .MuiList-root': {
|
|
1686
|
+
padding: 0
|
|
1687
|
+
}
|
|
1688
|
+
},
|
|
1689
|
+
selected: {
|
|
1690
|
+
color: `${color.text()} !important`,
|
|
1691
|
+
backgroundColor: `${color.background()} !important`,
|
|
1692
|
+
'&:hover': {
|
|
1693
|
+
color: color.text(),
|
|
1694
|
+
backgroundColor: `${color.dropdownBackground()} !important`
|
|
1695
|
+
}
|
|
1696
|
+
},
|
|
1697
|
+
menuRoot: {
|
|
1698
|
+
color: color.text(),
|
|
1699
|
+
backgroundColor: color.background(),
|
|
1700
|
+
'&:focus, &:focus-visible': {
|
|
1701
|
+
outline: `3px solid ${color.tertiary()}`,
|
|
1702
|
+
outlineOffset: '-1px' // keeps it inside the item
|
|
1703
|
+
|
|
1704
|
+
},
|
|
1705
|
+
'&:focus': {
|
|
1706
|
+
color: color.text(),
|
|
1707
|
+
backgroundColor: color.background()
|
|
1708
|
+
},
|
|
1709
|
+
'&:hover': {
|
|
1710
|
+
color: color.text(),
|
|
1711
|
+
backgroundColor: color.dropdownBackground()
|
|
1712
|
+
},
|
|
1713
|
+
boxSizing: 'border-box',
|
|
1714
|
+
padding: '25px',
|
|
1715
|
+
borderRadius: '4px'
|
|
1716
|
+
},
|
|
1717
|
+
label: {
|
|
1718
|
+
fontSize: 'max(1rem, 14px)'
|
|
1719
|
+
},
|
|
1720
|
+
selectedIndicator: {
|
|
1721
|
+
fontSize: 'max(1rem, 14px)',
|
|
1722
|
+
position: 'absolute',
|
|
1723
|
+
right: '10px'
|
|
1724
|
+
},
|
|
1725
|
+
srOnly: {
|
|
1726
|
+
position: 'absolute',
|
|
1727
|
+
left: '-10000px',
|
|
1728
|
+
top: 'auto',
|
|
1729
|
+
width: '1px',
|
|
1730
|
+
height: '1px',
|
|
1731
|
+
overflow: 'hidden'
|
|
1732
|
+
},
|
|
1733
|
+
correctnessIndicatorIcon: {
|
|
1734
|
+
color: `${color.white()} !important`,
|
|
1735
|
+
position: 'absolute',
|
|
1736
|
+
top: '-8px !important',
|
|
1737
|
+
left: '-8px',
|
|
1738
|
+
marginLeft: '0 !important',
|
|
1739
|
+
borderRadius: '50%',
|
|
1740
|
+
fontSize: '16px',
|
|
1741
|
+
padding: '2px'
|
|
1742
|
+
},
|
|
1743
|
+
correctIcon: {
|
|
1744
|
+
backgroundColor: color.correct()
|
|
1745
|
+
},
|
|
1746
|
+
incorrectIcon: {
|
|
1747
|
+
backgroundColor: color.incorrectWithIcon()
|
|
1748
|
+
}
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1751
|
+
var Dropdown$1 = withStyles(styles)(Dropdown);
|
|
1752
|
+
|
|
1753
|
+
var inlineDropdown = withMask('dropdown', props => (node, data, onChange) => {
|
|
1754
|
+
const dataset = node.data ? node.data.dataset || {} : {};
|
|
1755
|
+
|
|
1756
|
+
if (dataset.component === 'dropdown') {
|
|
1757
|
+
// eslint-disable-next-line react/prop-types
|
|
1758
|
+
const {
|
|
1759
|
+
choices,
|
|
1760
|
+
disabled,
|
|
1761
|
+
feedback,
|
|
1762
|
+
showCorrectAnswer
|
|
1763
|
+
} = props;
|
|
1764
|
+
const correctAnswer = choices && choices[dataset.id] && choices[dataset.id].find(c => c.correct);
|
|
1765
|
+
const finalChoice = showCorrectAnswer ? correctAnswer && correctAnswer.value : data[dataset.id];
|
|
1766
|
+
return /*#__PURE__*/React.createElement(Dropdown$1, {
|
|
1767
|
+
key: `${node.type}-dropdown-${dataset.id}`,
|
|
1768
|
+
correct: feedback && feedback[dataset.id] && feedback[dataset.id] === 'correct',
|
|
1769
|
+
disabled: disabled || showCorrectAnswer,
|
|
1770
|
+
value: finalChoice,
|
|
1771
|
+
correctValue: showCorrectAnswer ? correctAnswer && correctAnswer.label : undefined,
|
|
1772
|
+
id: dataset.id,
|
|
1773
|
+
onChange: onChange,
|
|
1774
|
+
choices: choices[dataset.id],
|
|
1775
|
+
showCorrectAnswer: showCorrectAnswer,
|
|
1776
|
+
singleQuery: Object.keys(choices).length == 1
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
});
|
|
1780
|
+
|
|
1781
|
+
export { constructedResponse as ConstructedResponse, customizable as Customizable, DragInTheBlank, inlineDropdown as InlineDropdown, buildLayoutFromMarkup, componentize, withMask };
|
|
1782
|
+
//# sourceMappingURL=index.js.map
|