postext 1.59.2 → 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__/exercises/followups.test.d.ts +2 -0
- package/dist/__tests__/exercises/followups.test.d.ts.map +1 -0
- package/dist/__tests__/exercises/followups.test.js +235 -0
- package/dist/__tests__/exercises/followups.test.js.map +1 -0
- 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/__tests__/pipeline/spanHeadingMidPage.test.js +76 -0
- package/dist/__tests__/pipeline/spanHeadingMidPage.test.js.map +1 -1
- package/dist/canvas-backend/annotations.d.ts +4 -1
- package/dist/canvas-backend/annotations.d.ts.map +1 -1
- package/dist/canvas-backend/annotations.js +5 -0
- package/dist/canvas-backend/annotations.js.map +1 -1
- package/dist/canvas-backend/chip.d.ts.map +1 -1
- package/dist/canvas-backend/chip.js +4 -0
- package/dist/canvas-backend/chip.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/exercises.d.ts.map +1 -1
- package/dist/defaults/exercises.js +15 -9
- package/dist/defaults/exercises.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/build.d.ts.map +1 -1
- package/dist/exercises/build.js +1 -0
- package/dist/exercises/build.js.map +1 -1
- package/dist/exercises/configWarnings.d.ts.map +1 -1
- package/dist/exercises/configWarnings.js +6 -2
- package/dist/exercises/configWarnings.js.map +1 -1
- package/dist/exercises/designs.d.ts +5 -0
- package/dist/exercises/designs.d.ts.map +1 -1
- package/dist/exercises/designs.js +45 -16
- package/dist/exercises/designs.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 +6 -0
- package/dist/exercises/expand.d.ts.map +1 -1
- package/dist/exercises/expand.js +108 -20
- package/dist/exercises/expand.js.map +1 -1
- package/dist/exercises/grids.d.ts.map +1 -1
- package/dist/exercises/grids.js +76 -16
- package/dist/exercises/grids.js.map +1 -1
- package/dist/exercises/model.d.ts +4 -1
- package/dist/exercises/model.d.ts.map +1 -1
- package/dist/exercises/model.js +56 -15
- package/dist/exercises/model.js.map +1 -1
- package/dist/exercises/record.d.ts +9 -1
- 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 +485 -69
- 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 +27 -16
- package/dist/html-backend.js.map +1 -1
- package/dist/htmlAnnotations.d.ts +5 -1
- package/dist/htmlAnnotations.d.ts.map +1 -1
- package/dist/htmlAnnotations.js +6 -0
- package/dist/htmlAnnotations.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/measure/cache.js +3 -3
- package/dist/measure/cache.js.map +1 -1
- package/dist/measure/rich.d.ts.map +1 -1
- package/dist/measure/rich.js +72 -4
- package/dist/measure/rich.js.map +1 -1
- package/dist/parse/types.d.ts +3 -0
- package/dist/parse/types.d.ts.map +1 -1
- package/dist/pipeline/build.d.ts.map +1 -1
- package/dist/pipeline/build.js +38 -7
- package/dist/pipeline/build.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 +61 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vdt.d.ts +5 -0
- package/dist/vdt.d.ts.map +1 -1
- package/dist/vdt.js.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,11 +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
|
-
'.pt-ex-
|
|
254
|
-
|
|
255
|
-
|
|
295
|
+
'.pt-ex-layer{position:absolute;left:0;top:0;transform-origin:0 0;}' +
|
|
296
|
+
// The controls of a mirrored flow (a right-to-left page of the HTML
|
|
297
|
+
// viewer) turn back about their own centres, as its text does (#1244).
|
|
298
|
+
'.pt-flow-mirrored .pt-ex-ui:not(svg){transform:scaleX(-1);}' +
|
|
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;}' +
|
|
256
303
|
'.pt-ex-bar.pt-ex-flow{position:static;justify-content:flex-start;margin-top:.6em;font-size:.8em;}' +
|
|
257
304
|
'.pt-ex-panel.pt-ex-flow{position:static;margin-top:.5em;box-shadow:none;font-size:.9em;}' +
|
|
258
305
|
'.pt-ex-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;}';
|
|
@@ -384,7 +431,7 @@ function exerciseRuntime() {
|
|
|
384
431
|
const persist = opts.persist !== undefined ? opts.persist : data.persist !== false;
|
|
385
432
|
const instant = (opts.check || data.check) === 'instant';
|
|
386
433
|
const words = data.words || {};
|
|
387
|
-
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'] };
|
|
388
435
|
const tf = look.trueFalse;
|
|
389
436
|
const exName = (words.exercise || 'Exercise') + ' ' + rec.number;
|
|
390
437
|
const key = base + ':' + (rec.anchor || '#' + rec.ordinal + '.' + rec.id);
|
|
@@ -393,6 +440,8 @@ function exerciseRuntime() {
|
|
|
393
440
|
const ppt = data.pxPerPt || 1;
|
|
394
441
|
const body = data.bodyPx || 16;
|
|
395
442
|
const flow = data.flow === true;
|
|
443
|
+
// A right-to-left page the HTML viewer paints as a mirrored flow (#1244).
|
|
444
|
+
const mirroredFlow = !!(outers[0] || found[0] || { closest: () => null }).closest('.pt-flow-mirrored');
|
|
396
445
|
// The answer key written for a reader with no script: the bar's own
|
|
397
446
|
// takes its place.
|
|
398
447
|
const statics = scope.querySelectorAll('.pt-ex-static[data-pt-ex="' + id + '"]');
|
|
@@ -401,6 +450,37 @@ function exerciseRuntime() {
|
|
|
401
450
|
// The font of the exercise's text, for what is written on its areas.
|
|
402
451
|
const firstLine = scope.querySelector('.pt-line');
|
|
403
452
|
const textFont = firstLine && firstLine.style ? firstLine.style.fontFamily : '';
|
|
453
|
+
// A drawn box of a reflowable book is set in ems (#1244): its controls
|
|
454
|
+
// go on a layer of the size it prints (`data-pt-w`, `data-pt-h`),
|
|
455
|
+
// scaled to the box as the reader sizes the text.
|
|
456
|
+
if (flow) {
|
|
457
|
+
for (const b in boxEls) {
|
|
458
|
+
const box = boxEls[b];
|
|
459
|
+
const w = parseFloat(box.getAttribute('data-pt-w') || '');
|
|
460
|
+
const h = parseFloat(box.getAttribute('data-pt-h') || '');
|
|
461
|
+
if (!(w > 0) || !(h > 0))
|
|
462
|
+
continue;
|
|
463
|
+
const layer = own(el('div', 'pt-ex-layer', box));
|
|
464
|
+
layer.style.width = px(w);
|
|
465
|
+
layer.style.height = px(h);
|
|
466
|
+
const fit = () => {
|
|
467
|
+
const k = box.clientWidth / w;
|
|
468
|
+
layer.style.transform = k > 0 && Math.abs(k - 1) > 0.001 ? 'scale(' + k + ')' : '';
|
|
469
|
+
};
|
|
470
|
+
fit();
|
|
471
|
+
const view = doc.defaultView;
|
|
472
|
+
if (view && view.ResizeObserver) {
|
|
473
|
+
const ro = new view.ResizeObserver(fit);
|
|
474
|
+
ro.observe(box);
|
|
475
|
+
cleanups.push(() => ro.disconnect());
|
|
476
|
+
}
|
|
477
|
+
else if (view) {
|
|
478
|
+
view.addEventListener('resize', fit);
|
|
479
|
+
cleanups.push(() => view.removeEventListener('resize', fit));
|
|
480
|
+
}
|
|
481
|
+
boxEls[b] = layer;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
404
484
|
function el(tag, cls, parent) {
|
|
405
485
|
const e = doc.createElement(tag);
|
|
406
486
|
e.setAttribute('class', cls);
|
|
@@ -418,6 +498,8 @@ function exerciseRuntime() {
|
|
|
418
498
|
return e;
|
|
419
499
|
}
|
|
420
500
|
function colours(e) {
|
|
501
|
+
if (data.text)
|
|
502
|
+
e.style.setProperty('--pt-ex-text', data.text);
|
|
421
503
|
e.style.setProperty('--pt-ex-right', look.correct);
|
|
422
504
|
e.style.setProperty('--pt-ex-wrong', look.wrong);
|
|
423
505
|
e.style.setProperty('--pt-ex-answer', look.answer);
|
|
@@ -456,6 +538,24 @@ function exerciseRuntime() {
|
|
|
456
538
|
const m = el('span', 'pt-ex-mark ' + (ok ? 'pt-ex-ok' : 'pt-ex-no'), holder);
|
|
457
539
|
m.setAttribute('aria-hidden', 'true');
|
|
458
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))';
|
|
459
559
|
}
|
|
460
560
|
const widgets = [];
|
|
461
561
|
let blankN = 0;
|
|
@@ -611,12 +711,136 @@ function exerciseRuntime() {
|
|
|
611
711
|
const v = state.revealed ? solutionValue(w.slot, rec) : state.v[w.slot.key];
|
|
612
712
|
const at = typeof v === 'string' ? categories.indexOf(v) : -1;
|
|
613
713
|
if (at >= 0 && w.word)
|
|
614
|
-
lists[at].push(w
|
|
714
|
+
lists[at].push(w);
|
|
615
715
|
}
|
|
616
716
|
for (let i = 0; i < columnWidgets.length; i++)
|
|
617
|
-
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;
|
|
618
730
|
}
|
|
619
|
-
/**
|
|
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; };
|
|
841
|
+
}
|
|
842
|
+
/** A word of a sort exercise's bank: dragged to a column, or a chooser
|
|
843
|
+
* of the categories. */
|
|
620
844
|
function chooser(slot, t) {
|
|
621
845
|
const word = chipText(t);
|
|
622
846
|
setAttr(t, 'role', 'button');
|
|
@@ -626,8 +850,7 @@ function exerciseRuntime() {
|
|
|
626
850
|
addClass(t, 'pt-ex-toggle');
|
|
627
851
|
addClass(t, 'pt-ex-ui');
|
|
628
852
|
colours(t);
|
|
629
|
-
const
|
|
630
|
-
tag.hidden = true;
|
|
853
|
+
const wasDrag = dragWord(t, slot, word, false);
|
|
631
854
|
let menu = null;
|
|
632
855
|
// A click anywhere else closes the menu.
|
|
633
856
|
const outside = (e) => { if (menu && !t.contains(e.target))
|
|
@@ -675,33 +898,78 @@ function exerciseRuntime() {
|
|
|
675
898
|
open();
|
|
676
899
|
}
|
|
677
900
|
};
|
|
678
|
-
const onClick = (e) => {
|
|
679
|
-
|
|
901
|
+
const onClick = (e) => {
|
|
902
|
+
if (wasDrag())
|
|
903
|
+
return;
|
|
904
|
+
if (!menu || !menu.contains(e.target))
|
|
905
|
+
open();
|
|
906
|
+
};
|
|
680
907
|
t.addEventListener('click', onClick);
|
|
681
908
|
t.addEventListener('keydown', onKey);
|
|
682
909
|
cleanups.push(() => { close(); t.removeEventListener('click', onClick); t.removeEventListener('keydown', onKey); });
|
|
683
|
-
|
|
910
|
+
const w = {
|
|
684
911
|
slot,
|
|
685
912
|
word,
|
|
913
|
+
ok: null,
|
|
686
914
|
paint(v) {
|
|
687
915
|
const at = typeof v === 'string' ? categories.indexOf(v) : -1;
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
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] : ''));
|
|
691
919
|
refreshColumns();
|
|
692
920
|
},
|
|
693
|
-
lock(on) { t.classList.toggle('pt-ex-revealed', on);
|
|
694
|
-
mark(ok) { badge(t, ok); },
|
|
695
|
-
}
|
|
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);
|
|
696
967
|
}
|
|
697
968
|
/** A text area on a writing area: an open answer, a sentence put in
|
|
698
|
-
* order
|
|
969
|
+
* order. */
|
|
699
970
|
function area(slot, box) {
|
|
700
|
-
const sortColumn = rec.kind === 'sort';
|
|
701
971
|
const ta = own(el('textarea', 'pt-ex-input pt-ex-ui', box));
|
|
702
|
-
|
|
703
|
-
const column = sortColumn ? categories[columnWidgets.length] : undefined;
|
|
704
|
-
ta.setAttribute('aria-label', column !== undefined ? column + ', ' + exName : (words.blank || 'blank') + ' ' + (++blankN) + ', ' + exName);
|
|
972
|
+
ta.setAttribute('aria-label', (words.blank || 'blank') + ' ' + (++blankN) + ', ' + exName);
|
|
705
973
|
ta.setAttribute('spellcheck', 'false');
|
|
706
974
|
const line = (slot.linePt || 0) * ppt || body * 1.35;
|
|
707
975
|
ta.style.fontSize = px(body);
|
|
@@ -710,20 +978,11 @@ function exerciseRuntime() {
|
|
|
710
978
|
if (textFont)
|
|
711
979
|
ta.style.fontFamily = textFont;
|
|
712
980
|
colours(ta);
|
|
713
|
-
|
|
714
|
-
ta.readOnly = true;
|
|
715
|
-
ta.setAttribute('tabindex', '-1');
|
|
716
|
-
ta.setAttribute('aria-live', 'polite');
|
|
717
|
-
}
|
|
718
|
-
else
|
|
719
|
-
ta.addEventListener('input', () => changed(slot, ta.value));
|
|
981
|
+
ta.addEventListener('input', () => changed(slot, ta.value));
|
|
720
982
|
const w = {
|
|
721
983
|
slot,
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
ta.value = typeof v === 'string' ? v : ''; },
|
|
725
|
-
lock(on) { if (!sortColumn)
|
|
726
|
-
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); },
|
|
727
986
|
mark(ok) {
|
|
728
987
|
ta.classList.remove('pt-ex-right', 'pt-ex-wrong');
|
|
729
988
|
const old = box.querySelector(':scope > .pt-ex-mark');
|
|
@@ -737,10 +996,9 @@ function exerciseRuntime() {
|
|
|
737
996
|
m.textContent = ok ? '\u2713' : '\u2717';
|
|
738
997
|
m.style.fontSize = px(body * 0.62);
|
|
739
998
|
colours(m);
|
|
999
|
+
placeMark(m, box);
|
|
740
1000
|
},
|
|
741
1001
|
};
|
|
742
|
-
if (sortColumn)
|
|
743
|
-
columnWidgets.push(w);
|
|
744
1002
|
widgets.push(w);
|
|
745
1003
|
}
|
|
746
1004
|
/** One-character inputs over the cells of a crossword or an operation. */
|
|
@@ -750,7 +1008,10 @@ function exerciseRuntime() {
|
|
|
750
1008
|
const c = slot.cell ? slot.cell[1] : 0;
|
|
751
1009
|
const size = grid.cellPt * ppt;
|
|
752
1010
|
const f = own(el('span', 'pt-ex-field pt-ex-ui', box));
|
|
753
|
-
|
|
1011
|
+
// A right-to-left page's grid is mirrored: its columns count from the
|
|
1012
|
+
// box's right edge. In a flow the page mirrors as a whole (the HTML
|
|
1013
|
+
// viewer's), the flow's left is that edge already.
|
|
1014
|
+
f.style[grid.rtl && !mirroredFlow ? 'right' : 'left'] = px((grid.xPt + c * grid.cellPt) * ppt);
|
|
754
1015
|
f.style.top = px((grid.yPt + r * grid.cellPt) * ppt);
|
|
755
1016
|
f.style.width = px(size);
|
|
756
1017
|
f.style.height = px(size);
|
|
@@ -788,15 +1049,21 @@ function exerciseRuntime() {
|
|
|
788
1049
|
});
|
|
789
1050
|
}
|
|
790
1051
|
/** The dots of a match: a left dot then a right one (or the other way
|
|
791
|
-
* 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. */
|
|
792
1054
|
function matchDots(box, grid, slots) {
|
|
793
1055
|
const w = parseFloat(box.style.width) || 0;
|
|
794
1056
|
const h = parseFloat(box.style.height) || 0;
|
|
795
1057
|
const s = own(svg('svg', { class: 'pt-ex-svg pt-ex-ui', width: w, height: h, viewBox: '0 0 ' + w + ' ' + h }, box));
|
|
796
1058
|
const lines = svg('g', {}, s);
|
|
797
1059
|
const marks = svg('g', {}, s);
|
|
798
|
-
const
|
|
799
|
-
const
|
|
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);
|
|
1064
|
+
// Mirrored on a right-to-left page: the left items stand on the right.
|
|
1065
|
+
const lx = grid.rtl ? w - grid.dotsPt[0] * ppt : grid.dotsPt[0] * ppt;
|
|
1066
|
+
const rx = grid.rtl ? w - grid.dotsPt[1] * ppt : grid.dotsPt[1] * ppt;
|
|
800
1067
|
const rows = grid.rowsPt.map((y) => y * ppt);
|
|
801
1068
|
const radius = Math.max(body * 0.55, 6);
|
|
802
1069
|
let pendingLeft = -1;
|
|
@@ -808,7 +1075,50 @@ function exerciseRuntime() {
|
|
|
808
1075
|
leftDots[i].classList.toggle('pt-ex-pending', i === pendingLeft);
|
|
809
1076
|
for (let i = 0; i < rightDots.length; i++)
|
|
810
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');
|
|
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;
|
|
811
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;
|
|
812
1122
|
const join = (li, ri) => {
|
|
813
1123
|
pendingLeft = -1;
|
|
814
1124
|
pendingRight = -1;
|
|
@@ -834,6 +1144,7 @@ function exerciseRuntime() {
|
|
|
834
1144
|
join(i, pendingRight);
|
|
835
1145
|
else {
|
|
836
1146
|
pendingLeft = pendingLeft === i ? -1 : i;
|
|
1147
|
+
pendingRight = -1;
|
|
837
1148
|
refresh();
|
|
838
1149
|
}
|
|
839
1150
|
}
|
|
@@ -841,10 +1152,61 @@ function exerciseRuntime() {
|
|
|
841
1152
|
join(pendingLeft, i);
|
|
842
1153
|
else {
|
|
843
1154
|
pendingRight = pendingRight === i ? -1 : i;
|
|
1155
|
+
pendingLeft = -1;
|
|
844
1156
|
refresh();
|
|
845
1157
|
}
|
|
846
1158
|
};
|
|
847
|
-
dot
|
|
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;
|
|
1201
|
+
refresh();
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
dot.addEventListener('pointerup', drop);
|
|
1205
|
+
dot.addEventListener('pointercancel', drop);
|
|
1206
|
+
dot.addEventListener('click', () => { if (skipClick) {
|
|
1207
|
+
skipClick = false;
|
|
1208
|
+
return;
|
|
1209
|
+
} pick(); });
|
|
848
1210
|
dot.addEventListener('keydown', (e) => { if (e.key === ' ' || e.key === 'Enter') {
|
|
849
1211
|
e.preventDefault();
|
|
850
1212
|
pick();
|
|
@@ -864,7 +1226,7 @@ function exerciseRuntime() {
|
|
|
864
1226
|
line = null;
|
|
865
1227
|
}
|
|
866
1228
|
if (typeof v === 'number' && rows[v - 1] !== undefined) {
|
|
867
|
-
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);
|
|
868
1230
|
if (state.revealed)
|
|
869
1231
|
line.setAttribute('stroke', look.answer);
|
|
870
1232
|
}
|
|
@@ -876,10 +1238,10 @@ function exerciseRuntime() {
|
|
|
876
1238
|
mark = null;
|
|
877
1239
|
}
|
|
878
1240
|
if (line)
|
|
879
|
-
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);
|
|
880
1242
|
if (ok === null)
|
|
881
1243
|
return;
|
|
882
|
-
mark = svg('text', { x: lx + radius * 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);
|
|
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);
|
|
883
1245
|
mark.textContent = ok ? '\u2713' : '\u2717';
|
|
884
1246
|
},
|
|
885
1247
|
});
|
|
@@ -890,11 +1252,13 @@ function exerciseRuntime() {
|
|
|
890
1252
|
const w = parseFloat(box.style.width) || 0;
|
|
891
1253
|
const h = parseFloat(box.style.height) || 0;
|
|
892
1254
|
const cellPx = grid.cellPt * ppt;
|
|
893
|
-
const x0 = grid.xPt * ppt;
|
|
894
1255
|
const y0 = grid.yPt * ppt;
|
|
1256
|
+
// Where column `c` (a fraction in it) stands: from the box's right edge
|
|
1257
|
+
// on a right-to-left page, whose grid is mirrored.
|
|
1258
|
+
const colX = (c) => (grid.rtl ? w - grid.xPt * ppt - c * cellPx : grid.xPt * ppt + c * cellPx);
|
|
895
1259
|
const s = own(svg('svg', { class: 'pt-ex-svg pt-ex-ui', width: w, height: h, viewBox: '0 0 ' + w + ' ' + h, role: 'group', 'aria-label': exName }, box));
|
|
896
1260
|
const paintG = svg('g', {}, s);
|
|
897
|
-
const hit = svg('rect', { x:
|
|
1261
|
+
const hit = svg('rect', { x: Math.min(colX(0), colX(grid.cols)), y: y0, width: grid.cols * cellPx, height: grid.rows * cellPx, fill: 'transparent', style: 'cursor:pointer;pointer-events:all' }, s);
|
|
898
1262
|
let start = null;
|
|
899
1263
|
let startMark = null;
|
|
900
1264
|
const capsule = (sel, color) => {
|
|
@@ -902,7 +1266,7 @@ function exerciseRuntime() {
|
|
|
902
1266
|
const a = ends[0].split(',').map(Number);
|
|
903
1267
|
const b = ends[1].split(',').map(Number);
|
|
904
1268
|
return svg('line', {
|
|
905
|
-
x1:
|
|
1269
|
+
x1: colX(a[1] + 0.5), y1: y0 + (a[0] + 0.5) * cellPx, x2: colX(b[1] + 0.5), y2: y0 + (b[0] + 0.5) * cellPx,
|
|
906
1270
|
stroke: color, 'stroke-opacity': 0.75, 'stroke-width': cellPx * 0.78, 'stroke-linecap': 'round', fill: 'none', 'pointer-events': 'none',
|
|
907
1271
|
}, paintG);
|
|
908
1272
|
};
|
|
@@ -912,13 +1276,13 @@ function exerciseRuntime() {
|
|
|
912
1276
|
const r = hit.getBoundingClientRect();
|
|
913
1277
|
const sx = r.width / (grid.cols * cellPx) || 1;
|
|
914
1278
|
const sy = r.height / (grid.rows * cellPx) || 1;
|
|
915
|
-
const c = Math.floor((e.clientX - r.left) / sx / cellPx);
|
|
1279
|
+
const c = Math.floor((grid.rtl ? r.right - e.clientX : e.clientX - r.left) / sx / cellPx);
|
|
916
1280
|
const rr = Math.floor((e.clientY - r.top) / sy / cellPx);
|
|
917
1281
|
if (rr < 0 || c < 0 || rr >= grid.rows || c >= grid.cols)
|
|
918
1282
|
return;
|
|
919
1283
|
if (!start) {
|
|
920
1284
|
start = [rr, c];
|
|
921
|
-
startMark = svg('circle', { cx:
|
|
1285
|
+
startMark = svg('circle', { cx: colX(c + 0.5), cy: y0 + (rr + 0.5) * cellPx, r: cellPx * 0.39, fill: 'none', stroke: '#1a5fb4', 'stroke-width': 2, 'pointer-events': 'none' }, s);
|
|
922
1286
|
return;
|
|
923
1287
|
}
|
|
924
1288
|
const dr = rr - start[0];
|
|
@@ -970,7 +1334,7 @@ function exerciseRuntime() {
|
|
|
970
1334
|
painted.push(capsule(list[i], state.revealed ? look.answer : ok === null ? '#9ec5ff' : ok ? look.correct : look.wrong));
|
|
971
1335
|
if (ok !== null) {
|
|
972
1336
|
const ends = list[i].split('-')[1].split(',').map(Number);
|
|
973
|
-
const t = svg('text', { x:
|
|
1337
|
+
const t = svg('text', { x: colX(ends[1] + 1), ...(grid.rtl ? { 'text-anchor': 'end' } : {}), y: y0 + ends[0] * cellPx + cellPx * 0.3, 'font-size': cellPx * 0.5, 'font-weight': 700, fill: ok ? look.correct : look.wrong, 'aria-hidden': 'true' }, paintG);
|
|
974
1338
|
t.textContent = ok ? '\u2713' : '\u2717';
|
|
975
1339
|
painted.push(t);
|
|
976
1340
|
}
|
|
@@ -1034,7 +1398,9 @@ function exerciseRuntime() {
|
|
|
1034
1398
|
}
|
|
1035
1399
|
else if (slot.kind === 'text') {
|
|
1036
1400
|
const box = slot.box !== undefined ? boxEls[String(slot.box)] : null;
|
|
1037
|
-
if (box)
|
|
1401
|
+
if (box && rec.kind === 'sort')
|
|
1402
|
+
sortColumn(slot, box);
|
|
1403
|
+
else if (box && !slot.bare)
|
|
1038
1404
|
area(slot, box);
|
|
1039
1405
|
}
|
|
1040
1406
|
else if (slot.kind === 'cell') {
|
|
@@ -1058,8 +1424,15 @@ function exerciseRuntime() {
|
|
|
1058
1424
|
if (g.kind === 'wordsearch' && wordSlots.length > 0)
|
|
1059
1425
|
wordSearch(box, g, wordSlots);
|
|
1060
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;
|
|
1061
1434
|
// The bar: on the last part of the exercise's outer box.
|
|
1062
|
-
const outer = outers.length > 0 ? outers[outers.length - 1] : null;
|
|
1435
|
+
const outer = outers.length > 0 && (resettable || solvable || hasHints) ? outers[outers.length - 1] : null;
|
|
1063
1436
|
let scoreEl = null;
|
|
1064
1437
|
let panel = null;
|
|
1065
1438
|
let solutionBtn = null;
|
|
@@ -1081,18 +1454,47 @@ function exerciseRuntime() {
|
|
|
1081
1454
|
scoreEl = el('span', 'pt-ex-score', bar);
|
|
1082
1455
|
scoreEl.setAttribute('aria-live', 'polite');
|
|
1083
1456
|
scoreEl.setAttribute('role', 'status');
|
|
1084
|
-
if (
|
|
1457
|
+
if (hasHints)
|
|
1085
1458
|
hintBtn = button(words.hint || 'Hint', () => showPanel(panel && panel.getAttribute('data-show') === 'hint' ? null : 'hint'));
|
|
1086
|
-
if (!instant)
|
|
1459
|
+
if (!instant && checkable)
|
|
1087
1460
|
button(words.check || 'Check', () => { state.checked = true; persistNow(); feedback(); });
|
|
1088
|
-
|
|
1089
|
-
|
|
1461
|
+
if (solvable)
|
|
1462
|
+
solutionBtn = button(words.showSolution || 'Show solution', () => reveal(!state.revealed));
|
|
1463
|
+
if (resettable)
|
|
1464
|
+
button(words.reset || 'Try again', reset);
|
|
1090
1465
|
panel = el('div', 'pt-ex-panel pt-ex-ui' + (flow ? ' pt-ex-flow' : ''), outer);
|
|
1091
1466
|
own(panel);
|
|
1092
1467
|
panel.hidden = true;
|
|
1093
1468
|
if (!flow)
|
|
1094
1469
|
panel.style.fontSize = px(Math.max(body * 0.72, 10));
|
|
1095
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)';
|
|
1096
1498
|
}
|
|
1097
1499
|
void hintBtn;
|
|
1098
1500
|
function showPanel(what) {
|
|
@@ -1114,6 +1516,11 @@ function exerciseRuntime() {
|
|
|
1114
1516
|
for (let i = 0; i < sol.length; i++)
|
|
1115
1517
|
el('p', '', panel).textContent = sol[i];
|
|
1116
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);
|
|
1117
1524
|
}
|
|
1118
1525
|
function valueOf(w) {
|
|
1119
1526
|
if (state.revealed)
|
|
@@ -1139,6 +1546,13 @@ function exerciseRuntime() {
|
|
|
1139
1546
|
const s = rec.slots[i];
|
|
1140
1547
|
if (!isGraded(s, rec))
|
|
1141
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
|
+
}
|
|
1142
1556
|
total++;
|
|
1143
1557
|
if (verdict(s) === true)
|
|
1144
1558
|
right++;
|
|
@@ -1230,7 +1644,7 @@ function exerciseRuntime() {
|
|
|
1230
1644
|
solutionBtn.setAttribute('aria-expanded', 'false');
|
|
1231
1645
|
return { record: rec, state, reveal, reset, feedback };
|
|
1232
1646
|
}
|
|
1233
|
-
return { normalizeAnswer, isGraded, checkSlot, solutionValue, attach };
|
|
1647
|
+
return { normalizeAnswer, isGraded, checkSlot, solutionValue, actionsOf, attach };
|
|
1234
1648
|
}
|
|
1235
1649
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
1236
1650
|
const runtime = exerciseRuntime();
|
|
@@ -1242,6 +1656,8 @@ export const isGradedSlot = runtime.isGraded;
|
|
|
1242
1656
|
* is no answer or not graded (`null`). `trueFalse` are the style's true
|
|
1243
1657
|
* and false labels, for a typed true/false. */
|
|
1244
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;
|
|
1245
1661
|
/** What a slot shows when the solution is shown. */
|
|
1246
1662
|
export const exerciseSolutionValue = runtime.solutionValue;
|
|
1247
1663
|
/** Makes the exercises of the HTML output under `root` answerable (see the
|