@webwriter/quiz 1.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/LICENSE +7 -0
- package/custom.d.ts +176 -0
- package/dist/widgets/webwriter-choice-item.css +444 -0
- package/dist/widgets/webwriter-choice-item.js +288 -0
- package/dist/widgets/webwriter-choice.css +444 -0
- package/dist/widgets/webwriter-choice.js +327 -0
- package/dist/widgets/webwriter-cloze-gap.css +444 -0
- package/dist/widgets/webwriter-cloze-gap.js +722 -0
- package/dist/widgets/webwriter-cloze.css +444 -0
- package/dist/widgets/webwriter-cloze.js +177 -0
- package/dist/widgets/webwriter-mark.css +444 -0
- package/dist/widgets/webwriter-mark.js +410 -0
- package/dist/widgets/webwriter-order-item.css +444 -0
- package/dist/widgets/webwriter-order-item.js +464 -0
- package/dist/widgets/webwriter-order.css +444 -0
- package/dist/widgets/webwriter-order.js +560 -0
- package/dist/widgets/webwriter-pairing-item.css +444 -0
- package/dist/widgets/webwriter-pairing-item.js +203 -0
- package/dist/widgets/webwriter-pairing.css +444 -0
- package/dist/widgets/webwriter-pairing.js +224 -0
- package/dist/widgets/webwriter-quiz.css +444 -0
- package/dist/widgets/webwriter-quiz.js +323 -0
- package/dist/widgets/webwriter-speech.css +444 -0
- package/dist/widgets/webwriter-speech.js +320 -0
- package/dist/widgets/webwriter-task-explainer.css +444 -0
- package/dist/widgets/webwriter-task-explainer.js +90 -0
- package/dist/widgets/webwriter-task-hint.css +444 -0
- package/dist/widgets/webwriter-task-hint.js +67 -0
- package/dist/widgets/webwriter-task-prompt.css +444 -0
- package/dist/widgets/webwriter-task-prompt.js +68 -0
- package/dist/widgets/webwriter-task.css +444 -0
- package/dist/widgets/webwriter-task.js +546 -0
- package/dist/widgets/webwriter-text.css +444 -0
- package/dist/widgets/webwriter-text.js +174 -0
- package/package.json +199 -0
- package/src/lib/combobox.ts +456 -0
- package/src/lib/highlighter-fill.svg +6 -0
- package/src/snippets/choice.html +8 -0
- package/src/snippets/cloze.html +4 -0
- package/src/snippets/mark.html +4 -0
- package/src/snippets/order.html +8 -0
- package/src/snippets/pairing.html +10 -0
- package/src/snippets/speech.html +4 -0
- package/src/snippets/text.html +4 -0
- package/src/snippets/wordsearch.html +4 -0
- package/src/widgets/webwriter-choice-item.ts +251 -0
- package/src/widgets/webwriter-choice.ts +310 -0
- package/src/widgets/webwriter-cloze-gap.ts +217 -0
- package/src/widgets/webwriter-cloze.ts +136 -0
- package/src/widgets/webwriter-mark.ts +435 -0
- package/src/widgets/webwriter-order-item.ts +445 -0
- package/src/widgets/webwriter-order.ts +273 -0
- package/src/widgets/webwriter-pairing-item.ts +156 -0
- package/src/widgets/webwriter-pairing.ts +188 -0
- package/src/widgets/webwriter-quiz.ts +281 -0
- package/src/widgets/webwriter-speech.ts +268 -0
- package/src/widgets/webwriter-task-explainer.ts +38 -0
- package/src/widgets/webwriter-task-hint.ts +17 -0
- package/src/widgets/webwriter-task-prompt.ts +18 -0
- package/src/widgets/webwriter-task.ts +544 -0
- package/src/widgets/webwriter-text.ts +131 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import {html, css, PropertyValueMap} from "lit"
|
|
2
|
+
import {LitElementWw} from "@webwriter/lit"
|
|
3
|
+
import {customElement, property, query} from "lit/decorators.js"
|
|
4
|
+
import "@shoelace-style/shoelace/dist/themes/light.css"
|
|
5
|
+
|
|
6
|
+
import IconMic from "bootstrap-icons/icons/mic.svg"
|
|
7
|
+
import IconMicFill from "bootstrap-icons/icons/mic-fill.svg"
|
|
8
|
+
import IconStopFill from "bootstrap-icons/icons/stop-fill.svg"
|
|
9
|
+
import IconTrash from "bootstrap-icons/icons/trash.svg"
|
|
10
|
+
import IconPlay from "bootstrap-icons/icons/play.svg"
|
|
11
|
+
import IconPause from "bootstrap-icons/icons/pause.svg"
|
|
12
|
+
import IconVolumeDown from "bootstrap-icons/icons/volume-down.svg"
|
|
13
|
+
import SlButton from "@shoelace-style/shoelace/dist/components/button/button.component.js"
|
|
14
|
+
import SlRange from "@shoelace-style/shoelace/dist/components/range/range.component.js"
|
|
15
|
+
import SlButtonGroup from "@shoelace-style/shoelace/dist/components/button-group/button-group.component.js"
|
|
16
|
+
import SlIcon from "@shoelace-style/shoelace/dist/components/icon/icon.component.js"
|
|
17
|
+
import SlFormatDate from "@shoelace-style/shoelace/dist/components/format-date/format-date.component.js"
|
|
18
|
+
import SlPopup from "@shoelace-style/shoelace/dist/components/popup/popup.component.js"
|
|
19
|
+
|
|
20
|
+
declare global {interface HTMLElementTagNameMap {
|
|
21
|
+
"webwriter-speech": WebwriterSpeech;
|
|
22
|
+
}}
|
|
23
|
+
|
|
24
|
+
@customElement("webwriter-speech")
|
|
25
|
+
export class WebwriterSpeech extends LitElementWw {
|
|
26
|
+
|
|
27
|
+
static scopedElements = {
|
|
28
|
+
"sl-button": SlButton,
|
|
29
|
+
"sl-icon": SlIcon,
|
|
30
|
+
"sl-button-group": SlButtonGroup,
|
|
31
|
+
"sl-range": SlRange,
|
|
32
|
+
"sl-format-date": SlFormatDate,
|
|
33
|
+
"sl-popup": SlPopup
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static styles = css`
|
|
37
|
+
|
|
38
|
+
@keyframes blinker {
|
|
39
|
+
50% {
|
|
40
|
+
opacity: 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
sl-icon {
|
|
45
|
+
height: 20px;
|
|
46
|
+
width: 20px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
sl-button-group {
|
|
50
|
+
width: 100%;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#record {
|
|
54
|
+
position: relative;
|
|
55
|
+
#recording-indicator {
|
|
56
|
+
background: darkred;
|
|
57
|
+
border-radius: 100%;
|
|
58
|
+
height: 10px;
|
|
59
|
+
width: 10px;
|
|
60
|
+
position: absolute;
|
|
61
|
+
top: 5px;
|
|
62
|
+
right: 5px;
|
|
63
|
+
animation: blinker 1.25s linear infinite;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#record:hover .idle {
|
|
68
|
+
display: none;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
:host([recording]) #record .torecord, :host(:not([recording])) #record:not(:hover) .torecord {
|
|
72
|
+
display: none;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
:host(:not([recording])) #record .tostop, :host([recording]) #record:not(:hover) .tostop {
|
|
76
|
+
display: none;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
:host(:not([recording])) #recording-indicator {
|
|
80
|
+
display: none;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#playback {
|
|
84
|
+
width: 100%;
|
|
85
|
+
display: flex;
|
|
86
|
+
flex-direction: row;
|
|
87
|
+
align-items: center;
|
|
88
|
+
border-top: 1px solid var(--sl-color-gray-300);
|
|
89
|
+
border-bottom: 1px solid var(--sl-color-gray-300);
|
|
90
|
+
padding: 0 1ch;
|
|
91
|
+
gap: 1ch;
|
|
92
|
+
color: var(--sl-color-gray-800);
|
|
93
|
+
font-size: var(--sl-font-size-small);
|
|
94
|
+
user-select: none;
|
|
95
|
+
|
|
96
|
+
sl-range {
|
|
97
|
+
flex-grow: 1;
|
|
98
|
+
display: flex;
|
|
99
|
+
flex-direction: row;
|
|
100
|
+
align-items: center;
|
|
101
|
+
|
|
102
|
+
&::part(form-control) {
|
|
103
|
+
width: 100%;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
:host(:not([src])) #playback {
|
|
109
|
+
opacity: 0.5;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
sl-button {
|
|
113
|
+
&::part(label) {
|
|
114
|
+
display: flex;
|
|
115
|
+
flex-direction: row;
|
|
116
|
+
align-items: center;
|
|
117
|
+
gap: 1ch;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
sl-tooltip {
|
|
122
|
+
z-index: 1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#volume {
|
|
126
|
+
--arrow-color: var(--sl-color-gray-600);
|
|
127
|
+
&::part(popup) {
|
|
128
|
+
background: var(--arrow-color);
|
|
129
|
+
height: 30px;
|
|
130
|
+
padding: 1ch;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
`
|
|
134
|
+
|
|
135
|
+
mediaRecorder: MediaRecorder
|
|
136
|
+
chunks = [] as Blob[]
|
|
137
|
+
|
|
138
|
+
@property({type: Boolean, attribute: true})
|
|
139
|
+
accessor loading = false
|
|
140
|
+
|
|
141
|
+
@property({type: String, attribute: true, reflect: true})
|
|
142
|
+
accessor src: string
|
|
143
|
+
|
|
144
|
+
firstUpdated() {
|
|
145
|
+
const evs = ["abort", "canplay", "canplaythrough", "durationchange", "emptied", "ended", "error", "loadeddata", "loadedmetadata", "loadstart", "pause", "play", "playing", "progress", "ratechange", "resize", "seeked", "seeking", "stalled", "suspend", "timeupdate", "volumechange", "waiting"] as const
|
|
146
|
+
evs.forEach(k => this.audioEl.addEventListener(k, ()=>this.requestUpdate()))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
connectedCallback(): void {
|
|
150
|
+
super.connectedCallback()
|
|
151
|
+
this.initializeRecorder()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async initializeRecorder() {
|
|
155
|
+
if(!this.mediaRecorder) {
|
|
156
|
+
this.loading = true
|
|
157
|
+
try {
|
|
158
|
+
const stream = await navigator.mediaDevices.getUserMedia({audio: true})
|
|
159
|
+
this.mediaRecorder = new MediaRecorder(stream)
|
|
160
|
+
this.mediaRecorder.addEventListener("start", e => {
|
|
161
|
+
this.requestUpdate("recording")
|
|
162
|
+
})
|
|
163
|
+
this.mediaRecorder.addEventListener("dataavailable", e => {
|
|
164
|
+
this.chunks.push(e.data)
|
|
165
|
+
})
|
|
166
|
+
this.mediaRecorder.addEventListener("stop", async e => {
|
|
167
|
+
const blob = new Blob(this.chunks, {type: "audio/ogg; codecs=opus"})
|
|
168
|
+
this.chunks = []
|
|
169
|
+
this.src = URL.createObjectURL(blob)
|
|
170
|
+
const reader = new FileReader()
|
|
171
|
+
reader.readAsDataURL(blob)
|
|
172
|
+
reader.addEventListener("load", e => {
|
|
173
|
+
this.src = reader.result as string
|
|
174
|
+
this.requestUpdate("recording")
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
this.loading = false
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
toggleRecording = async () => {
|
|
185
|
+
if(this.mediaRecorder?.state === "recording") {
|
|
186
|
+
this.mediaRecorder.stop()
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
await this.initializeRecorder()
|
|
190
|
+
this.mediaRecorder.start()
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@query("audio")
|
|
195
|
+
accessor audioEl: HTMLAudioElement
|
|
196
|
+
|
|
197
|
+
get duration() {
|
|
198
|
+
const d = this.audioEl?.duration
|
|
199
|
+
return !Number.isNaN(d) && Number.isFinite(d)? d: 0
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
get currentTime() {
|
|
203
|
+
return this.audioEl?.currentTime
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
get playing() {
|
|
207
|
+
return !this.audioEl?.paused && !this.audioEl?.ended
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
get recording() {
|
|
212
|
+
return this.mediaRecorder?.state === "recording"
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@property({type: Boolean, attribute: true, reflect: true})
|
|
216
|
+
set recording(value: boolean) {
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
get volume() {
|
|
221
|
+
return Math.round(this.audioEl?.volume * 100)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
set volume(value: number) {
|
|
225
|
+
this.audioEl.volume = value / 100
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
togglePlay = () => {
|
|
229
|
+
!this.playing? this.audioEl.play(): this.audioEl.pause()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@property({type: Boolean})
|
|
233
|
+
accessor volumeOpen = false
|
|
234
|
+
|
|
235
|
+
render() {
|
|
236
|
+
return html`<sl-button-group>
|
|
237
|
+
<sl-button id="record" @click=${this.toggleRecording} ?loading=${this.loading} ?disabled=${this.loading}>
|
|
238
|
+
<sl-icon class="idle" src=${IconMic}></sl-icon>
|
|
239
|
+
<sl-icon class="torecord" src=${IconMicFill}></sl-icon>
|
|
240
|
+
<sl-icon class="tostop" src=${IconStopFill}></sl-icon>
|
|
241
|
+
<div id="recording-indicator"></div>
|
|
242
|
+
</sl-button>
|
|
243
|
+
<sl-button id="playpause" ?disabled=${this.loading || !this.src} @click=${this.togglePlay}>
|
|
244
|
+
<sl-icon src=${!this.playing? IconPlay: IconPause}></sl-icon>
|
|
245
|
+
</sl-button>
|
|
246
|
+
<div id="playback">
|
|
247
|
+
<audio src=${this.src} preload="metadata"></audio>
|
|
248
|
+
<div>
|
|
249
|
+
<sl-format-date .date=${new Date(this.currentTime * 1000)} minute="numeric" second="numeric"></sl-format-date>
|
|
250
|
+
/
|
|
251
|
+
<sl-format-date .date=${new Date(this.duration * 1000)} minute="numeric" second="numeric"></sl-format-date>
|
|
252
|
+
</div>
|
|
253
|
+
<sl-range ?disabled=${this.loading || !this.src}></sl-range>
|
|
254
|
+
</div>
|
|
255
|
+
<sl-popup id="volume" ?active=${this.volumeOpen} hoist strategy="fixed" arrow distance="6">
|
|
256
|
+
<sl-button ?disabled=${this.loading || !this.src} slot="anchor" @click=${() => this.volumeOpen = !this.volumeOpen}>
|
|
257
|
+
<sl-icon src=${IconVolumeDown}></sl-icon>
|
|
258
|
+
</sl-button>
|
|
259
|
+
<sl-range ?disabled=${this.loading || !this.src} id="volume-range" value=${this.volume} @sl-change=${(e: any) => this.volume = e.target.value}>
|
|
260
|
+
|
|
261
|
+
</sl-range>
|
|
262
|
+
</sl-popup>
|
|
263
|
+
<sl-button ?disabled=${this.loading || !this.src} id="delete" @click=${() => this.src = undefined}>
|
|
264
|
+
<sl-icon src=${IconTrash}></sl-icon>
|
|
265
|
+
</sl-button>
|
|
266
|
+
</sl-button-group>`
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {html, css} from "lit"
|
|
2
|
+
import {LitElementWw} from "@webwriter/lit"
|
|
3
|
+
import {customElement, property} from "lit/decorators.js"
|
|
4
|
+
import "@shoelace-style/shoelace/dist/themes/light.css"
|
|
5
|
+
import { styleMap } from "lit/directives/style-map.js";
|
|
6
|
+
|
|
7
|
+
declare global {interface HTMLElementTagNameMap {
|
|
8
|
+
"webwriter-task-explainer": WebwriterTaskExplainer;
|
|
9
|
+
}}
|
|
10
|
+
|
|
11
|
+
@customElement("webwriter-task-explainer")
|
|
12
|
+
export class WebwriterTaskExplainer extends LitElementWw {
|
|
13
|
+
|
|
14
|
+
@property({type: Boolean, attribute: true, reflect: true})
|
|
15
|
+
accessor active = false
|
|
16
|
+
|
|
17
|
+
static styles = css`
|
|
18
|
+
:host {
|
|
19
|
+
background: var(--sl-color-gray-100);
|
|
20
|
+
border: 2px solid var(--sl-color-gray-200);
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
padding: 10px;
|
|
23
|
+
font-size: 0.8rem;
|
|
24
|
+
height: 100px;
|
|
25
|
+
min-height: 100px;
|
|
26
|
+
overflow-y: auto;
|
|
27
|
+
scrollbar-width: thin;
|
|
28
|
+
resize: vertical;
|
|
29
|
+
display: block;
|
|
30
|
+
}
|
|
31
|
+
`
|
|
32
|
+
|
|
33
|
+
render() {
|
|
34
|
+
return html`<slot>
|
|
35
|
+
|
|
36
|
+
</slot>`
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {html} from "lit"
|
|
2
|
+
import {LitElementWw} from "@webwriter/lit"
|
|
3
|
+
import {customElement} from "lit/decorators.js"
|
|
4
|
+
import "@shoelace-style/shoelace/dist/themes/light.css"
|
|
5
|
+
|
|
6
|
+
declare global {interface HTMLElementTagNameMap {
|
|
7
|
+
"webwriter-task-hint": WebwriterTaskHint;
|
|
8
|
+
}}
|
|
9
|
+
|
|
10
|
+
@customElement("webwriter-task-hint")
|
|
11
|
+
export class WebwriterTaskHint extends LitElementWw {
|
|
12
|
+
render() {
|
|
13
|
+
return html`<slot>
|
|
14
|
+
|
|
15
|
+
</slot>`
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {html} from "lit"
|
|
2
|
+
import {LitElementWw} from "@webwriter/lit"
|
|
3
|
+
import {customElement} from "lit/decorators.js"
|
|
4
|
+
import "@shoelace-style/shoelace/dist/themes/light.css"
|
|
5
|
+
import { styleMap } from "lit/directives/style-map.js"
|
|
6
|
+
|
|
7
|
+
declare global {interface HTMLElementTagNameMap {
|
|
8
|
+
"webwriter-task-prompt": WebwriterTaskPrompt;
|
|
9
|
+
}}
|
|
10
|
+
|
|
11
|
+
@customElement("webwriter-task-prompt")
|
|
12
|
+
export class WebwriterTaskPrompt extends LitElementWw {
|
|
13
|
+
render() {
|
|
14
|
+
return html`<slot style=${styleMap({"--ww-placeholder": `"Prompt"`})}>
|
|
15
|
+
|
|
16
|
+
</slot>`
|
|
17
|
+
}
|
|
18
|
+
}
|