@webwriter/quiz 1.1.3 → 2.0.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/.github/workflows/publish.yml +8 -0
- package/LICENSE +6 -6
- package/README.md +778 -0
- package/assets/icons/grip-horizontal.svg +8 -0
- package/assets/icons/grip-vertical.svg +8 -0
- package/assets/icons/truefalse.svg +6 -0
- package/custom-elements.json +2779 -0
- package/custom.d.ts +180 -176
- package/dist/api/index.d.ts +27 -0
- package/dist/api/index.js +40 -0
- package/dist/migrate.js +1434 -0
- package/dist/widgets/webwriter-choice-item.js +4036 -3895
- package/dist/widgets/webwriter-choice.js +2269 -5821
- package/dist/widgets/webwriter-gap-item.js +3987 -0
- package/dist/widgets/webwriter-gap.js +2908 -0
- package/dist/widgets/webwriter-mark.js +4335 -3489
- package/dist/widgets/webwriter-order-item.js +4014 -3306
- package/dist/widgets/webwriter-order.js +2944 -4386
- package/dist/widgets/webwriter-pairing-item.js +1058 -2421
- package/dist/widgets/webwriter-pairing.js +2993 -4383
- package/dist/widgets/webwriter-quiz-hint.js +2469 -0
- package/dist/widgets/webwriter-quiz.js +4933 -6099
- package/dist/widgets/webwriter-speech.js +6077 -5230
- package/dist/widgets/webwriter-task-prompt.js +4775 -2165
- package/dist/widgets/webwriter-task.js +2364 -8382
- package/dist/widgets/webwriter-text.js +4169 -4337
- package/dist/widgets/webwriter-true-false.js +4674 -0
- package/editing-config.json +106 -0
- package/lit-localize.json +21 -15
- package/localization/de.xlf +407 -74
- package/localization/es.xlf +407 -69
- package/localization/fr.xlf +407 -69
- package/localization/generated/de.ts +112 -29
- package/localization/generated/es.ts +112 -29
- package/localization/generated/fr.ts +112 -29
- package/localization/generated/index.d.ts +5 -0
- package/localization/generated/index.js +2 -2
- package/localization/generated/it.ts +112 -29
- package/localization/generated/locale-codes.js +31 -79
- package/localization/generated/nl.ts +112 -29
- package/localization/it.xlf +407 -69
- package/localization/nl.xlf +407 -69
- package/package.json +119 -252
- package/scripts/apply-translations.mjs +97 -0
- package/src/api/index.ts +72 -0
- package/src/lib/artboard.ts +686 -0
- package/src/lib/draggable-grid.ts +432 -0
- package/src/lib/encrypted-property.ts +202 -0
- package/src/lib/hint-popup.ts +152 -0
- package/src/lib/utils.ts +97 -0
- package/src/lib/webwriter-quirks.ts +121 -0
- package/src/migrate/index.ts +622 -0
- package/src/migrate/legacy-solution.ts +362 -0
- package/src/snippets/order.html +9 -8
- package/src/widgets/choice/webwriter-choice-item.ts +316 -0
- package/src/widgets/choice/webwriter-choice.ts +328 -0
- package/src/widgets/choice/webwriter-true-false.ts +113 -0
- package/src/widgets/dnd/webwriter-dnd-draggable.ts +28 -0
- package/src/widgets/dnd/webwriter-dnd-dropzone.ts +9 -0
- package/src/widgets/dnd/webwriter-dnd.ts +36 -0
- package/src/widgets/gap/webwriter-gap-item.ts +250 -0
- package/src/widgets/gap/webwriter-gap.ts +447 -0
- package/src/widgets/order/webwriter-order-item.ts +243 -0
- package/src/widgets/order/webwriter-order.ts +282 -0
- package/src/widgets/pairing/webwriter-pairing-item.ts +46 -0
- package/src/widgets/pairing/webwriter-pairing.ts +934 -0
- package/src/widgets/speech/audio-player.ts +325 -0
- package/src/widgets/speech/audio-recorder.ts +285 -0
- package/src/widgets/speech/waveform.ts +113 -0
- package/src/widgets/speech/webwriter-speech.ts +87 -0
- package/src/widgets/webwriter-mark.ts +374 -432
- package/src/widgets/webwriter-quiz-hint.ts +36 -0
- package/src/widgets/webwriter-quiz.ts +320 -282
- package/src/widgets/webwriter-task-prompt.ts +49 -24
- package/src/widgets/webwriter-task.ts +216 -547
- package/src/widgets/webwriter-text.ts +291 -156
- package/tsconfig.api.json +12 -0
- package/tsconfig.json +8 -7
- package/dist/widgets/webwriter-choice.css +0 -444
- package/dist/widgets/webwriter-cloze-gap.css +0 -444
- package/dist/widgets/webwriter-cloze-gap.js +0 -5866
- package/dist/widgets/webwriter-cloze.css +0 -444
- package/dist/widgets/webwriter-cloze.js +0 -3786
- package/dist/widgets/webwriter-mark.css +0 -444
- package/dist/widgets/webwriter-order.css +0 -444
- package/dist/widgets/webwriter-pairing-item.css +0 -444
- package/dist/widgets/webwriter-pairing.css +0 -444
- package/dist/widgets/webwriter-quiz.css +0 -444
- package/dist/widgets/webwriter-task-explainer.css +0 -444
- package/dist/widgets/webwriter-task-explainer.js +0 -2872
- package/dist/widgets/webwriter-task-hint.css +0 -444
- package/dist/widgets/webwriter-task-hint.js +0 -2811
- package/dist/widgets/webwriter-task.css +0 -444
- package/dist/widgets/webwriter-text.css +0 -444
- package/localization/bg.xlf +0 -69
- package/localization/cs.xlf +0 -69
- package/localization/da.xlf +0 -69
- package/localization/el.xlf +0 -69
- package/localization/et.xlf +0 -69
- package/localization/fi.xlf +0 -69
- package/localization/generated/bg.ts +0 -30
- package/localization/generated/cs.ts +0 -30
- package/localization/generated/da.ts +0 -30
- package/localization/generated/el.ts +0 -30
- package/localization/generated/et.ts +0 -30
- package/localization/generated/fi.ts +0 -30
- package/localization/generated/hu.ts +0 -30
- package/localization/generated/id.ts +0 -30
- package/localization/generated/ja.ts +0 -30
- package/localization/generated/ko.ts +0 -30
- package/localization/generated/lt.ts +0 -30
- package/localization/generated/lv.ts +0 -30
- package/localization/generated/nb.ts +0 -30
- package/localization/generated/pl.ts +0 -30
- package/localization/generated/pt-BR.ts +0 -30
- package/localization/generated/pt-PT.ts +0 -30
- package/localization/generated/ro.ts +0 -30
- package/localization/generated/ru.ts +0 -30
- package/localization/generated/sk.ts +0 -30
- package/localization/generated/sl.ts +0 -30
- package/localization/generated/sv.ts +0 -30
- package/localization/generated/tr.ts +0 -30
- package/localization/generated/uk.ts +0 -30
- package/localization/generated/zh-hans.ts +0 -30
- package/localization/hu.xlf +0 -69
- package/localization/id.xlf +0 -69
- package/localization/ja.xlf +0 -69
- package/localization/ko.xlf +0 -69
- package/localization/lt.xlf +0 -69
- package/localization/lv.xlf +0 -69
- package/localization/nb.xlf +0 -69
- package/localization/pl.xlf +0 -69
- package/localization/pt-BR.xlf +0 -69
- package/localization/pt-PT.xlf +0 -69
- package/localization/ro.xlf +0 -69
- package/localization/ru.xlf +0 -69
- package/localization/sk.xlf +0 -69
- package/localization/sl.xlf +0 -69
- package/localization/sv.xlf +0 -69
- package/localization/tr.xlf +0 -69
- package/localization/uk.xlf +0 -69
- package/localization/zh-hans.xlf +0 -69
- package/src/lib/combobox.ts +0 -456
- package/src/lib/highlighter-fill.svg +0 -6
- package/src/snippets/Beispiel-Choice.html +0 -57
- package/src/snippets/Beispiel-Mark.html +0 -14
- package/src/snippets/Beispiel-Order.html +0 -52
- package/src/snippets/Beispiel-Text.html +0 -20
- package/src/snippets/blank.html +0 -1
- package/src/snippets/choice.html +0 -8
- package/src/snippets/cloze.html +0 -4
- package/src/snippets/mark.html +0 -4
- package/src/snippets/pairing.html +0 -10
- package/src/snippets/speech.html +0 -4
- package/src/snippets/text.html +0 -4
- package/src/snippets/wordsearch.html +0 -4
- package/src/widgets/webwriter-choice-item.ts +0 -254
- package/src/widgets/webwriter-choice.ts +0 -353
- package/src/widgets/webwriter-cloze-gap.ts +0 -216
- package/src/widgets/webwriter-cloze.ts +0 -136
- package/src/widgets/webwriter-order-item.ts +0 -441
- package/src/widgets/webwriter-order.ts +0 -327
- package/src/widgets/webwriter-pairing-item.ts +0 -161
- package/src/widgets/webwriter-pairing.ts +0 -193
- package/src/widgets/webwriter-speech.ts +0 -273
- package/src/widgets/webwriter-task-explainer.ts +0 -43
- package/src/widgets/webwriter-task-hint.ts +0 -23
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { css } from "lit";
|
|
2
|
+
import { createRef } from "lit/directives/ref.js";
|
|
3
|
+
|
|
4
|
+
import { resolveCssVariable } from "../../lib/utils";
|
|
5
|
+
|
|
6
|
+
/** Horizontal distance between two amplitude bars, in CSS pixels. */
|
|
7
|
+
export const SAMPLE_BAR_SPACING = 6;
|
|
8
|
+
/** Stroke width of a single amplitude bar, in CSS pixels. */
|
|
9
|
+
export const BAR_WIDTH = 3;
|
|
10
|
+
|
|
11
|
+
/** Styles shared by the recorder and player waveforms. */
|
|
12
|
+
export const waveformStyles = css`
|
|
13
|
+
.duration {
|
|
14
|
+
font-feature-settings: "tnum";
|
|
15
|
+
font-variant-numeric: tabular-nums;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.waveform {
|
|
19
|
+
flex-grow: 1;
|
|
20
|
+
height: 40px;
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.waveform canvas {
|
|
25
|
+
display: block;
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
/** Formats a duration in seconds as MM:SS. */
|
|
30
|
+
export function formatDuration(seconds: number) {
|
|
31
|
+
const minutes = Math.floor(seconds / 60);
|
|
32
|
+
const remainder = Math.floor(seconds % 60);
|
|
33
|
+
return `${minutes.toString().padStart(2, "0")}:${remainder.toString().padStart(2, "0")}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Largest deviation of `data[start..end)` from `center`, normalized to 0-1 by `scale`.
|
|
38
|
+
* Time domain data is unsigned (0-255 around 128), decoded sample data is signed (-1 to 1).
|
|
39
|
+
*/
|
|
40
|
+
export function peakAmplitude(data: ArrayLike<number>, start: number, end: number, center = 0, scale = 1) {
|
|
41
|
+
let peak = 0;
|
|
42
|
+
for (let i = start; i < end; i++) {
|
|
43
|
+
const amplitude = Math.abs(data[i] - center) / scale;
|
|
44
|
+
if (amplitude > peak) peak = amplitude;
|
|
45
|
+
}
|
|
46
|
+
return peak;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class WaveformCanvas {
|
|
50
|
+
readonly container = createRef<HTMLDivElement>();
|
|
51
|
+
readonly canvas = createRef<HTMLCanvasElement>();
|
|
52
|
+
|
|
53
|
+
private ctx: CanvasRenderingContext2D | null = null;
|
|
54
|
+
|
|
55
|
+
get width() {
|
|
56
|
+
return (this.ctx?.canvas.width ?? 0) / devicePixelRatio;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get height() {
|
|
60
|
+
return (this.ctx?.canvas.height ?? 0) / devicePixelRatio;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get containerBounds() {
|
|
64
|
+
return this.container.value?.getBoundingClientRect();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get activeColor() {
|
|
68
|
+
return resolveCssVariable("--sl-color-primary-600", "#0484c7");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get inactiveColor() {
|
|
72
|
+
return resolveCssVariable("--sl-color-neutral-200", "#e4e4e7");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get backgroundColor() {
|
|
76
|
+
return resolveCssVariable("--sl-color-neutral-0", "#ffffff");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Matches the canvas backing store to its container, accounting for the device pixel ratio. */
|
|
80
|
+
resize() {
|
|
81
|
+
const bounds = this.containerBounds;
|
|
82
|
+
const canvas = this.canvas.value;
|
|
83
|
+
if (!bounds || !canvas) return false;
|
|
84
|
+
|
|
85
|
+
canvas.width = bounds.width * devicePixelRatio;
|
|
86
|
+
canvas.height = bounds.height * devicePixelRatio;
|
|
87
|
+
canvas.style.width = `${bounds.width}px`;
|
|
88
|
+
canvas.style.height = `${bounds.height}px`;
|
|
89
|
+
|
|
90
|
+
this.ctx = canvas.getContext("2d")!;
|
|
91
|
+
this.ctx.resetTransform();
|
|
92
|
+
this.ctx.scale(devicePixelRatio, devicePixelRatio);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Clears the canvas and returns its context, or null while the canvas is not sized yet. */
|
|
97
|
+
begin() {
|
|
98
|
+
this.ctx?.clearRect(0, 0, this.width, this.height);
|
|
99
|
+
return this.ctx;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Draws a single bar of the given amplitude (0-1), vertically centered at `x`. */
|
|
103
|
+
drawBar(x: number, amplitude: number, color: string) {
|
|
104
|
+
const ctx = this.ctx!;
|
|
105
|
+
ctx.beginPath();
|
|
106
|
+
ctx.moveTo(x, (this.height / 2) * (0.95 - amplitude * 0.85));
|
|
107
|
+
ctx.lineTo(x, (this.height / 2) * (1.05 + amplitude * 0.85));
|
|
108
|
+
ctx.strokeStyle = color;
|
|
109
|
+
ctx.lineCap = "round";
|
|
110
|
+
ctx.lineWidth = BAR_WIDTH;
|
|
111
|
+
ctx.stroke();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { msg } from "@lit/localize";
|
|
2
|
+
import "@shoelace-style/shoelace/dist/themes/light.css";
|
|
3
|
+
import { LitElementWw } from "@webwriter/lit";
|
|
4
|
+
import MicIcon from "bootstrap-icons/icons/mic.svg";
|
|
5
|
+
import { css, html } from "lit";
|
|
6
|
+
import { customElement, property } from "lit/decorators.js";
|
|
7
|
+
import LOCALIZE from "../../../localization/generated";
|
|
8
|
+
import { name as packageId } from "../../../package.json";
|
|
9
|
+
|
|
10
|
+
import { IWebWriterQuizType, registerQuizType } from "../../api";
|
|
11
|
+
import { AudioPlayer } from "./audio-player";
|
|
12
|
+
import { AudioRecorder } from "./audio-recorder";
|
|
13
|
+
|
|
14
|
+
declare global {
|
|
15
|
+
interface HTMLElementTagNameMap {
|
|
16
|
+
"webwriter-speech": WebwriterSpeech;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
registerQuizType({
|
|
21
|
+
packageId,
|
|
22
|
+
widgetPosition: 40,
|
|
23
|
+
id: "webwriter-speech",
|
|
24
|
+
getName: () => msg("Speech"),
|
|
25
|
+
icon: MicIcon,
|
|
26
|
+
createInstance: () => document.createElement("webwriter-speech"),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* An answer where learners record a spoken response.
|
|
31
|
+
*
|
|
32
|
+
* It must be assigned to the default slot of a `<webwriter-task>`.
|
|
33
|
+
*/
|
|
34
|
+
@customElement("webwriter-speech")
|
|
35
|
+
export class WebwriterSpeech extends LitElementWw implements IWebWriterQuizType {
|
|
36
|
+
/** @internal */
|
|
37
|
+
localize = LOCALIZE;
|
|
38
|
+
|
|
39
|
+
/** @internal */
|
|
40
|
+
static scopedElements = {
|
|
41
|
+
"audio-recorder": AudioRecorder,
|
|
42
|
+
"audio-player": AudioPlayer,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
static styles = css`
|
|
46
|
+
:host {
|
|
47
|
+
display: block;
|
|
48
|
+
width: 100%;
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The recording of the learner as a data URL, or `null` if nothing was recorded yet.
|
|
54
|
+
*/
|
|
55
|
+
@property({ type: String, attribute: true, reflect: true })
|
|
56
|
+
accessor src: string | null = null;
|
|
57
|
+
|
|
58
|
+
reset() {
|
|
59
|
+
this.src = null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
checkValidity(): boolean {
|
|
63
|
+
return this.src !== null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
render() {
|
|
67
|
+
if (!this.src) {
|
|
68
|
+
return html`<audio-recorder
|
|
69
|
+
@recording-complete=${(e: CustomEvent) => {
|
|
70
|
+
const blob = e.detail.audioBlob;
|
|
71
|
+
const reader = new FileReader();
|
|
72
|
+
reader.onloadend = () => {
|
|
73
|
+
this.src = reader.result as string;
|
|
74
|
+
};
|
|
75
|
+
reader.readAsDataURL(blob);
|
|
76
|
+
}}
|
|
77
|
+
></audio-recorder>`;
|
|
78
|
+
} else {
|
|
79
|
+
return html`<audio-player
|
|
80
|
+
.src=${this.src}
|
|
81
|
+
@delete-recording=${() => {
|
|
82
|
+
this.src = null;
|
|
83
|
+
}}
|
|
84
|
+
></audio-player>`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|