@webwriter/quiz 1.1.5 → 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.
Files changed (165) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +778 -0
  3. package/assets/icons/grip-horizontal.svg +8 -0
  4. package/assets/icons/grip-vertical.svg +8 -0
  5. package/assets/icons/truefalse.svg +6 -0
  6. package/custom-elements.json +2779 -0
  7. package/custom.d.ts +5 -1
  8. package/dist/api/index.d.ts +27 -0
  9. package/dist/api/index.js +40 -0
  10. package/dist/migrate.js +1434 -0
  11. package/dist/widgets/webwriter-choice-item.js +4095 -3818
  12. package/dist/widgets/webwriter-choice.js +2179 -5598
  13. package/dist/widgets/webwriter-gap-item.js +3987 -0
  14. package/dist/widgets/webwriter-gap.js +2908 -0
  15. package/dist/widgets/webwriter-mark.js +4347 -3368
  16. package/dist/widgets/webwriter-order-item.js +4038 -3197
  17. package/dist/widgets/webwriter-order.js +2885 -4200
  18. package/dist/widgets/webwriter-pairing-item.js +1070 -2330
  19. package/dist/widgets/webwriter-pairing.js +2968 -4231
  20. package/dist/widgets/webwriter-quiz-hint.js +2469 -0
  21. package/dist/widgets/webwriter-quiz.js +3337 -4364
  22. package/dist/widgets/webwriter-speech.js +6100 -5120
  23. package/dist/widgets/webwriter-task-prompt.js +4787 -2068
  24. package/dist/widgets/webwriter-task.js +2264 -8157
  25. package/dist/widgets/webwriter-text.js +4075 -4132
  26. package/dist/widgets/webwriter-true-false.js +4674 -0
  27. package/editing-config.json +106 -0
  28. package/lit-localize.json +20 -14
  29. package/localization/de.xlf +395 -65
  30. package/localization/es.xlf +396 -61
  31. package/localization/fr.xlf +396 -61
  32. package/localization/generated/de.ts +97 -15
  33. package/localization/generated/es.ts +97 -15
  34. package/localization/generated/fr.ts +98 -16
  35. package/localization/generated/index.d.ts +5 -0
  36. package/localization/generated/it.ts +96 -14
  37. package/localization/generated/locale-codes.js +0 -48
  38. package/localization/generated/nl.ts +99 -17
  39. package/localization/it.xlf +396 -61
  40. package/localization/nl.xlf +396 -61
  41. package/package.json +120 -254
  42. package/scripts/apply-translations.mjs +97 -0
  43. package/src/api/index.ts +72 -0
  44. package/src/lib/artboard.ts +686 -0
  45. package/src/lib/draggable-grid.ts +432 -0
  46. package/src/lib/encrypted-property.ts +202 -0
  47. package/src/lib/hint-popup.ts +152 -0
  48. package/src/lib/utils.ts +97 -0
  49. package/src/lib/webwriter-quirks.ts +121 -0
  50. package/src/migrate/index.ts +622 -0
  51. package/src/migrate/legacy-solution.ts +362 -0
  52. package/src/snippets/order.html +8 -7
  53. package/src/widgets/choice/webwriter-choice-item.ts +316 -0
  54. package/src/widgets/choice/webwriter-choice.ts +328 -0
  55. package/src/widgets/choice/webwriter-true-false.ts +113 -0
  56. package/src/widgets/dnd/webwriter-dnd-draggable.ts +28 -0
  57. package/src/widgets/dnd/webwriter-dnd-dropzone.ts +9 -0
  58. package/src/widgets/dnd/webwriter-dnd.ts +36 -0
  59. package/src/widgets/gap/webwriter-gap-item.ts +250 -0
  60. package/src/widgets/gap/webwriter-gap.ts +447 -0
  61. package/src/widgets/order/webwriter-order-item.ts +243 -0
  62. package/src/widgets/order/webwriter-order.ts +282 -0
  63. package/src/widgets/pairing/webwriter-pairing-item.ts +46 -0
  64. package/src/widgets/pairing/webwriter-pairing.ts +934 -0
  65. package/src/widgets/speech/audio-player.ts +325 -0
  66. package/src/widgets/speech/audio-recorder.ts +285 -0
  67. package/src/widgets/speech/waveform.ts +113 -0
  68. package/src/widgets/speech/webwriter-speech.ts +87 -0
  69. package/src/widgets/webwriter-mark.ts +366 -424
  70. package/src/widgets/webwriter-quiz-hint.ts +36 -0
  71. package/src/widgets/webwriter-quiz.ts +318 -280
  72. package/src/widgets/webwriter-task-prompt.ts +43 -18
  73. package/src/widgets/webwriter-task.ts +212 -551
  74. package/src/widgets/webwriter-text.ts +286 -172
  75. package/tsconfig.api.json +12 -0
  76. package/tsconfig.json +7 -6
  77. package/dist/widgets/webwriter-choice.css +0 -444
  78. package/dist/widgets/webwriter-cloze-gap.css +0 -444
  79. package/dist/widgets/webwriter-cloze-gap.js +0 -5727
  80. package/dist/widgets/webwriter-cloze.css +0 -444
  81. package/dist/widgets/webwriter-cloze.js +0 -3653
  82. package/dist/widgets/webwriter-mark.css +0 -444
  83. package/dist/widgets/webwriter-order.css +0 -444
  84. package/dist/widgets/webwriter-pairing-item.css +0 -444
  85. package/dist/widgets/webwriter-pairing.css +0 -444
  86. package/dist/widgets/webwriter-quiz.css +0 -444
  87. package/dist/widgets/webwriter-task-explainer.css +0 -444
  88. package/dist/widgets/webwriter-task-explainer.js +0 -2769
  89. package/dist/widgets/webwriter-task-hint.css +0 -444
  90. package/dist/widgets/webwriter-task-hint.js +0 -2708
  91. package/dist/widgets/webwriter-task.css +0 -444
  92. package/dist/widgets/webwriter-text.css +0 -444
  93. package/localization/bg.xlf +0 -72
  94. package/localization/cs.xlf +0 -72
  95. package/localization/da.xlf +0 -72
  96. package/localization/el.xlf +0 -72
  97. package/localization/et.xlf +0 -72
  98. package/localization/fi.xlf +0 -72
  99. package/localization/generated/bg.ts +0 -31
  100. package/localization/generated/cs.ts +0 -31
  101. package/localization/generated/da.ts +0 -31
  102. package/localization/generated/el.ts +0 -31
  103. package/localization/generated/et.ts +0 -31
  104. package/localization/generated/fi.ts +0 -31
  105. package/localization/generated/hu.ts +0 -31
  106. package/localization/generated/id.ts +0 -31
  107. package/localization/generated/ja.ts +0 -31
  108. package/localization/generated/ko.ts +0 -31
  109. package/localization/generated/lt.ts +0 -31
  110. package/localization/generated/lv.ts +0 -31
  111. package/localization/generated/nb.ts +0 -31
  112. package/localization/generated/pl.ts +0 -31
  113. package/localization/generated/pt-BR.ts +0 -31
  114. package/localization/generated/pt-PT.ts +0 -31
  115. package/localization/generated/ro.ts +0 -31
  116. package/localization/generated/ru.ts +0 -31
  117. package/localization/generated/sk.ts +0 -31
  118. package/localization/generated/sl.ts +0 -31
  119. package/localization/generated/sv.ts +0 -31
  120. package/localization/generated/tr.ts +0 -31
  121. package/localization/generated/uk.ts +0 -31
  122. package/localization/generated/zh-hans.ts +0 -31
  123. package/localization/hu.xlf +0 -72
  124. package/localization/id.xlf +0 -72
  125. package/localization/ja.xlf +0 -72
  126. package/localization/ko.xlf +0 -72
  127. package/localization/lt.xlf +0 -72
  128. package/localization/lv.xlf +0 -72
  129. package/localization/nb.xlf +0 -72
  130. package/localization/pl.xlf +0 -72
  131. package/localization/pt-BR.xlf +0 -72
  132. package/localization/pt-PT.xlf +0 -72
  133. package/localization/ro.xlf +0 -72
  134. package/localization/ru.xlf +0 -72
  135. package/localization/sk.xlf +0 -72
  136. package/localization/sl.xlf +0 -72
  137. package/localization/sv.xlf +0 -72
  138. package/localization/tr.xlf +0 -72
  139. package/localization/uk.xlf +0 -72
  140. package/localization/zh-hans.xlf +0 -72
  141. package/src/lib/combobox.ts +0 -456
  142. package/src/lib/highlighter-fill.svg +0 -6
  143. package/src/snippets/Beispiel-Choice.html +0 -57
  144. package/src/snippets/Beispiel-Mark.html +0 -14
  145. package/src/snippets/Beispiel-Order.html +0 -52
  146. package/src/snippets/Beispiel-Text.html +0 -20
  147. package/src/snippets/blank.html +0 -1
  148. package/src/snippets/choice.html +0 -8
  149. package/src/snippets/cloze.html +0 -4
  150. package/src/snippets/mark.html +0 -4
  151. package/src/snippets/pairing.html +0 -10
  152. package/src/snippets/speech.html +0 -4
  153. package/src/snippets/text.html +0 -4
  154. package/src/snippets/wordsearch.html +0 -4
  155. package/src/widgets/webwriter-choice-item.ts +0 -257
  156. package/src/widgets/webwriter-choice.ts +0 -353
  157. package/src/widgets/webwriter-cloze-gap.ts +0 -216
  158. package/src/widgets/webwriter-cloze.ts +0 -136
  159. package/src/widgets/webwriter-order-item.ts +0 -441
  160. package/src/widgets/webwriter-order.ts +0 -327
  161. package/src/widgets/webwriter-pairing-item.ts +0 -161
  162. package/src/widgets/webwriter-pairing.ts +0 -193
  163. package/src/widgets/webwriter-speech.ts +0 -273
  164. package/src/widgets/webwriter-task-explainer.ts +0 -43
  165. package/src/widgets/webwriter-task-hint.ts +0 -23
@@ -1,177 +1,291 @@
1
- import {html, css, PropertyValues, TemplateResult} from "lit"
2
- import {LitElementWw, option} from "@webwriter/lit"
3
- import {customElement, property, query} from "lit/decorators.js"
4
- import {ifDefined} from "lit/directives/if-defined.js"
1
+ import { msg } from "@lit/localize";
2
+ import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js";
3
+ import SlTextarea from "@shoelace-style/shoelace/dist/components/textarea/textarea.component.js";
4
+ import { LitElementWw, type OptionDeclaration } from "@webwriter/lit";
5
+ import TextParagraphIcon from "bootstrap-icons/icons/text-paragraph.svg";
6
+ import { css, html, nothing, type PropertyValues } from "lit";
7
+ import { customElement, property, query, state } from "lit/decorators.js";
8
+ import LOCALIZE from "../../localization/generated";
9
+ import { name as packageId } from "../../package.json";
10
+ import { registerQuizType, type IWebWriterQuizType } from "../api";
11
+ import { encryptedProperty, encryptPlaintextAttributes } from "../lib/encrypted-property";
5
12
 
6
- import SlTextarea from "@shoelace-style/shoelace/dist/components/textarea/textarea.component.js"
7
- import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js"
8
- import "@shoelace-style/shoelace/dist/themes/light.css"
13
+ declare global {
14
+ interface HTMLElementTagNameMap {
15
+ "webwriter-text": WebwriterText;
16
+ }
17
+ }
9
18
 
10
- import LOCALIZE from "../../localization/generated"
11
- import {msg} from "@lit/localize"
19
+ export type TextType = "long-text" | "text" | "number" | "date" | "time" | "datetime-local";
12
20
 
13
- declare global {interface HTMLElementTagNameMap {
14
- "webwriter-text": WebwriterText;
15
- }}
21
+ registerQuizType({
22
+ packageId,
23
+ widgetPosition: 30,
24
+ id: "webwriter-text",
25
+ getName: () => msg("Text"),
26
+ icon: TextParagraphIcon,
27
+ createInstance: () => document.createElement("webwriter-text"),
28
+ });
16
29
 
30
+ /**
31
+ * An answer where learners type a value into an input field.
32
+ *
33
+ * It must be assigned to the default slot of a `<webwriter-task>`.
34
+ */
17
35
  @customElement("webwriter-text")
18
- export class WebwriterText extends LitElementWw {
19
-
20
- localize = LOCALIZE
21
-
22
- @property({type: String, attribute: true, reflect: true})
23
- @option({
24
- type: "select",
25
- label: {"en": "Type", "de": "Typ"},
26
- options: [
27
- {value: "long-text", label: {"en": "Long Text", "de": "Langer Text"}},
28
- {value: "text", label: {"en": "Short Text", "de": "Kurzer Text"}},
29
- {value: "number", label: {"en": "Number", "de": "Zahl"}},
30
- {value: "date", label: {"en": "Date", "de": "Datum"}},
31
- {value: "time", label: {"en": "Time", "de": "Uhrzeit"}},
32
- {value: "datetime-local", label: {"en": "Date & Time", "de": "Datum & Uhrzeit"}}
33
- ]
34
- })
35
- accessor type: "date" | "datetime-local" | "number" | "text" | "time" | "long-text" = "long-text"
36
-
37
- @property({type: String, attribute: true, reflect: true})
38
- @option()
39
- accessor placeholder: string
40
-
41
- @property({type: Boolean, attribute: true, reflect: true})
42
- @option({
43
- type: Boolean,
44
- label: {"en": "free answer (ignores the other options)", "de": "Freitext (ignoriert andere Optionen)"},
45
- })
46
- accessor freeText = false
47
-
48
- @property({type: Boolean, attribute: true, reflect: true})
49
- @option({
50
- type: Boolean,
51
- label: {"en": "Ignore capitalization", "de": "Großschreibung ignorieren"},
52
- })
53
- accessor ignoreCase = false
54
-
55
- @property({type: Boolean, attribute: true, reflect: true})
56
- @option({
57
- type: Boolean,
58
- label: {"en": "show Solution", "de": "Lösung anzeigen"},
59
- })
60
- accessor showSolution = false
61
-
62
- @property({type: String, attribute: true, reflect: true})
63
- @option({
64
- label: {"en": "Message for wrong solution", "de": "Nachricht bei falscher Lösung"},
65
- })
66
- accessor wrongMessage: string
67
-
68
- @property({type: String, attribute: true, reflect: true})
69
- accessor value: string
70
-
71
- /*
72
- @property({type: Number, attribute: true, reflect: true})
73
- @option({type: Number})
74
- min: number
75
-
76
- @property({type: Number, attribute: true, reflect: true})
77
- @option({type: Number})
78
- max: number
79
-
80
- @property({type: Number, attribute: true, reflect: true})
81
- @option({type: Number})
82
- step: number
83
- */
84
-
85
- static scopedElements = {
86
- "sl-textarea": SlTextarea,
87
- "sl-input": SlInput
88
- }
89
-
90
- static styles = css`
91
- :host(:is([contenteditable=true], [contenteditable=""])) sl-textarea::part(textarea) {
92
- color: var(--sl-color-success-700);
93
- }
94
-
95
- sl-textarea {
96
- resize: vertical;
97
- overflow: hidden;
98
- min-height: 40px;
99
-
100
- &::part(form-control), &::part(form-control-input), &::part(base), &::part(textarea) {
101
- height: 100%;
102
- }
103
- }
104
-
105
- :is(sl-textarea, sl-input)[data-correct]::part(base) {
106
- background: var(--sl-color-success-200);
107
- }
108
-
109
- #solution {
110
- padding: 1rem;
111
- }
112
-
113
- #solution[data-correct] {
114
- background-color: var(--sl-color-success-200);
115
- }
116
-
117
- #solution:not([data-correct]) {
118
- background-color: var(--sl-color-danger-200);
119
- }
120
- `
121
-
122
- handleChange = (e: CustomEvent) => {
123
- const target = e.target as SlTextarea | SlInput
124
- if(this.isContentEditable) {
125
- this.solution = target.value?.trim()
126
- }
127
- else {
128
- this.value = target.value?.trim()
129
- }
130
- this.dispatchEvent(new CustomEvent("ww-answer-change", {
131
- detail: {value: target.value},
132
- bubbles: true,
133
- composed: true
134
- }))
135
- }
136
-
137
- @query("sl-textarea, sl-input")
138
- accessor input: SlTextarea | SlInput
139
-
140
- focus() {
141
- this.input.focus()
142
- }
143
-
144
- reset() {
145
- this.solution = undefined
146
- if(!this.freeText){
147
- this.input.value = ""
148
- }
149
-
150
-
151
- let inputElem: SlTextarea | SlInput = this.shadowRoot.getElementById("inputElem") as SlTextarea | SlInput
152
- inputElem.disabled = false
153
- }
154
-
155
- reportSolution() {
156
- let inputElem: SlTextarea | SlInput = this.shadowRoot.getElementById("inputElem") as SlTextarea | SlInput
157
- inputElem.disabled = true
158
- }
159
-
160
- @property({type: String, attribute: false, reflect: false})
161
- accessor solution: string
162
-
163
- render() {
164
- const correct = this.freeText ? this.value?.trim() != "" : !this.ignoreCase ? this.solution && this.value?.trim() ===
165
- this.solution : this.solution && this.value?.trim().toLowerCase() === this.solution.toLowerCase()
166
- if(this.freeText && correct){
167
- this.solution = this.value
168
- }
169
- const textarea = html`<sl-textarea id="inputElem" ?data-correct=${correct && !(this.freeText)} value=${this.isContentEditable? this.solution: this.value} placeholder=${this.placeholder} resize="none" @sl-change=${this.handleChange}></sl-textarea>`
170
- const input = html`<sl-input id="inputElem" ?data-correct=${correct} value=${this.isContentEditable? this.solution: this.value}
171
- placeholder=${this.placeholder} type=${this.type} @sl-change=${this.handleChange}></sl-input>`
172
- return html`
173
- ${this.type === "long-text"? textarea: input}
174
- ${this.solution && !this.isContentEditable && !correct? html`<div id="solution" ?data-correct=${correct}>${this.showSolution?this.solution:html`<i>${this.wrongMessage}</i>`}</div>`: undefined}
175
- `
176
- }
177
- }
36
+ export class WebwriterText extends LitElementWw implements IWebWriterQuizType {
37
+ /** @internal */
38
+ localize = LOCALIZE;
39
+
40
+ /** @internal */
41
+ static scopedElements = {
42
+ "sl-textarea": SlTextarea,
43
+ "sl-input": SlInput,
44
+ };
45
+
46
+ static styles = css`
47
+ :host {
48
+ display: block;
49
+ width: 100%;
50
+ }
51
+
52
+ /* Highlight the author's solution while editing. */
53
+ :host(:is([contenteditable="true"], [contenteditable=""])) :is(sl-textarea::part(textarea), sl-input::part(input)) {
54
+ color: var(--sl-color-success-700);
55
+ }
56
+
57
+ sl-textarea::part(base),
58
+ sl-input::part(base) {
59
+ font-size: 1em !important;
60
+ }
61
+
62
+ sl-textarea {
63
+ resize: vertical;
64
+ overflow: hidden;
65
+ min-height: 40px;
66
+
67
+ &::part(form-control),
68
+ &::part(form-control-input),
69
+ &::part(base),
70
+ &::part(textarea) {
71
+ height: 100%;
72
+ }
73
+ }
74
+
75
+ :is(sl-textarea, sl-input):disabled::part(base) {
76
+ background-color: var(--sl-input-background-color);
77
+ border-color: var(--sl-input-border-color);
78
+ opacity: 1;
79
+ cursor: inherit;
80
+ }
81
+
82
+ :is(sl-textarea, sl-input)[data-state="correct"]::part(base) {
83
+ background: var(--sl-color-success-50);
84
+ border-color: var(--sl-color-success-400);
85
+ }
86
+
87
+ :is(sl-textarea, sl-input)[data-state="incorrect"]::part(base) {
88
+ background: var(--sl-color-danger-50);
89
+ border-color: var(--sl-color-danger-400);
90
+ }
91
+
92
+ .feedback {
93
+ margin-top: var(--sl-spacing-x-small);
94
+ color: var(--sl-color-neutral-500);
95
+ font-size: var(--sl-font-size-small);
96
+ overflow-wrap: anywhere;
97
+ }
98
+
99
+ .solution {
100
+ margin-top: var(--sl-spacing-x-small);
101
+ }
102
+ `;
103
+
104
+ /**
105
+ * The kind of input field learners answer in.
106
+ *
107
+ * - `long-text`: A multi-line text area.
108
+ * - `text`: A single-line text field.
109
+ * - `number`: A number field.
110
+ * - `date`: A date picker.
111
+ * - `time`: A time picker.
112
+ * - `datetime-local`: A combined date and time picker.
113
+ */
114
+ @property({ type: String, attribute: true, reflect: true })
115
+ accessor type: TextType = "long-text";
116
+
117
+ /**
118
+ * The placeholder shown while the field is empty.
119
+ */
120
+ @property({ type: String, attribute: true, reflect: true })
121
+ accessor placeholder: string = "";
122
+
123
+ /**
124
+ * Whether any non-empty answer counts as correct, without comparing it to the solution.
125
+ */
126
+ @property({ type: Boolean, attribute: "free-text", reflect: true })
127
+ accessor freeText = false;
128
+
129
+ /**
130
+ * Whether the answer is compared to the solution case-insensitively.
131
+ */
132
+ @property({ type: Boolean, attribute: "ignore-case", reflect: true })
133
+ accessor ignoreCase = false;
134
+
135
+ /**
136
+ * The feedback shown after submitting if the answer is correct.
137
+ */
138
+ @property({ type: String, attribute: "correct-message", reflect: true })
139
+ accessor correctMessage: string = "";
140
+
141
+ /**
142
+ * The feedback shown after submitting if the answer is wrong.
143
+ */
144
+ @property({ type: String, attribute: "wrong-message", reflect: true })
145
+ accessor wrongMessage: string = "";
146
+
147
+ /** @internal */
148
+ get dynamicOptions(): Record<string, OptionDeclaration> {
149
+ const isText = this.type === "long-text" || this.type === "text";
150
+ const options: Record<string, OptionDeclaration> = {
151
+ type: {
152
+ type: "select",
153
+ label: { _: msg("Type") },
154
+ options: [
155
+ { value: "long-text", label: { _: msg("Long Text") } },
156
+ { value: "text", label: { _: msg("Short Text") } },
157
+ { value: "number", label: { _: msg("Number") } },
158
+ { value: "date", label: { _: msg("Date") } },
159
+ { value: "time", label: { _: msg("Time") } },
160
+ { value: "datetime-local", label: { _: msg("Date & Time") } },
161
+ ],
162
+ },
163
+ };
164
+
165
+ if (isText || this.type === "number") {
166
+ options.placeholder = { type: "string", label: { _: msg("Placeholder") } };
167
+ }
168
+
169
+ options["free-text"] = { type: "boolean", label: { _: msg("Free answer") } };
170
+
171
+ if (!this.freeText) {
172
+ if (isText) {
173
+ options["ignore-case"] = {
174
+ type: "boolean",
175
+ label: { _: msg("Ignore capitalization") },
176
+ };
177
+ }
178
+ options["correct-message"] = {
179
+ type: "string",
180
+ label: { _: msg("Message for correct solution") },
181
+ };
182
+ options["wrong-message"] = {
183
+ type: "string",
184
+ label: { _: msg("Message for wrong solution") },
185
+ };
186
+ }
187
+
188
+ return options;
189
+ }
190
+
191
+ /**
192
+ * The value counting as correct. It is obfuscated in the markup so that learners cannot read it directly.
193
+ */
194
+ @property({ type: String, attribute: true, reflect: true, converter: encryptedProperty })
195
+ accessor solution: string = "";
196
+
197
+ @state() private accessor currentAnswer: string | undefined;
198
+ @state() private accessor graded: boolean = false;
199
+ @state() private accessor detailedFeedback: boolean = false;
200
+
201
+ @query("sl-textarea, sl-input")
202
+ private accessor input!: SlTextarea | SlInput;
203
+
204
+ protected firstUpdated(changed: PropertyValues): void {
205
+ super.firstUpdated(changed);
206
+ encryptPlaintextAttributes(this);
207
+ }
208
+
209
+ private get value(): string {
210
+ return this.isContentEditable ? this.solution : (this.currentAnswer ?? "");
211
+ }
212
+
213
+ private set value(value: string) {
214
+ if (this.isContentEditable) this.solution = value;
215
+ else this.currentAnswer = value;
216
+ }
217
+
218
+ private isCorrect(): boolean {
219
+ const answer = (this.currentAnswer ?? "").trim();
220
+ if (this.freeText) return answer !== "";
221
+ const solution = this.solution.trim();
222
+ if (!solution) return false;
223
+ return this.ignoreCase ? answer.toLowerCase() === solution.toLowerCase() : answer === solution;
224
+ }
225
+
226
+ override focus() {
227
+ this.input?.focus();
228
+ }
229
+
230
+ checkValidity(): boolean {
231
+ return (this.currentAnswer ?? "").trim() !== "";
232
+ }
233
+
234
+ reset() {
235
+ this.graded = false;
236
+ this.detailedFeedback = false;
237
+ this.currentAnswer = undefined;
238
+ }
239
+
240
+ checkAnswer(detailedFeedback: boolean): number {
241
+ this.graded = true;
242
+ this.detailedFeedback = detailedFeedback;
243
+ return this.isCorrect() ? 1 : 0;
244
+ }
245
+
246
+ private handleChange = (e: Event) => {
247
+ const target = e.target as SlTextarea | SlInput;
248
+ this.value = target.value?.trim() ?? "";
249
+ };
250
+
251
+ private get feedbackState(): "correct" | "incorrect" | undefined {
252
+ // Free answers are locked on submit but never marked correct/incorrect.
253
+ if (this.freeText || this.isContentEditable || !this.graded || !this.detailedFeedback) return undefined;
254
+ return this.isCorrect() ? "correct" : "incorrect";
255
+ }
256
+
257
+ render() {
258
+ const value = this.value;
259
+ const state = this.feedbackState;
260
+ const message = state === "correct" ? this.correctMessage : this.wrongMessage;
261
+ const showSolution = this.detailedFeedback && this.solution.trim() !== "";
262
+
263
+ const field =
264
+ this.type === "long-text"
265
+ ? html`<sl-textarea
266
+ data-state=${state ?? nothing}
267
+ value=${value}
268
+ placeholder=${this.placeholder}
269
+ resize="none"
270
+ ?disabled=${this.graded}
271
+ @sl-change=${this.handleChange}
272
+ ></sl-textarea>`
273
+ : html`<sl-input
274
+ data-state=${state ?? nothing}
275
+ value=${value}
276
+ placeholder=${this.placeholder}
277
+ type=${this.type}
278
+ ?disabled=${this.graded}
279
+ @sl-change=${this.handleChange}
280
+ ></sl-input>`;
281
+
282
+ return html`${field}${state && (message || showSolution)
283
+ ? html`<div class="feedback" data-state=${state} role="status">
284
+ ${message ? html`<div>${message}</div>` : nothing}
285
+ ${showSolution
286
+ ? html`<div class="solution"><strong>${msg("Solution")}:</strong> ${this.solution}</div>`
287
+ : nothing}
288
+ </div>`
289
+ : nothing}`;
290
+ }
291
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "declaration": true,
5
+ "emitDeclarationOnly": false,
6
+ "outDir": "./dist/api",
7
+ "rootDir": "./src/api",
8
+ "skipLibCheck": true,
9
+ "sourceMap": false
10
+ },
11
+ "include": ["src/api/index.ts"]
12
+ }
package/tsconfig.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
- "compilerOptions": {
3
- "target": "es2022",
4
- "moduleResolution": "Bundler",
5
- "module": "ES2022"
6
- }
7
- }
2
+ "compilerOptions": {
3
+ "target": "es2022",
4
+ "moduleResolution": "Bundler",
5
+ "module": "ES2022",
6
+ "lib": ["ESNext", "DOM", "DOM.Iterable"]
7
+ }
8
+ }