@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 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 ? ' &check;' : ''
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