@pie-element/image-cloze-association 10.2.0 → 10.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/configure/CHANGELOG.md +4 -0
- package/configure/package.json +3 -3
- package/lib/image-container.js +12 -3
- package/lib/image-container.js.map +1 -1
- package/lib/image-drop-target.js +46 -6
- package/lib/image-drop-target.js.map +1 -1
- package/lib/keyboard-coordinates.js +111 -0
- package/lib/keyboard-coordinates.js.map +1 -0
- package/lib/possible-response.js +36 -5
- package/lib/possible-response.js.map +1 -1
- package/lib/possible-responses.js +39 -19
- package/lib/possible-responses.js.map +1 -1
- package/lib/root.js +120 -6
- package/lib/root.js.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/image-drop-target.test.jsx +169 -0
- package/src/__tests__/keyboard-coordinates.test.js +127 -0
- package/src/__tests__/possible-response.test.jsx +113 -0
- package/src/__tests__/possible-responses.test.jsx +63 -0
- package/src/__tests__/root.test.jsx +261 -2
- package/src/image-container.jsx +9 -0
- package/src/image-drop-target.jsx +44 -1
- package/src/keyboard-coordinates.js +115 -0
- package/src/possible-response.jsx +45 -1
- package/src/possible-responses.jsx +37 -16
- package/src/root.jsx +117 -2
package/src/root.jsx
CHANGED
|
@@ -16,9 +16,16 @@ import InteractiveSection from './interactive-section';
|
|
|
16
16
|
import PossibleResponses from './possible-responses';
|
|
17
17
|
import { getUnansweredAnswers, getAnswersCorrectness } from './utils-correctness';
|
|
18
18
|
import PossibleResponse from './possible-response';
|
|
19
|
+
import { closestDroppableKeyboardCoordinates } from './keyboard-coordinates';
|
|
19
20
|
|
|
20
21
|
const generateId = () => Math.random().toString(36).substring(2) + new Date().getTime().toString(36);
|
|
21
22
|
|
|
23
|
+
// A click that lands right after a real drag gesture ends (pointer drag-and-drop, or
|
|
24
|
+
// the browser's own synthetic click for a keyboard Space/Enter) must be ignored by the
|
|
25
|
+
// click-to-select/click-to-place handlers below, or it would immediately reopen or
|
|
26
|
+
// re-trigger a selection for a drag that just completed.
|
|
27
|
+
const CLICK_AFTER_DRAG_GUARD_MS = 250;
|
|
28
|
+
|
|
22
29
|
const StyledUiLayout = styled(UiLayout)({
|
|
23
30
|
color: color.text(),
|
|
24
31
|
backgroundColor: color.background(),
|
|
@@ -76,7 +83,9 @@ export class ImageClozeAssociationComponent extends React.Component {
|
|
|
76
83
|
maxResponsePerZone: maxResponsePerZone || 1,
|
|
77
84
|
showCorrect: false,
|
|
78
85
|
isValidDrop: false,
|
|
86
|
+
selectedResponse: null,
|
|
79
87
|
};
|
|
88
|
+
this.lastDragEndAt = 0;
|
|
80
89
|
}
|
|
81
90
|
|
|
82
91
|
onDragStart = (event) => {
|
|
@@ -86,6 +95,7 @@ export class ImageClozeAssociationComponent extends React.Component {
|
|
|
86
95
|
this.setState({
|
|
87
96
|
draggingElement: active.data.current,
|
|
88
97
|
isValidDrop: false,
|
|
98
|
+
selectedResponse: active.data.current,
|
|
89
99
|
});
|
|
90
100
|
}
|
|
91
101
|
};
|
|
@@ -112,6 +122,9 @@ export class ImageClozeAssociationComponent extends React.Component {
|
|
|
112
122
|
isValidDrop: shouldDisableAnimation,
|
|
113
123
|
});
|
|
114
124
|
|
|
125
|
+
this.cancelSelection();
|
|
126
|
+
this.lastDragEndAt = Date.now();
|
|
127
|
+
|
|
115
128
|
if (!over || !active) {
|
|
116
129
|
return;
|
|
117
130
|
}
|
|
@@ -121,7 +134,9 @@ export class ImageClozeAssociationComponent extends React.Component {
|
|
|
121
134
|
}
|
|
122
135
|
|
|
123
136
|
if (over.id === 'ica-board') {
|
|
124
|
-
|
|
137
|
+
if (draggedItem.containerIndex !== undefined) {
|
|
138
|
+
this.handleOnAnswerRemove(draggedItem);
|
|
139
|
+
}
|
|
125
140
|
return;
|
|
126
141
|
}
|
|
127
142
|
|
|
@@ -130,6 +145,94 @@ export class ImageClozeAssociationComponent extends React.Component {
|
|
|
130
145
|
}
|
|
131
146
|
};
|
|
132
147
|
|
|
148
|
+
onDragCancel = () => {
|
|
149
|
+
this.setState({ draggingElement: { id: '', value: '' } });
|
|
150
|
+
this.cancelSelection();
|
|
151
|
+
this.lastDragEndAt = Date.now();
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
isSameResponse = (a, b) => !!a && !!b && a.id === b.id && a.containerIndex === b.containerIndex;
|
|
155
|
+
|
|
156
|
+
// Click-to-select semantics: selecting the currently-selected response again clears
|
|
157
|
+
// the selection instead of re-selecting it.
|
|
158
|
+
toggleResponseSelection = (data) => {
|
|
159
|
+
this.setState((state) => ({
|
|
160
|
+
selectedResponse: this.isSameResponse(state.selectedResponse, data) ? null : data,
|
|
161
|
+
}));
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
cancelSelection = () => {
|
|
165
|
+
this.setState({ selectedResponse: null });
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// If a real dnd-kit drag (started via keyboard Space/Enter) is still live when a
|
|
169
|
+
// click completes the placement below, it needs to be cleanly ended — otherwise
|
|
170
|
+
// dnd-kit would still think a drag is in progress. Escape is already configured as
|
|
171
|
+
// this sensor's cancel key (see the keyboardCodes passed to DragProvider below), and
|
|
172
|
+
// dispatching it as a real DOM KeyboardEvent is how dnd-kit's own document-level
|
|
173
|
+
// listener is reached from outside its sensor.
|
|
174
|
+
//
|
|
175
|
+
// Only dispatch when a drag is actually live (draggingElement.id is truthy) — this is
|
|
176
|
+
// a synthetic Escape keydown on `document`, so an unconditional dispatch would also be
|
|
177
|
+
// observed by any other document-level Escape listener (host player modals/dialogs,
|
|
178
|
+
// or another mounted instance of this same component) even when nothing here actually
|
|
179
|
+
// needed cancelling.
|
|
180
|
+
endAnyLiveKeyboardDrag = () => {
|
|
181
|
+
if (!this.state.draggingElement.id) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { code: 'Escape', bubbles: true, cancelable: true }));
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
placeSelectedResponse = (containerIndex) => {
|
|
189
|
+
const { selectedResponse } = this.state;
|
|
190
|
+
|
|
191
|
+
if (!selectedResponse) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (containerIndex === undefined) {
|
|
196
|
+
// Placing into the pool = removing it from wherever it currently is
|
|
197
|
+
if (selectedResponse.containerIndex !== undefined) {
|
|
198
|
+
this.handleOnAnswerRemove(selectedResponse);
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
this.handleOnAnswerSelect(selectedResponse, containerIndex);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
this.cancelSelection();
|
|
205
|
+
this.endAnyLiveKeyboardDrag();
|
|
206
|
+
this.lastDragEndAt = Date.now();
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
isClickSoonAfterDragEnd = () => Date.now() - this.lastDragEndAt < CLICK_AFTER_DRAG_GUARD_MS;
|
|
210
|
+
|
|
211
|
+
onResponseClick = (data) => {
|
|
212
|
+
if (this.isClickSoonAfterDragEnd()) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// A click that selects/deselects/switches a tile must end any dnd-kit drag that's
|
|
217
|
+
// still live from an earlier keyboard Space/Enter pick-up first — otherwise dnd-kit
|
|
218
|
+
// keeps thinking that earlier item is being dragged (it ignores new sensor
|
|
219
|
+
// activation while a drag is active) while selectedResponse visually points at
|
|
220
|
+
// whatever this click just selected. Ending the stale drag first (rather than
|
|
221
|
+
// after) matters: ending it also cancels the current selection as a side effect
|
|
222
|
+
// (see onDragCancel above), so doing it before
|
|
223
|
+
// toggleResponseSelection lets this click's own selection be the one that sticks.
|
|
224
|
+
this.endAnyLiveKeyboardDrag();
|
|
225
|
+
this.toggleResponseSelection(data);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
onPlacementClick = (containerIndex) => {
|
|
229
|
+
if (this.isClickSoonAfterDragEnd()) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
this.placeSelectedResponse(containerIndex);
|
|
234
|
+
};
|
|
235
|
+
|
|
133
236
|
renderDragOverlay = () => {
|
|
134
237
|
const { draggingElement } = this.state;
|
|
135
238
|
const { model } = this.props;
|
|
@@ -353,6 +456,9 @@ export class ImageClozeAssociationComponent extends React.Component {
|
|
|
353
456
|
responseContainerPadding,
|
|
354
457
|
imageDropTargetPadding,
|
|
355
458
|
maxResponsePerZone,
|
|
459
|
+
selectedResponse: this.state.selectedResponse,
|
|
460
|
+
onSelectClick: this.onResponseClick,
|
|
461
|
+
onPlacementClick: this.onPlacementClick,
|
|
356
462
|
};
|
|
357
463
|
|
|
358
464
|
const renderImage = () => (
|
|
@@ -381,13 +487,22 @@ export class ImageClozeAssociationComponent extends React.Component {
|
|
|
381
487
|
}}
|
|
382
488
|
isVertical={isVertical}
|
|
383
489
|
minHeight={isVertical ? image?.height : undefined}
|
|
490
|
+
selectedResponse={this.state.selectedResponse}
|
|
491
|
+
onSelectClick={this.onResponseClick}
|
|
492
|
+
onPlacementClick={this.onPlacementClick}
|
|
384
493
|
/>
|
|
385
494
|
</React.Fragment>
|
|
386
495
|
);
|
|
387
496
|
};
|
|
388
497
|
|
|
389
498
|
return (
|
|
390
|
-
<DragProvider
|
|
499
|
+
<DragProvider
|
|
500
|
+
onDragStart={this.onDragStart}
|
|
501
|
+
onDragEnd={this.onDragEnd}
|
|
502
|
+
onDragCancel={this.onDragCancel}
|
|
503
|
+
keyboardCoordinateGetter={closestDroppableKeyboardCoordinates}
|
|
504
|
+
keyboardCodes={{ start: ['Space', 'Enter'], cancel: ['Escape'], end: ['Space', 'Enter'] }}
|
|
505
|
+
>
|
|
391
506
|
<StyledUiLayout extraCSSRules={extraCSSRules} id={'main-container'} fontSizeFactor={fontSizeFactor}>
|
|
392
507
|
{showTeacherInstructions && (
|
|
393
508
|
<StyledTeacherInstructions
|