postext 1.60.0 → 1.61.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/dist/__tests__/exercises/digital.test.js +38 -1
- package/dist/__tests__/exercises/digital.test.js.map +1 -1
- package/dist/__tests__/exports.test.js +16 -0
- package/dist/__tests__/exports.test.js.map +1 -1
- package/dist/__tests__/figureStyle.test.d.ts +2 -0
- package/dist/__tests__/figureStyle.test.d.ts.map +1 -0
- package/dist/__tests__/figureStyle.test.js +218 -0
- package/dist/__tests__/figureStyle.test.js.map +1 -0
- package/dist/__tests__/headingLetterSpacing.test.js +1 -1
- package/dist/__tests__/headingLetterSpacing.test.js.map +1 -1
- package/dist/__tests__/pipeline/paragraphStyleFaces.test.js +1 -1
- package/dist/__tests__/pipeline/paragraphStyleFaces.test.js.map +1 -1
- package/dist/canvas-backend/verticalText.d.ts.map +1 -1
- package/dist/canvas-backend/verticalText.js +5 -2
- package/dist/canvas-backend/verticalText.js.map +1 -1
- package/dist/configWarnings.d.ts.map +1 -1
- package/dist/configWarnings.js +18 -4
- package/dist/configWarnings.js.map +1 -1
- package/dist/defaults/bodyText.d.ts.map +1 -1
- package/dist/defaults/bodyText.js +8 -1
- package/dist/defaults/bodyText.js.map +1 -1
- package/dist/defaults/captionStyle.d.ts.map +1 -1
- package/dist/defaults/captionStyle.js +7 -2
- package/dist/defaults/captionStyle.js.map +1 -1
- package/dist/defaults/headerFooter.d.ts.map +1 -1
- package/dist/defaults/headerFooter.js +3 -1
- package/dist/defaults/headerFooter.js.map +1 -1
- package/dist/defaults/headings.d.ts.map +1 -1
- package/dist/defaults/headings.js +15 -3
- package/dist/defaults/headings.js.map +1 -1
- package/dist/defaults/paragraphStyles.d.ts.map +1 -1
- package/dist/defaults/paragraphStyles.js +4 -1
- package/dist/defaults/paragraphStyles.js.map +1 -1
- package/dist/defaults/tableStyle.d.ts.map +1 -1
- package/dist/defaults/tableStyle.js +5 -2
- package/dist/defaults/tableStyle.js.map +1 -1
- package/dist/design/elementKeys.d.ts +1 -0
- package/dist/design/elementKeys.d.ts.map +1 -1
- package/dist/design/elementKeys.js +1 -1
- package/dist/design/elementKeys.js.map +1 -1
- package/dist/exercises/digital.d.ts +7 -0
- package/dist/exercises/digital.d.ts.map +1 -1
- package/dist/exercises/digital.js +8 -1
- package/dist/exercises/digital.js.map +1 -1
- package/dist/exercises/expand.d.ts +4 -0
- package/dist/exercises/expand.d.ts.map +1 -1
- package/dist/exercises/expand.js +65 -4
- package/dist/exercises/expand.js.map +1 -1
- package/dist/exercises/record.d.ts +4 -0
- package/dist/exercises/record.d.ts.map +1 -1
- package/dist/exercises/runtime.d.ts +6 -0
- package/dist/exercises/runtime.d.ts.map +1 -1
- package/dist/exercises/runtime.js +432 -59
- package/dist/exercises/runtime.js.map +1 -1
- package/dist/exercises/styles.d.ts +1 -0
- package/dist/exercises/styles.d.ts.map +1 -1
- package/dist/exercises/styles.js +3 -0
- package/dist/exercises/styles.js.map +1 -1
- package/dist/fonts/featureFaces.d.ts +32 -0
- package/dist/fonts/featureFaces.d.ts.map +1 -0
- package/dist/fonts/featureFaces.js +166 -0
- package/dist/fonts/featureFaces.js.map +1 -0
- package/dist/fonts/figureStyle.d.ts +69 -0
- package/dist/fonts/figureStyle.d.ts.map +1 -0
- package/dist/fonts/figureStyle.js +196 -0
- package/dist/fonts/figureStyle.js.map +1 -0
- package/dist/fonts/freezeFeatures.d.ts +10 -0
- package/dist/fonts/freezeFeatures.d.ts.map +1 -0
- package/dist/fonts/freezeFeatures.js +372 -0
- package/dist/fonts/freezeFeatures.js.map +1 -0
- package/dist/fonts/glyphFallback.d.ts.map +1 -1
- package/dist/fonts/glyphFallback.js +6 -3
- package/dist/fonts/glyphFallback.js.map +1 -1
- package/dist/fonts/prepare.d.ts.map +1 -1
- package/dist/fonts/prepare.js +22 -4
- package/dist/fonts/prepare.js.map +1 -1
- package/dist/fonts/usage.d.ts +10 -0
- package/dist/fonts/usage.d.ts.map +1 -1
- package/dist/fonts/usage.js +97 -0
- package/dist/fonts/usage.js.map +1 -1
- package/dist/html-backend.d.ts.map +1 -1
- package/dist/html-backend.js +8 -5
- package/dist/html-backend.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/pipeline/headingStyles.d.ts.map +1 -1
- package/dist/pipeline/headingStyles.js +3 -0
- package/dist/pipeline/headingStyles.js.map +1 -1
- package/dist/types.d.ts +45 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/worker/client.d.ts.map +1 -1
- package/dist/worker/client.js +13 -2
- package/dist/worker/client.js.map +1 -1
- package/dist/worker/fonts.d.ts +1 -1
- package/dist/worker/fonts.d.ts.map +1 -1
- package/dist/worker/fonts.js +1 -1
- package/dist/worker/fonts.js.map +1 -1
- package/dist/worker/protocol.d.ts +4 -0
- package/dist/worker/protocol.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -165,6 +165,32 @@ function exerciseRuntime() {
|
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
|
+
/** The buttons an exercise's bar needs: a check where an answer is known
|
|
169
|
+
* for a slot the reader fills, the solution where there is one to show,
|
|
170
|
+
* a fresh start where there is anything to answer. */
|
|
171
|
+
function actionsOf(record) {
|
|
172
|
+
let anyMarkRight = false;
|
|
173
|
+
for (let i = 0; i < record.slots.length; i++)
|
|
174
|
+
if (record.slots[i].kind === 'mark' && (record.slots[i].correct || []).indexOf(1) >= 0)
|
|
175
|
+
anyMarkRight = true;
|
|
176
|
+
let check = false;
|
|
177
|
+
let solution = !!record.key || (!!record.solution && record.solution.length > 0);
|
|
178
|
+
let reset = false;
|
|
179
|
+
for (let i = 0; i < record.slots.length; i++) {
|
|
180
|
+
const slot = record.slots[i];
|
|
181
|
+
// Room on the printed page only.
|
|
182
|
+
if (slot.bare)
|
|
183
|
+
continue;
|
|
184
|
+
reset = true;
|
|
185
|
+
const known = slot.kind === 'mark' ? anyMarkRight : (!!slot.correct && slot.correct.length > 0) || (!!slot.accept && slot.accept.length > 0);
|
|
186
|
+
if (!known)
|
|
187
|
+
continue;
|
|
188
|
+
solution = true;
|
|
189
|
+
if (isGraded(slot, record))
|
|
190
|
+
check = true;
|
|
191
|
+
}
|
|
192
|
+
return { check, solution, reset };
|
|
193
|
+
}
|
|
168
194
|
// --- Storage -----------------------------------------------------------------
|
|
169
195
|
function load(key, persist) {
|
|
170
196
|
let text = null;
|
|
@@ -215,28 +241,34 @@ function exerciseRuntime() {
|
|
|
215
241
|
// --- The page ----------------------------------------------------------------
|
|
216
242
|
const CSS = '.pt-ex-ui{pointer-events:auto;box-sizing:border-box;z-index:1;}' +
|
|
217
243
|
'.pt-ex-field{position:absolute;display:block;}' +
|
|
218
|
-
'.pt-ex-input{position:absolute;left:0;top:0;width:100%;height:100%;margin:0;padding:0 .15em;border:0;border-radius:.15em;background:transparent;color:
|
|
219
|
-
|
|
220
|
-
'
|
|
244
|
+
'.pt-ex-input{position:absolute;left:0;top:0;width:100%;height:100%;margin:0;padding:0 .15em;border:0;border-radius:.15em;background:transparent;color:var(--pt-ex-text,#1d1d1f);font:inherit;line-height:1;text-align:center;box-sizing:border-box;}' +
|
|
245
|
+
// Focus rings, ticks and rings are drawn inside the slot: a page that
|
|
246
|
+
// clips at its edge (the Sandbox's HTML tab) cuts nothing.
|
|
247
|
+
'.pt-ex-input:focus{background:rgba(255,255,255,.9);outline:2px solid #1a5fb4;outline-offset:-2px;}' +
|
|
248
|
+
'textarea.pt-ex-input{text-align:start;resize:none;overflow:hidden;background:transparent;padding:0 .2em;color:var(--pt-ex-text,#1d1d1f);}' +
|
|
221
249
|
'textarea.pt-ex-input:focus{background:rgba(255,255,255,.35);}' +
|
|
222
250
|
'.pt-ex-cell{text-transform:uppercase;}' +
|
|
223
251
|
'.pt-ex-toggle{cursor:pointer;pointer-events:auto;}' +
|
|
224
|
-
'.pt-ex-toggle:focus-visible{outline:2px solid #1a5fb4;outline-offset
|
|
225
|
-
'.pt-ex-on.pt-ex-ring,.pt-ex-on.pt-ex-mk-circle{
|
|
226
|
-
'.pt-ex-on.pt-ex-mk-box{
|
|
227
|
-
'.pt-ex-on.pt-ex-mk-underline{
|
|
252
|
+
'.pt-ex-toggle:focus-visible{outline:2px solid #1a5fb4;outline-offset:-2px;}' +
|
|
253
|
+
'.pt-ex-on.pt-ex-ring,.pt-ex-on.pt-ex-mk-circle{box-shadow:inset 0 0 0 .1em var(--pt-ex-ring,currentColor);border-radius:999px;}' +
|
|
254
|
+
'.pt-ex-on.pt-ex-mk-box{box-shadow:inset 0 0 0 .09em var(--pt-ex-ring,currentColor);}' +
|
|
255
|
+
'.pt-ex-on.pt-ex-mk-underline{box-shadow:inset 0 -.12em 0 var(--pt-ex-ring,currentColor);}' +
|
|
228
256
|
'.pt-ex-on.pt-ex-mk-highlight{background:rgba(255,214,0,.45);}' +
|
|
229
257
|
'.pt-ex-on.pt-ex-mk-strike{background:linear-gradient(transparent 46%,currentColor 46%,currentColor 56%,transparent 56%);}' +
|
|
230
258
|
'.pt-ex-on.pt-ex-tick::after{content:"\\2713";position:absolute;left:0;right:0;top:50%;transform:translateY(-55%);text-align:center;line-height:1;font-weight:700;}' +
|
|
231
259
|
'.pt-ex-revealed,.pt-ex-revealed .pt-ex-input{color:var(--pt-ex-answer,#d6246e);}' +
|
|
232
|
-
'.pt-ex-right{outline-color:var(--pt-ex-right,#1e8e3e)!important;}' +
|
|
233
|
-
'.pt-ex-wrong{outline-color:var(--pt-ex-wrong,#c5221f)!important;}' +
|
|
260
|
+
'.pt-ex-right{--pt-ex-ring:var(--pt-ex-right,#1e8e3e);outline-color:var(--pt-ex-right,#1e8e3e)!important;}' +
|
|
261
|
+
'.pt-ex-wrong{--pt-ex-ring:var(--pt-ex-wrong,#c5221f);outline-color:var(--pt-ex-wrong,#c5221f)!important;}' +
|
|
234
262
|
'.pt-ex-field.pt-ex-right .pt-ex-input,textarea.pt-ex-right{box-shadow:inset 0 -.12em 0 var(--pt-ex-right,#1e8e3e);}' +
|
|
235
263
|
'.pt-ex-field.pt-ex-wrong .pt-ex-input,textarea.pt-ex-wrong{box-shadow:inset 0 -.12em 0 var(--pt-ex-wrong,#c5221f);}' +
|
|
236
|
-
|
|
264
|
+
// A right or wrong mark stands on its slot's top corner, over the line
|
|
265
|
+
// gap rather than the next word, slid onto the page at its edge (see
|
|
266
|
+
// `placeMark`).
|
|
267
|
+
'.pt-ex-mark{position:absolute;left:100%;top:0;transform:translate(-55%,-60%);font:700 .62em/1 system-ui,sans-serif;padding:.12em .2em;border-radius:999px;color:#fff;pointer-events:none;z-index:2;white-space:nowrap;}' +
|
|
237
268
|
'.pt-ex-mark.pt-ex-ok{background:var(--pt-ex-right,#1e8e3e);}' +
|
|
238
269
|
'.pt-ex-mark.pt-ex-no{background:var(--pt-ex-wrong,#c5221f);}' +
|
|
239
270
|
'.pt-ex-bar{position:absolute;right:0;top:100%;margin-top:.2em;display:flex;flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:.35em;font:400 1em/1.2 system-ui,sans-serif;color:#1d1d1f;z-index:3;}' +
|
|
271
|
+
'.pt-ex-bar:not(.pt-ex-flow){padding:.15em;border-radius:999px;background:rgba(255,255,255,.85);}' +
|
|
240
272
|
'.pt-ex-bar button{font:inherit;margin:0;padding:.18em .6em;border:1px solid rgba(0,0,0,.35);border-radius:999px;background:#fff;color:#1d1d1f;cursor:pointer;}' +
|
|
241
273
|
'.pt-ex-bar button:hover{background:#f1f1f3;}' +
|
|
242
274
|
'.pt-ex-bar button:focus-visible{outline:2px solid #1a5fb4;outline-offset:2px;}' +
|
|
@@ -248,15 +280,26 @@ function exerciseRuntime() {
|
|
|
248
280
|
'.pt-ex-menu{position:absolute;left:0;top:100%;margin-top:.2em;display:flex;flex-direction:column;min-width:8em;padding:.2em;background:#fff;border:1px solid rgba(0,0,0,.25);border-radius:.3em;box-shadow:0 .2em .6em rgba(0,0,0,.18);font:400 .8em/1.2 system-ui,sans-serif;z-index:5;}' +
|
|
249
281
|
'.pt-ex-menu button{font:inherit;text-align:start;padding:.3em .5em;border:0;background:none;color:#1d1d1f;cursor:pointer;border-radius:.2em;}' +
|
|
250
282
|
'.pt-ex-menu button:hover,.pt-ex-menu button:focus{background:#eef2fb;}' +
|
|
251
|
-
|
|
283
|
+
// A sort exercise: a word sorted fades in the bank and stands in its
|
|
284
|
+
// column; words are dragged from the bank to a column and back.
|
|
285
|
+
'.pt-ex-drag{touch-action:none;user-select:none;-webkit-user-select:none;}' +
|
|
286
|
+
// The word is painted under its slot: a veil fades it.
|
|
287
|
+
'.pt-ex-sorted{background:rgba(255,255,255,.62);border-radius:.25em;}' +
|
|
288
|
+
'.pt-ex-col{position:absolute;left:0;top:0;width:100%;height:100%;box-sizing:border-box;padding:0 .2em;color:var(--pt-ex-text,#1d1d1f);border-radius:.2em;}' +
|
|
289
|
+
'.pt-ex-placed{display:table;cursor:grab;white-space:nowrap;}' +
|
|
290
|
+
'.pt-ex-placed.pt-ex-right{color:var(--pt-ex-right,#1e8e3e);}' +
|
|
291
|
+
'.pt-ex-placed.pt-ex-wrong{color:var(--pt-ex-wrong,#c5221f);}' +
|
|
292
|
+
'.pt-ex-col.pt-ex-revealed,.pt-ex-col.pt-ex-revealed .pt-ex-placed{color:var(--pt-ex-answer,#d6246e);cursor:default;}' +
|
|
293
|
+
'.pt-ex-drop{background:rgba(26,95,180,.08);box-shadow:inset 0 0 0 2px rgba(26,95,180,.55);}' +
|
|
252
294
|
'.pt-ex-svg{position:absolute;left:0;top:0;overflow:visible;}' +
|
|
253
295
|
'.pt-ex-layer{position:absolute;left:0;top:0;transform-origin:0 0;}' +
|
|
254
296
|
// The controls of a mirrored flow (a right-to-left page of the HTML
|
|
255
297
|
// viewer) turn back about their own centres, as its text does (#1244).
|
|
256
298
|
'.pt-flow-mirrored .pt-ex-ui:not(svg){transform:scaleX(-1);}' +
|
|
257
|
-
'.pt-ex-dot{fill:transparent;stroke:transparent;cursor:pointer;pointer-events:all;}' +
|
|
258
|
-
'.pt-ex-dot
|
|
259
|
-
'.pt-ex-dot
|
|
299
|
+
'.pt-ex-dot{fill:transparent;stroke:transparent;cursor:pointer;pointer-events:all;touch-action:none;user-select:none;-webkit-user-select:none;}' +
|
|
300
|
+
'.pt-ex-dot:hover{fill:rgba(26,95,180,.12);}' +
|
|
301
|
+
'.pt-ex-dot.pt-ex-pending{fill:rgba(26,95,180,.2);stroke:#1a5fb4;stroke-width:2.5;}' +
|
|
302
|
+
'.pt-ex-dot:focus{outline:none;stroke:#1a5fb4;stroke-width:2.5;}' +
|
|
260
303
|
'.pt-ex-bar.pt-ex-flow{position:static;justify-content:flex-start;margin-top:.6em;font-size:.8em;}' +
|
|
261
304
|
'.pt-ex-panel.pt-ex-flow{position:static;margin-top:.5em;box-shadow:none;font-size:.9em;}' +
|
|
262
305
|
'.pt-ex-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;}';
|
|
@@ -388,7 +431,7 @@ function exerciseRuntime() {
|
|
|
388
431
|
const persist = opts.persist !== undefined ? opts.persist : data.persist !== false;
|
|
389
432
|
const instant = (opts.check || data.check) === 'instant';
|
|
390
433
|
const words = data.words || {};
|
|
391
|
-
const look = (data.styles && data.styles[rec.style]) || { correct: '#1e8e3e', wrong: '#c5221f', answer: '#d6246e', marker: 'none', trueFalse: words.trueFalse || ['T', 'F'] };
|
|
434
|
+
const look = (data.styles && data.styles[rec.style]) || { correct: '#1e8e3e', wrong: '#c5221f', answer: '#d6246e', ink: 'currentColor', marker: 'none', trueFalse: words.trueFalse || ['T', 'F'] };
|
|
392
435
|
const tf = look.trueFalse;
|
|
393
436
|
const exName = (words.exercise || 'Exercise') + ' ' + rec.number;
|
|
394
437
|
const key = base + ':' + (rec.anchor || '#' + rec.ordinal + '.' + rec.id);
|
|
@@ -455,6 +498,8 @@ function exerciseRuntime() {
|
|
|
455
498
|
return e;
|
|
456
499
|
}
|
|
457
500
|
function colours(e) {
|
|
501
|
+
if (data.text)
|
|
502
|
+
e.style.setProperty('--pt-ex-text', data.text);
|
|
458
503
|
e.style.setProperty('--pt-ex-right', look.correct);
|
|
459
504
|
e.style.setProperty('--pt-ex-wrong', look.wrong);
|
|
460
505
|
e.style.setProperty('--pt-ex-answer', look.answer);
|
|
@@ -493,6 +538,24 @@ function exerciseRuntime() {
|
|
|
493
538
|
const m = el('span', 'pt-ex-mark ' + (ok ? 'pt-ex-ok' : 'pt-ex-no'), holder);
|
|
494
539
|
m.setAttribute('aria-hidden', 'true');
|
|
495
540
|
m.textContent = ok ? '\u2713' : '\u2717';
|
|
541
|
+
placeMark(m, holder);
|
|
542
|
+
}
|
|
543
|
+
/** A mark slid onto the page where its corner place would cross the
|
|
544
|
+
* page's edge (a page that clips, as the Sandbox's does). */
|
|
545
|
+
function placeMark(m, holder) {
|
|
546
|
+
const page = typeof holder.closest === 'function' ? holder.closest('.pt-page') : null;
|
|
547
|
+
if (!page || typeof m.getBoundingClientRect !== 'function')
|
|
548
|
+
return;
|
|
549
|
+
const pr = page.getBoundingClientRect();
|
|
550
|
+
const r = m.getBoundingClientRect();
|
|
551
|
+
if (r.width === 0)
|
|
552
|
+
return;
|
|
553
|
+
// Screen pixels per CSS pixel of the page (a page shown scaled).
|
|
554
|
+
const k = m.offsetWidth > 0 ? r.width / m.offsetWidth : 1;
|
|
555
|
+
const dx = r.right > pr.right - 1 ? pr.right - 1 - r.right : r.left < pr.left + 1 ? pr.left + 1 - r.left : 0;
|
|
556
|
+
const dy = r.top < pr.top + 1 ? pr.top + 1 - r.top : 0;
|
|
557
|
+
if (dx !== 0 || dy !== 0)
|
|
558
|
+
m.style.transform = 'translate(calc(-55% + ' + (dx / k).toFixed(1) + 'px),calc(-60% + ' + (dy / k).toFixed(1) + 'px))';
|
|
496
559
|
}
|
|
497
560
|
const widgets = [];
|
|
498
561
|
let blankN = 0;
|
|
@@ -648,12 +711,136 @@ function exerciseRuntime() {
|
|
|
648
711
|
const v = state.revealed ? solutionValue(w.slot, rec) : state.v[w.slot.key];
|
|
649
712
|
const at = typeof v === 'string' ? categories.indexOf(v) : -1;
|
|
650
713
|
if (at >= 0 && w.word)
|
|
651
|
-
lists[at].push(w
|
|
714
|
+
lists[at].push(w);
|
|
652
715
|
}
|
|
653
716
|
for (let i = 0; i < columnWidgets.length; i++)
|
|
654
|
-
columnWidgets[i].show(
|
|
717
|
+
columnWidgets[i].show(lists[i] || []);
|
|
718
|
+
}
|
|
719
|
+
/** The category of the column under a point of the screen: `undefined`
|
|
720
|
+
* off every column. The words set above a column's area (its heading)
|
|
721
|
+
* count as the column. */
|
|
722
|
+
function columnAt(x, y) {
|
|
723
|
+
for (let i = 0; i < columnWidgets.length; i++) {
|
|
724
|
+
const r = columnWidgets[i].col.getBoundingClientRect();
|
|
725
|
+
const above = Math.min(r.height, body * 3 * (r.height / (columnWidgets[i].col.offsetHeight || r.height || 1)));
|
|
726
|
+
if (x >= r.left && x <= r.right && y >= r.top - above && y <= r.bottom)
|
|
727
|
+
return i;
|
|
728
|
+
}
|
|
729
|
+
return undefined;
|
|
730
|
+
}
|
|
731
|
+
/** Dragging a word of a sort exercise (from the bank or from a column)
|
|
732
|
+
* onto a column sorts it there; a word dragged out of its column goes
|
|
733
|
+
* back to the bank. A press with no drag is left to the click. Returns
|
|
734
|
+
* whether the click that follows a drag is to be ignored. */
|
|
735
|
+
function dragWord(t, slot, word, fromColumn) {
|
|
736
|
+
let start = null;
|
|
737
|
+
let ghost = null;
|
|
738
|
+
let over = -1;
|
|
739
|
+
let dragged = false;
|
|
740
|
+
// Where the ghost's left and top of 0 stand on the screen, and screen
|
|
741
|
+
// pixels per CSS pixel: the document may be zoomed.
|
|
742
|
+
let origin = [0, 0];
|
|
743
|
+
let scale = 1;
|
|
744
|
+
addClass(t, 'pt-ex-drag');
|
|
745
|
+
const highlight = (i) => {
|
|
746
|
+
if (i === over)
|
|
747
|
+
return;
|
|
748
|
+
if (over >= 0 && columnWidgets[over])
|
|
749
|
+
columnWidgets[over].col.classList.remove('pt-ex-drop');
|
|
750
|
+
over = i;
|
|
751
|
+
if (over >= 0 && columnWidgets[over])
|
|
752
|
+
columnWidgets[over].col.classList.add('pt-ex-drop');
|
|
753
|
+
};
|
|
754
|
+
const end = () => {
|
|
755
|
+
start = null;
|
|
756
|
+
if (ghost && ghost.parentNode)
|
|
757
|
+
ghost.parentNode.removeChild(ghost);
|
|
758
|
+
ghost = null;
|
|
759
|
+
highlight(-1);
|
|
760
|
+
};
|
|
761
|
+
const down = (e) => {
|
|
762
|
+
if (state.revealed || e.button > 0)
|
|
763
|
+
return;
|
|
764
|
+
start = [e.clientX, e.clientY];
|
|
765
|
+
dragged = false;
|
|
766
|
+
// The pointer's moves come here wherever it goes.
|
|
767
|
+
if (typeof t.setPointerCapture === 'function') {
|
|
768
|
+
try {
|
|
769
|
+
t.setPointerCapture(e.pointerId);
|
|
770
|
+
}
|
|
771
|
+
catch { /* a pointer gone */ }
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
const move = (e) => {
|
|
775
|
+
if (!start)
|
|
776
|
+
return;
|
|
777
|
+
if (!ghost) {
|
|
778
|
+
if (Math.abs(e.clientX - start[0]) + Math.abs(e.clientY - start[1]) < 6)
|
|
779
|
+
return;
|
|
780
|
+
// The word follows the pointer, set at the size the page shows it
|
|
781
|
+
// (the page may be scaled, and the ghost lives outside it).
|
|
782
|
+
const r = t.getBoundingClientRect();
|
|
783
|
+
ghost = doc.createElement('span');
|
|
784
|
+
ghost.textContent = word;
|
|
785
|
+
ghost.setAttribute('aria-hidden', 'true');
|
|
786
|
+
const st = ghost.style;
|
|
787
|
+
st.position = 'fixed';
|
|
788
|
+
st.zIndex = '2147483647';
|
|
789
|
+
st.pointerEvents = 'none';
|
|
790
|
+
st.transform = 'translate(-50%,-50%)';
|
|
791
|
+
st.padding = '.1em .4em';
|
|
792
|
+
st.borderRadius = '.3em';
|
|
793
|
+
st.border = '1px solid rgba(0,0,0,.35)';
|
|
794
|
+
st.background = '#fff';
|
|
795
|
+
st.color = data.text || '#1d1d1f';
|
|
796
|
+
st.boxShadow = '0 .2em .6em rgba(0,0,0,.25)';
|
|
797
|
+
st.whiteSpace = 'nowrap';
|
|
798
|
+
st.fontFamily = textFont || 'system-ui,sans-serif';
|
|
799
|
+
st.left = '0';
|
|
800
|
+
st.top = '0';
|
|
801
|
+
(doc.body || doc.documentElement).appendChild(ghost);
|
|
802
|
+
const g0 = ghost.getBoundingClientRect();
|
|
803
|
+
st.left = '100px';
|
|
804
|
+
const g1 = ghost.getBoundingClientRect();
|
|
805
|
+
scale = (g1.left - g0.left) / 100 || 1;
|
|
806
|
+
origin = [g0.left + g0.width / 2, g0.top + g0.height / 2];
|
|
807
|
+
st.fontSize = px(Math.max(12, r.height * (fromColumn ? 0.75 : 0.62)) / scale);
|
|
808
|
+
dragged = true;
|
|
809
|
+
}
|
|
810
|
+
e.preventDefault();
|
|
811
|
+
ghost.style.left = px((e.clientX - origin[0]) / scale);
|
|
812
|
+
ghost.style.top = px((e.clientY - origin[1]) / scale);
|
|
813
|
+
const at = columnAt(e.clientX, e.clientY);
|
|
814
|
+
highlight(at === undefined ? -1 : at);
|
|
815
|
+
};
|
|
816
|
+
const up = (e) => {
|
|
817
|
+
if (!start)
|
|
818
|
+
return;
|
|
819
|
+
const was = !!ghost;
|
|
820
|
+
const at = was ? columnAt(e.clientX, e.clientY) : undefined;
|
|
821
|
+
end();
|
|
822
|
+
if (!was)
|
|
823
|
+
return;
|
|
824
|
+
if (at !== undefined)
|
|
825
|
+
changed(slot, categories[at]);
|
|
826
|
+
else if (fromColumn)
|
|
827
|
+
changed(slot, null);
|
|
828
|
+
};
|
|
829
|
+
t.addEventListener('pointerdown', down);
|
|
830
|
+
t.addEventListener('pointermove', move);
|
|
831
|
+
t.addEventListener('pointerup', up);
|
|
832
|
+
t.addEventListener('pointercancel', end);
|
|
833
|
+
cleanups.push(() => {
|
|
834
|
+
end();
|
|
835
|
+
t.removeEventListener('pointerdown', down);
|
|
836
|
+
t.removeEventListener('pointermove', move);
|
|
837
|
+
t.removeEventListener('pointerup', up);
|
|
838
|
+
t.removeEventListener('pointercancel', end);
|
|
839
|
+
});
|
|
840
|
+
return () => { const d = dragged; dragged = false; return d; };
|
|
655
841
|
}
|
|
656
|
-
/** A word of a sort exercise's bank: a
|
|
842
|
+
/** A word of a sort exercise's bank: dragged to a column, or a chooser
|
|
843
|
+
* of the categories. */
|
|
657
844
|
function chooser(slot, t) {
|
|
658
845
|
const word = chipText(t);
|
|
659
846
|
setAttr(t, 'role', 'button');
|
|
@@ -663,8 +850,7 @@ function exerciseRuntime() {
|
|
|
663
850
|
addClass(t, 'pt-ex-toggle');
|
|
664
851
|
addClass(t, 'pt-ex-ui');
|
|
665
852
|
colours(t);
|
|
666
|
-
const
|
|
667
|
-
tag.hidden = true;
|
|
853
|
+
const wasDrag = dragWord(t, slot, word, false);
|
|
668
854
|
let menu = null;
|
|
669
855
|
// A click anywhere else closes the menu.
|
|
670
856
|
const outside = (e) => { if (menu && !t.contains(e.target))
|
|
@@ -712,33 +898,78 @@ function exerciseRuntime() {
|
|
|
712
898
|
open();
|
|
713
899
|
}
|
|
714
900
|
};
|
|
715
|
-
const onClick = (e) => {
|
|
716
|
-
|
|
901
|
+
const onClick = (e) => {
|
|
902
|
+
if (wasDrag())
|
|
903
|
+
return;
|
|
904
|
+
if (!menu || !menu.contains(e.target))
|
|
905
|
+
open();
|
|
906
|
+
};
|
|
717
907
|
t.addEventListener('click', onClick);
|
|
718
908
|
t.addEventListener('keydown', onKey);
|
|
719
909
|
cleanups.push(() => { close(); t.removeEventListener('click', onClick); t.removeEventListener('keydown', onKey); });
|
|
720
|
-
|
|
910
|
+
const w = {
|
|
721
911
|
slot,
|
|
722
912
|
word,
|
|
913
|
+
ok: null,
|
|
723
914
|
paint(v) {
|
|
724
915
|
const at = typeof v === 'string' ? categories.indexOf(v) : -1;
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
t.
|
|
916
|
+
// Sorted: the word stands in its column, and fades in the bank.
|
|
917
|
+
t.classList.toggle('pt-ex-sorted', at >= 0 && columnWidgets.length > 0);
|
|
918
|
+
t.setAttribute('aria-label', (word || lineText(t)) + (at >= 0 ? ', ' + categories[at] : ''));
|
|
728
919
|
refreshColumns();
|
|
729
920
|
},
|
|
730
|
-
lock(on) { t.classList.toggle('pt-ex-revealed', on);
|
|
731
|
-
mark(ok) { badge(t, ok); },
|
|
732
|
-
}
|
|
921
|
+
lock(on) { t.classList.toggle('pt-ex-revealed', on); },
|
|
922
|
+
mark(ok) { w.ok = ok; badge(t, ok); refreshColumns(); },
|
|
923
|
+
};
|
|
924
|
+
widgets.push(w);
|
|
925
|
+
}
|
|
926
|
+
/** A sort exercise's column over its writing area: the words sorted in
|
|
927
|
+
* it, one a line, each dragged to another column or back to the bank
|
|
928
|
+
* (or sent back with a click). */
|
|
929
|
+
function sortColumn(slot, box) {
|
|
930
|
+
const name = categories[columnWidgets.length];
|
|
931
|
+
const col = own(el('div', 'pt-ex-col pt-ex-ui', box));
|
|
932
|
+
col.setAttribute('role', 'list');
|
|
933
|
+
col.setAttribute('aria-label', (name !== undefined ? name + ', ' : '') + exName);
|
|
934
|
+
const line = (slot.linePt || 0) * ppt || body * 1.35;
|
|
935
|
+
col.style.fontSize = px(body);
|
|
936
|
+
col.style.lineHeight = px(line);
|
|
937
|
+
col.style.paddingTop = px(Math.max(0, line * 0.12));
|
|
938
|
+
if (textFont)
|
|
939
|
+
col.style.fontFamily = textFont;
|
|
940
|
+
colours(col);
|
|
941
|
+
const w = {
|
|
942
|
+
slot,
|
|
943
|
+
col,
|
|
944
|
+
show(list) {
|
|
945
|
+
while (col.firstChild)
|
|
946
|
+
col.removeChild(col.firstChild);
|
|
947
|
+
for (let i = 0; i < list.length; i++) {
|
|
948
|
+
const src = list[i];
|
|
949
|
+
const p = el('span', 'pt-ex-placed', col);
|
|
950
|
+
p.setAttribute('role', 'listitem');
|
|
951
|
+
p.textContent = src.word;
|
|
952
|
+
if (state.revealed)
|
|
953
|
+
continue;
|
|
954
|
+
if (src.ok !== null && src.ok !== undefined)
|
|
955
|
+
p.classList.add(src.ok ? 'pt-ex-right' : 'pt-ex-wrong');
|
|
956
|
+
const wasDrag = dragWord(p, src.slot, src.word, true);
|
|
957
|
+
p.addEventListener('click', () => { if (!wasDrag())
|
|
958
|
+
changed(src.slot, null); });
|
|
959
|
+
}
|
|
960
|
+
},
|
|
961
|
+
paint() { },
|
|
962
|
+
lock(on) { col.classList.toggle('pt-ex-revealed', on); },
|
|
963
|
+
mark() { },
|
|
964
|
+
};
|
|
965
|
+
columnWidgets.push(w);
|
|
966
|
+
widgets.push(w);
|
|
733
967
|
}
|
|
734
968
|
/** A text area on a writing area: an open answer, a sentence put in
|
|
735
|
-
* order
|
|
969
|
+
* order. */
|
|
736
970
|
function area(slot, box) {
|
|
737
|
-
const sortColumn = rec.kind === 'sort';
|
|
738
971
|
const ta = own(el('textarea', 'pt-ex-input pt-ex-ui', box));
|
|
739
|
-
|
|
740
|
-
const column = sortColumn ? categories[columnWidgets.length] : undefined;
|
|
741
|
-
ta.setAttribute('aria-label', column !== undefined ? column + ', ' + exName : (words.blank || 'blank') + ' ' + (++blankN) + ', ' + exName);
|
|
972
|
+
ta.setAttribute('aria-label', (words.blank || 'blank') + ' ' + (++blankN) + ', ' + exName);
|
|
742
973
|
ta.setAttribute('spellcheck', 'false');
|
|
743
974
|
const line = (slot.linePt || 0) * ppt || body * 1.35;
|
|
744
975
|
ta.style.fontSize = px(body);
|
|
@@ -747,20 +978,11 @@ function exerciseRuntime() {
|
|
|
747
978
|
if (textFont)
|
|
748
979
|
ta.style.fontFamily = textFont;
|
|
749
980
|
colours(ta);
|
|
750
|
-
|
|
751
|
-
ta.readOnly = true;
|
|
752
|
-
ta.setAttribute('tabindex', '-1');
|
|
753
|
-
ta.setAttribute('aria-live', 'polite');
|
|
754
|
-
}
|
|
755
|
-
else
|
|
756
|
-
ta.addEventListener('input', () => changed(slot, ta.value));
|
|
981
|
+
ta.addEventListener('input', () => changed(slot, ta.value));
|
|
757
982
|
const w = {
|
|
758
983
|
slot,
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
ta.value = typeof v === 'string' ? v : ''; },
|
|
762
|
-
lock(on) { if (!sortColumn)
|
|
763
|
-
ta.readOnly = on; ta.classList.toggle('pt-ex-revealed', on); },
|
|
984
|
+
paint(v) { ta.value = typeof v === 'string' ? v : ''; },
|
|
985
|
+
lock(on) { ta.readOnly = on; ta.classList.toggle('pt-ex-revealed', on); },
|
|
764
986
|
mark(ok) {
|
|
765
987
|
ta.classList.remove('pt-ex-right', 'pt-ex-wrong');
|
|
766
988
|
const old = box.querySelector(':scope > .pt-ex-mark');
|
|
@@ -774,10 +996,9 @@ function exerciseRuntime() {
|
|
|
774
996
|
m.textContent = ok ? '\u2713' : '\u2717';
|
|
775
997
|
m.style.fontSize = px(body * 0.62);
|
|
776
998
|
colours(m);
|
|
999
|
+
placeMark(m, box);
|
|
777
1000
|
},
|
|
778
1001
|
};
|
|
779
|
-
if (sortColumn)
|
|
780
|
-
columnWidgets.push(w);
|
|
781
1002
|
widgets.push(w);
|
|
782
1003
|
}
|
|
783
1004
|
/** One-character inputs over the cells of a crossword or an operation. */
|
|
@@ -828,13 +1049,18 @@ function exerciseRuntime() {
|
|
|
828
1049
|
});
|
|
829
1050
|
}
|
|
830
1051
|
/** The dots of a match: a left dot then a right one (or the other way
|
|
831
|
-
* round) join them with a line
|
|
1052
|
+
* round) join them with a line, clicked one after the other or dragged
|
|
1053
|
+
* from one to the other. */
|
|
832
1054
|
function matchDots(box, grid, slots) {
|
|
833
1055
|
const w = parseFloat(box.style.width) || 0;
|
|
834
1056
|
const h = parseFloat(box.style.height) || 0;
|
|
835
1057
|
const s = own(svg('svg', { class: 'pt-ex-svg pt-ex-ui', width: w, height: h, viewBox: '0 0 ' + w + ' ' + h }, box));
|
|
836
1058
|
const lines = svg('g', {}, s);
|
|
837
1059
|
const marks = svg('g', {}, s);
|
|
1060
|
+
const ink = look.ink || 'currentColor';
|
|
1061
|
+
const stroke = Math.max(2.5, body * 0.13);
|
|
1062
|
+
// The line being drawn: from the dot picked to the pointer.
|
|
1063
|
+
const rubber = svg('line', { stroke: ink, 'stroke-width': stroke, 'stroke-linecap': 'round', 'stroke-dasharray': stroke * 1.5 + ' ' + stroke * 1.5, 'pointer-events': 'none', visibility: 'hidden' }, s);
|
|
838
1064
|
// Mirrored on a right-to-left page: the left items stand on the right.
|
|
839
1065
|
const lx = grid.rtl ? w - grid.dotsPt[0] * ppt : grid.dotsPt[0] * ppt;
|
|
840
1066
|
const rx = grid.rtl ? w - grid.dotsPt[1] * ppt : grid.dotsPt[1] * ppt;
|
|
@@ -849,7 +1075,50 @@ function exerciseRuntime() {
|
|
|
849
1075
|
leftDots[i].classList.toggle('pt-ex-pending', i === pendingLeft);
|
|
850
1076
|
for (let i = 0; i < rightDots.length; i++)
|
|
851
1077
|
rightDots[i].classList.toggle('pt-ex-pending', i === pendingRight);
|
|
1078
|
+
if (pendingLeft < 0 && pendingRight < 0)
|
|
1079
|
+
rubber.setAttribute('visibility', 'hidden');
|
|
1080
|
+
};
|
|
1081
|
+
/** A point of the screen in the drawing's own units. */
|
|
1082
|
+
const local = (x, y) => {
|
|
1083
|
+
const m = typeof s.getScreenCTM === 'function' ? s.getScreenCTM() : null;
|
|
1084
|
+
if (!m)
|
|
1085
|
+
return [x, y];
|
|
1086
|
+
const p = new DOMPoint(x, y).matrixTransform(m.inverse());
|
|
1087
|
+
return [p.x, p.y];
|
|
1088
|
+
};
|
|
1089
|
+
const follow = (x, y) => {
|
|
1090
|
+
const from = pendingLeft >= 0 ? [lx, rows[pendingLeft]] : pendingRight >= 0 ? [rx, rows[pendingRight]] : null;
|
|
1091
|
+
if (!from)
|
|
1092
|
+
return;
|
|
1093
|
+
const to = local(x, y);
|
|
1094
|
+
rubber.setAttribute('x1', String(from[0]));
|
|
1095
|
+
rubber.setAttribute('y1', String(from[1]));
|
|
1096
|
+
rubber.setAttribute('x2', String(to[0]));
|
|
1097
|
+
rubber.setAttribute('y2', String(to[1]));
|
|
1098
|
+
rubber.setAttribute('visibility', 'visible');
|
|
852
1099
|
};
|
|
1100
|
+
/** The dot of a side nearest a screen point, if the point is on it. */
|
|
1101
|
+
const dotAt = (side, x, y) => {
|
|
1102
|
+
const p = local(x, y);
|
|
1103
|
+
const dx = side === 0 ? lx : rx;
|
|
1104
|
+
let best = -1;
|
|
1105
|
+
let bestD = radius * 1.8;
|
|
1106
|
+
const n = side === 0 ? leftDots.length : rightDots.length;
|
|
1107
|
+
for (let i = 0; i < n; i++) {
|
|
1108
|
+
const d = Math.hypot(p[0] - dx, p[1] - rows[i]);
|
|
1109
|
+
if (d < bestD) {
|
|
1110
|
+
best = i;
|
|
1111
|
+
bestD = d;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
return best;
|
|
1115
|
+
};
|
|
1116
|
+
const onMove = (e) => { if (!state.revealed)
|
|
1117
|
+
follow(e.clientX, e.clientY); };
|
|
1118
|
+
s.addEventListener('pointermove', onMove);
|
|
1119
|
+
let dragFrom = null;
|
|
1120
|
+
let dragging = false;
|
|
1121
|
+
let skipClick = false;
|
|
853
1122
|
const join = (li, ri) => {
|
|
854
1123
|
pendingLeft = -1;
|
|
855
1124
|
pendingRight = -1;
|
|
@@ -875,6 +1144,7 @@ function exerciseRuntime() {
|
|
|
875
1144
|
join(i, pendingRight);
|
|
876
1145
|
else {
|
|
877
1146
|
pendingLeft = pendingLeft === i ? -1 : i;
|
|
1147
|
+
pendingRight = -1;
|
|
878
1148
|
refresh();
|
|
879
1149
|
}
|
|
880
1150
|
}
|
|
@@ -882,10 +1152,61 @@ function exerciseRuntime() {
|
|
|
882
1152
|
join(pendingLeft, i);
|
|
883
1153
|
else {
|
|
884
1154
|
pendingRight = pendingRight === i ? -1 : i;
|
|
1155
|
+
pendingLeft = -1;
|
|
1156
|
+
refresh();
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
// A drag from a dot to one of the other side joins them; a press
|
|
1160
|
+
// with no drag is a click.
|
|
1161
|
+
dot.addEventListener('pointerdown', (e) => {
|
|
1162
|
+
if (state.revealed || e.button > 0)
|
|
1163
|
+
return;
|
|
1164
|
+
dragFrom = [e.clientX, e.clientY];
|
|
1165
|
+
dragging = false;
|
|
1166
|
+
if (typeof dot.setPointerCapture === 'function') {
|
|
1167
|
+
try {
|
|
1168
|
+
dot.setPointerCapture(e.pointerId);
|
|
1169
|
+
}
|
|
1170
|
+
catch { /* a pointer gone */ }
|
|
1171
|
+
}
|
|
1172
|
+
});
|
|
1173
|
+
dot.addEventListener('pointermove', (e) => {
|
|
1174
|
+
if (!dragFrom)
|
|
1175
|
+
return;
|
|
1176
|
+
if (!dragging) {
|
|
1177
|
+
if (Math.abs(e.clientX - dragFrom[0]) + Math.abs(e.clientY - dragFrom[1]) < 6)
|
|
1178
|
+
return;
|
|
1179
|
+
dragging = true;
|
|
1180
|
+
pendingLeft = side === 0 ? i : -1;
|
|
1181
|
+
pendingRight = side === 0 ? -1 : i;
|
|
1182
|
+
refresh();
|
|
1183
|
+
}
|
|
1184
|
+
e.preventDefault();
|
|
1185
|
+
follow(e.clientX, e.clientY);
|
|
1186
|
+
});
|
|
1187
|
+
const drop = (e) => {
|
|
1188
|
+
if (!dragFrom)
|
|
1189
|
+
return;
|
|
1190
|
+
dragFrom = null;
|
|
1191
|
+
if (!dragging)
|
|
1192
|
+
return;
|
|
1193
|
+
dragging = false;
|
|
1194
|
+
skipClick = true;
|
|
1195
|
+
const other = e.type === 'pointerup' ? dotAt(1 - side, e.clientX, e.clientY) : -1;
|
|
1196
|
+
if (other >= 0)
|
|
1197
|
+
join(side === 0 ? i : other, side === 0 ? other : i);
|
|
1198
|
+
else {
|
|
1199
|
+
pendingLeft = -1;
|
|
1200
|
+
pendingRight = -1;
|
|
885
1201
|
refresh();
|
|
886
1202
|
}
|
|
887
1203
|
};
|
|
888
|
-
dot.addEventListener('
|
|
1204
|
+
dot.addEventListener('pointerup', drop);
|
|
1205
|
+
dot.addEventListener('pointercancel', drop);
|
|
1206
|
+
dot.addEventListener('click', () => { if (skipClick) {
|
|
1207
|
+
skipClick = false;
|
|
1208
|
+
return;
|
|
1209
|
+
} pick(); });
|
|
889
1210
|
dot.addEventListener('keydown', (e) => { if (e.key === ' ' || e.key === 'Enter') {
|
|
890
1211
|
e.preventDefault();
|
|
891
1212
|
pick();
|
|
@@ -905,7 +1226,7 @@ function exerciseRuntime() {
|
|
|
905
1226
|
line = null;
|
|
906
1227
|
}
|
|
907
1228
|
if (typeof v === 'number' && rows[v - 1] !== undefined) {
|
|
908
|
-
line = svg('line', { x1: lx, y1: rows[li], x2: rx, y2: rows[v - 1], stroke:
|
|
1229
|
+
line = svg('line', { x1: lx, y1: rows[li], x2: rx, y2: rows[v - 1], stroke: ink, 'stroke-width': stroke, 'stroke-linecap': 'round' }, lines);
|
|
909
1230
|
if (state.revealed)
|
|
910
1231
|
line.setAttribute('stroke', look.answer);
|
|
911
1232
|
}
|
|
@@ -917,7 +1238,7 @@ function exerciseRuntime() {
|
|
|
917
1238
|
mark = null;
|
|
918
1239
|
}
|
|
919
1240
|
if (line)
|
|
920
|
-
line.setAttribute('stroke', ok === null ? (state.revealed ? look.answer :
|
|
1241
|
+
line.setAttribute('stroke', ok === null ? (state.revealed ? look.answer : ink) : ok ? look.correct : look.wrong);
|
|
921
1242
|
if (ok === null)
|
|
922
1243
|
return;
|
|
923
1244
|
mark = svg('text', { x: lx + radius * (grid.rtl ? -2.2 : 2.2), y: rows[li] - body * 0.25, 'font-size': body * 0.8, 'font-weight': 700, fill: ok ? look.correct : look.wrong, 'text-anchor': 'middle', 'aria-hidden': 'true' }, marks);
|
|
@@ -1077,7 +1398,9 @@ function exerciseRuntime() {
|
|
|
1077
1398
|
}
|
|
1078
1399
|
else if (slot.kind === 'text') {
|
|
1079
1400
|
const box = slot.box !== undefined ? boxEls[String(slot.box)] : null;
|
|
1080
|
-
if (box)
|
|
1401
|
+
if (box && rec.kind === 'sort')
|
|
1402
|
+
sortColumn(slot, box);
|
|
1403
|
+
else if (box && !slot.bare)
|
|
1081
1404
|
area(slot, box);
|
|
1082
1405
|
}
|
|
1083
1406
|
else if (slot.kind === 'cell') {
|
|
@@ -1101,8 +1424,15 @@ function exerciseRuntime() {
|
|
|
1101
1424
|
if (g.kind === 'wordsearch' && wordSlots.length > 0)
|
|
1102
1425
|
wordSearch(box, g, wordSlots);
|
|
1103
1426
|
}
|
|
1427
|
+
// An exercise with nothing to check, show or answer (a talk with a
|
|
1428
|
+
// classmate) has no bar.
|
|
1429
|
+
const actions = actionsOf(rec);
|
|
1430
|
+
const checkable = actions.check;
|
|
1431
|
+
const solvable = actions.solution;
|
|
1432
|
+
const resettable = actions.reset && widgets.length > 0;
|
|
1433
|
+
const hasHints = !!rec.hints && rec.hints.length > 0;
|
|
1104
1434
|
// The bar: on the last part of the exercise's outer box.
|
|
1105
|
-
const outer = outers.length > 0 ? outers[outers.length - 1] : null;
|
|
1435
|
+
const outer = outers.length > 0 && (resettable || solvable || hasHints) ? outers[outers.length - 1] : null;
|
|
1106
1436
|
let scoreEl = null;
|
|
1107
1437
|
let panel = null;
|
|
1108
1438
|
let solutionBtn = null;
|
|
@@ -1124,18 +1454,47 @@ function exerciseRuntime() {
|
|
|
1124
1454
|
scoreEl = el('span', 'pt-ex-score', bar);
|
|
1125
1455
|
scoreEl.setAttribute('aria-live', 'polite');
|
|
1126
1456
|
scoreEl.setAttribute('role', 'status');
|
|
1127
|
-
if (
|
|
1457
|
+
if (hasHints)
|
|
1128
1458
|
hintBtn = button(words.hint || 'Hint', () => showPanel(panel && panel.getAttribute('data-show') === 'hint' ? null : 'hint'));
|
|
1129
|
-
if (!instant)
|
|
1459
|
+
if (!instant && checkable)
|
|
1130
1460
|
button(words.check || 'Check', () => { state.checked = true; persistNow(); feedback(); });
|
|
1131
|
-
|
|
1132
|
-
|
|
1461
|
+
if (solvable)
|
|
1462
|
+
solutionBtn = button(words.showSolution || 'Show solution', () => reveal(!state.revealed));
|
|
1463
|
+
if (resettable)
|
|
1464
|
+
button(words.reset || 'Try again', reset);
|
|
1133
1465
|
panel = el('div', 'pt-ex-panel pt-ex-ui' + (flow ? ' pt-ex-flow' : ''), outer);
|
|
1134
1466
|
own(panel);
|
|
1135
1467
|
panel.hidden = true;
|
|
1136
1468
|
if (!flow)
|
|
1137
1469
|
panel.style.fontSize = px(Math.max(body * 0.72, 10));
|
|
1138
1470
|
colours(panel);
|
|
1471
|
+
if (!flow) {
|
|
1472
|
+
liftOntoPage(bar);
|
|
1473
|
+
const view = doc.defaultView;
|
|
1474
|
+
// Again once the fonts have set the bar's size.
|
|
1475
|
+
if (view && view.requestAnimationFrame)
|
|
1476
|
+
view.requestAnimationFrame(() => liftOntoPage(bar));
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
/** Lifts a control laid under the exercise back onto its page where it
|
|
1480
|
+
* would cross the page's foot (a page that clips, as the Sandbox's
|
|
1481
|
+
* short screen pages do). */
|
|
1482
|
+
function liftOntoPage(e) {
|
|
1483
|
+
e.style.transform = '';
|
|
1484
|
+
const page = typeof e.closest === 'function' ? e.closest('.pt-page') : null;
|
|
1485
|
+
if (!page || typeof e.getBoundingClientRect !== 'function')
|
|
1486
|
+
return;
|
|
1487
|
+
const pr = page.getBoundingClientRect();
|
|
1488
|
+
const r = e.getBoundingClientRect();
|
|
1489
|
+
if (r.height === 0)
|
|
1490
|
+
return;
|
|
1491
|
+
const k = e.offsetHeight > 0 ? r.height / e.offsetHeight : 1;
|
|
1492
|
+
let up = r.bottom - (pr.bottom - 2);
|
|
1493
|
+
// Never above the page's head.
|
|
1494
|
+
if (up > r.top - pr.top)
|
|
1495
|
+
up = r.top - pr.top;
|
|
1496
|
+
if (up > 0)
|
|
1497
|
+
e.style.transform = 'translateY(' + (-up / k).toFixed(1) + 'px)';
|
|
1139
1498
|
}
|
|
1140
1499
|
void hintBtn;
|
|
1141
1500
|
function showPanel(what) {
|
|
@@ -1157,6 +1516,11 @@ function exerciseRuntime() {
|
|
|
1157
1516
|
for (let i = 0; i < sol.length; i++)
|
|
1158
1517
|
el('p', '', panel).textContent = sol[i];
|
|
1159
1518
|
}
|
|
1519
|
+
// A solution shown in its slots alone needs no panel.
|
|
1520
|
+
if (!panel.firstChild)
|
|
1521
|
+
panel.hidden = true;
|
|
1522
|
+
if (!panel.hidden && !flow)
|
|
1523
|
+
liftOntoPage(panel);
|
|
1160
1524
|
}
|
|
1161
1525
|
function valueOf(w) {
|
|
1162
1526
|
if (state.revealed)
|
|
@@ -1182,6 +1546,13 @@ function exerciseRuntime() {
|
|
|
1182
1546
|
const s = rec.slots[i];
|
|
1183
1547
|
if (!isGraded(s, rec))
|
|
1184
1548
|
continue;
|
|
1549
|
+
// A word of a mark exercise not to be marked counts only when it is
|
|
1550
|
+
// marked: a wrong answer that keeps the score short of the total.
|
|
1551
|
+
if (s.kind === 'mark' && (s.correct || []).indexOf(1) < 0) {
|
|
1552
|
+
if (verdict(s) === false)
|
|
1553
|
+
total++;
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
1185
1556
|
total++;
|
|
1186
1557
|
if (verdict(s) === true)
|
|
1187
1558
|
right++;
|
|
@@ -1273,7 +1644,7 @@ function exerciseRuntime() {
|
|
|
1273
1644
|
solutionBtn.setAttribute('aria-expanded', 'false');
|
|
1274
1645
|
return { record: rec, state, reveal, reset, feedback };
|
|
1275
1646
|
}
|
|
1276
|
-
return { normalizeAnswer, isGraded, checkSlot, solutionValue, attach };
|
|
1647
|
+
return { normalizeAnswer, isGraded, checkSlot, solutionValue, actionsOf, attach };
|
|
1277
1648
|
}
|
|
1278
1649
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
1279
1650
|
const runtime = exerciseRuntime();
|
|
@@ -1285,6 +1656,8 @@ export const isGradedSlot = runtime.isGraded;
|
|
|
1285
1656
|
* is no answer or not graded (`null`). `trueFalse` are the style's true
|
|
1286
1657
|
* and false labels, for a typed true/false. */
|
|
1287
1658
|
export const checkExerciseSlot = runtime.checkSlot;
|
|
1659
|
+
/** The buttons an exercise's bar offers: Check, Show solution, Try again. */
|
|
1660
|
+
export const exerciseActions = runtime.actionsOf;
|
|
1288
1661
|
/** What a slot shows when the solution is shown. */
|
|
1289
1662
|
export const exerciseSolutionValue = runtime.solutionValue;
|
|
1290
1663
|
/** Makes the exercises of the HTML output under `root` answerable (see the
|