@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,435 @@
|
|
|
1
|
+
import {html, css, RenderOptions} from "lit"
|
|
2
|
+
import {action, LitElementWw} from "@webwriter/lit"
|
|
3
|
+
import {customElement, eventOptions, property} from "lit/decorators.js"
|
|
4
|
+
import {styleMap} from "lit/directives/style-map.js"
|
|
5
|
+
import "@shoelace-style/shoelace/dist/themes/light.css"
|
|
6
|
+
|
|
7
|
+
import SlIconButton from "@shoelace-style/shoelace/dist/components/icon-button/icon-button.component.js"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {!Node} node
|
|
11
|
+
* @param {boolean=} optimized
|
|
12
|
+
* @return {string}
|
|
13
|
+
*/
|
|
14
|
+
export const xPath = function (node, optimized=false) {
|
|
15
|
+
if (node.nodeType === Node.DOCUMENT_NODE) {
|
|
16
|
+
return '/';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const steps = [];
|
|
20
|
+
let contextNode = node;
|
|
21
|
+
while (contextNode) {
|
|
22
|
+
const step = _xPathValue(contextNode, optimized);
|
|
23
|
+
if (!step) {
|
|
24
|
+
break;
|
|
25
|
+
} // Error - bail out early.
|
|
26
|
+
steps.push(step);
|
|
27
|
+
if (step.optimized) {
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
contextNode = contextNode.parentNode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
steps.reverse();
|
|
34
|
+
return (steps.length && steps[0].optimized ? '' : '/') + steps.join('/');
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {!Node} node
|
|
39
|
+
* @param {boolean=} optimized
|
|
40
|
+
* @return {?Step}
|
|
41
|
+
*/
|
|
42
|
+
const _xPathValue = function (node, optimized) {
|
|
43
|
+
let ownValue;
|
|
44
|
+
const ownIndex = _xPathIndex(node);
|
|
45
|
+
if (ownIndex === -1) {
|
|
46
|
+
return null;
|
|
47
|
+
} // Error.
|
|
48
|
+
|
|
49
|
+
switch (node.nodeType) {
|
|
50
|
+
case Node.ELEMENT_NODE:
|
|
51
|
+
if (optimized && node.getAttribute('id')) {
|
|
52
|
+
return new Step('//*[@id="' + node.getAttribute('id') + '"]', true);
|
|
53
|
+
}
|
|
54
|
+
ownValue = node.localName;
|
|
55
|
+
break;
|
|
56
|
+
case Node.ATTRIBUTE_NODE:
|
|
57
|
+
ownValue = '@' + node.nodeName;
|
|
58
|
+
break;
|
|
59
|
+
case Node.TEXT_NODE:
|
|
60
|
+
case Node.CDATA_SECTION_NODE:
|
|
61
|
+
ownValue = 'text()';
|
|
62
|
+
break;
|
|
63
|
+
case Node.PROCESSING_INSTRUCTION_NODE:
|
|
64
|
+
ownValue = 'processing-instruction()';
|
|
65
|
+
break;
|
|
66
|
+
case Node.COMMENT_NODE:
|
|
67
|
+
ownValue = 'comment()';
|
|
68
|
+
break;
|
|
69
|
+
case Node.DOCUMENT_NODE:
|
|
70
|
+
ownValue = '';
|
|
71
|
+
break;
|
|
72
|
+
default:
|
|
73
|
+
ownValue = '';
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (ownIndex > 0) {
|
|
78
|
+
ownValue += '[' + ownIndex + ']';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return new Step(ownValue, node.nodeType === Node.DOCUMENT_NODE);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {!Node} node
|
|
86
|
+
* @return {number}
|
|
87
|
+
*/
|
|
88
|
+
const _xPathIndex = function (node) {
|
|
89
|
+
// Returns -1 in case of error, 0 if no siblings matching the same expression,
|
|
90
|
+
// <XPath index among the same expression-matching sibling nodes> otherwise.
|
|
91
|
+
function areNodesSimilar(left, right) {
|
|
92
|
+
if (left === right) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (left.nodeType === Node.ELEMENT_NODE && right.nodeType === Node.ELEMENT_NODE) {
|
|
97
|
+
return left.localName === right.localName;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (left.nodeType === right.nodeType) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// XPath treats CDATA as text nodes.
|
|
105
|
+
const leftType = left.nodeType === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : left.nodeType;
|
|
106
|
+
const rightType = right.nodeType === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : right.nodeType;
|
|
107
|
+
return leftType === rightType;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const siblings = node.parentNode ? node.parentNode.children : null;
|
|
111
|
+
if (!siblings) {
|
|
112
|
+
return 0;
|
|
113
|
+
} // Root node - no siblings.
|
|
114
|
+
let hasSameNamedElements;
|
|
115
|
+
for (let i = 0; i < siblings.length; ++i) {
|
|
116
|
+
if (areNodesSimilar(node, siblings[i]) && siblings[i] !== node) {
|
|
117
|
+
hasSameNamedElements = true;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!hasSameNamedElements) {
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
let ownIndex = 1; // XPath indices start with 1.
|
|
125
|
+
for (let i = 0; i < siblings.length; ++i) {
|
|
126
|
+
if (areNodesSimilar(node, siblings[i])) {
|
|
127
|
+
if (siblings[i] === node) {
|
|
128
|
+
return ownIndex;
|
|
129
|
+
}
|
|
130
|
+
++ownIndex;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return -1; // An error occurred: |node| not found in parent's children.
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @unrestricted
|
|
138
|
+
*/
|
|
139
|
+
const Step = class {
|
|
140
|
+
constructor(readonly value: string, readonly optimized=false) {}
|
|
141
|
+
|
|
142
|
+
toString() {
|
|
143
|
+
return this.value;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
import IconHighlighter from "bootstrap-icons/icons/highlighter.svg"
|
|
148
|
+
import IconHighlighterFill from "../lib/highlighter-fill.svg"
|
|
149
|
+
|
|
150
|
+
function getCaretPositionFromPoint(e: PointerEvent) {
|
|
151
|
+
let range: Range | null;
|
|
152
|
+
let textNode: Text;
|
|
153
|
+
let offset: number;
|
|
154
|
+
|
|
155
|
+
if ((document as any).caretPositionFromPoint) {
|
|
156
|
+
range = (document as any).caretPositionFromPoint(e.clientX, e.clientY);
|
|
157
|
+
textNode = (range as any).offsetNode;
|
|
158
|
+
offset = (range as any).offset;
|
|
159
|
+
return {textNode, offset}
|
|
160
|
+
} else if (document.caretRangeFromPoint) {
|
|
161
|
+
// Use WebKit-proprietary fallback method
|
|
162
|
+
range = document.caretRangeFromPoint(e.clientX, e.clientY);
|
|
163
|
+
textNode = range.startContainer as Text;
|
|
164
|
+
offset = range.startOffset;
|
|
165
|
+
return {textNode, offset}
|
|
166
|
+
} else {
|
|
167
|
+
throw Error("Both 'caretPositionFromPoint' and 'caretRangeFromPoint' are unsupported")
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const toAttributeRange = (ranges: SerializableRange[]) => {
|
|
172
|
+
return JSON.stringify(ranges)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const fromAttributeRange = (attr?: string) => {
|
|
176
|
+
if(!attr) {
|
|
177
|
+
return []
|
|
178
|
+
}
|
|
179
|
+
const ranges = JSON.parse(attr) as {startContainer: string, startOffset: number, endContainer?: string, endOffset: number}[]
|
|
180
|
+
return ranges.map(range => new SerializableRange(range))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
type SerializableRangeLike = SerializableRange | {startContainer: string, startOffset: number, endContainer?: string, endOffset: number}
|
|
184
|
+
|
|
185
|
+
class SerializableRange extends Range {
|
|
186
|
+
|
|
187
|
+
constructor(value?: string | SerializableRangeLike) {
|
|
188
|
+
super()
|
|
189
|
+
if(value instanceof SerializableRange) {
|
|
190
|
+
return value
|
|
191
|
+
}
|
|
192
|
+
else if(value) {
|
|
193
|
+
const {startContainer, startOffset, endContainer, endOffset} = typeof value === "string"? JSON.parse(value) as {startContainer: string, startOffset: number, endContainer?: string, endOffset: number}: value
|
|
194
|
+
const startNode = document.evaluate(startContainer, document, null, 9, null).singleNodeValue
|
|
195
|
+
startNode && this.setStart(startNode, startOffset)
|
|
196
|
+
const endNode = document.evaluate(endContainer ?? startContainer, document, null, 9, null).singleNodeValue
|
|
197
|
+
endNode && this.setEnd(endNode, endOffset)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
toJSON() {
|
|
202
|
+
return this.startContainer === this.endContainer
|
|
203
|
+
? {
|
|
204
|
+
startContainer: xPath(this.startContainer),
|
|
205
|
+
startOffset: this.startOffset,
|
|
206
|
+
endOffset: this.endOffset
|
|
207
|
+
}
|
|
208
|
+
: {
|
|
209
|
+
startContainer: xPath(this.startContainer),
|
|
210
|
+
startOffset: this.startOffset,
|
|
211
|
+
endContainer: xPath(this.endContainer),
|
|
212
|
+
endOffset: this.endOffset
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
toString() {
|
|
217
|
+
return JSON.stringify(this.toJSON())
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
declare global {interface HTMLElementTagNameMap {
|
|
222
|
+
"webwriter-mark": WebwriterMark;
|
|
223
|
+
}}
|
|
224
|
+
|
|
225
|
+
@customElement("webwriter-mark")
|
|
226
|
+
export class WebwriterMark extends LitElementWw {
|
|
227
|
+
|
|
228
|
+
static shadowRootOptions = {...LitElementWw.shadowRootOptions, delegatesFocus: false}
|
|
229
|
+
|
|
230
|
+
static localization = {}
|
|
231
|
+
|
|
232
|
+
// @ts-ignore: Experimental API
|
|
233
|
+
static highlightValue = new Highlight()
|
|
234
|
+
|
|
235
|
+
// @ts-ignore: Experimental API
|
|
236
|
+
static highlightSolution = new Highlight()
|
|
237
|
+
|
|
238
|
+
static {
|
|
239
|
+
// @ts-ignore: Experimental API
|
|
240
|
+
CSS.highlights.set("webwriter-mark-solution", this.highlightSolution)
|
|
241
|
+
// @ts-ignore: Experimental API
|
|
242
|
+
CSS.highlights.set("webwriter-mark-value", this.highlightValue)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
msg = (str: string) => this.lang in WebwriterMark.localization? WebwriterMark.localization[this.lang][str] ?? str: str
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
static scopedElements = {
|
|
249
|
+
"sl-icon-button": SlIconButton
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
static styles = css`
|
|
253
|
+
:host {
|
|
254
|
+
min-height: 1rem;
|
|
255
|
+
position: relative;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
slot {
|
|
259
|
+
display: block;
|
|
260
|
+
cursor: text;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
:host(:not([contenteditable=true]):not([contenteditable=""])) slot {
|
|
264
|
+
cursor: pointer;
|
|
265
|
+
user-select: none;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
#highlight {
|
|
269
|
+
position: absolute;
|
|
270
|
+
right: 0;
|
|
271
|
+
top: 0;
|
|
272
|
+
background: rgba(255, 255, 255, 0.85)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
slot[data-empty]:after {
|
|
276
|
+
content: var(--ww-placeholder);
|
|
277
|
+
position: absolute;
|
|
278
|
+
left: 0;
|
|
279
|
+
top: 0;
|
|
280
|
+
color: darkgray;
|
|
281
|
+
pointer-events: none;
|
|
282
|
+
user-select: none;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#highlight::part(base):hover {
|
|
286
|
+
background: yellow;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
:host ::highlight(webwriter-mark-value) {
|
|
290
|
+
background-color: yellow;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
:host ::highlight(webwriter-mark-solution) {
|
|
294
|
+
background-color: greenyellow;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
#highlight[data-highlighting]::part(base) {
|
|
298
|
+
background-color: lightyellow;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
:host(:has(#highlight[data-highlighting])) ::selection {
|
|
302
|
+
background: lightyellow !important;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
:host([highlighting]) #highlight::part(base) {
|
|
306
|
+
background: yellow;
|
|
307
|
+
}
|
|
308
|
+
`
|
|
309
|
+
|
|
310
|
+
@property({type: Boolean, attribute: true, reflect: true})
|
|
311
|
+
accessor highlighting = false
|
|
312
|
+
|
|
313
|
+
#solution: SerializableRange[] = []
|
|
314
|
+
|
|
315
|
+
get solution(): SerializableRange[] {
|
|
316
|
+
return this.#solution
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
@property({attribute: false})
|
|
320
|
+
set solution(value: SerializableRangeLike[]) {
|
|
321
|
+
this.#updateHighlight("solution", value.map(v => new SerializableRange(v)))
|
|
322
|
+
this.requestUpdate("solution")
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#value: SerializableRange[] = []
|
|
326
|
+
|
|
327
|
+
get value(): SerializableRange[] {
|
|
328
|
+
return this.#value
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
@property({attribute: true, reflect: true, converter: {toAttribute: toAttributeRange, fromAttribute: fromAttributeRange}})
|
|
332
|
+
set value(value: SerializableRangeLike[]) {
|
|
333
|
+
this.#updateHighlight("value", value.map(v => new SerializableRange(v)))
|
|
334
|
+
this.requestUpdate("value")
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
#updateHighlight(key: "value" | "solution", value: SerializableRange[]) {
|
|
338
|
+
const prev = this[key]
|
|
339
|
+
const finalValue = value.filter(range => {
|
|
340
|
+
const isContained = value.some(otherRange => {
|
|
341
|
+
if(range === otherRange) {
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
const startsWithin = range.compareBoundaryPoints(Range.START_TO_START, otherRange) > -1
|
|
345
|
+
const endsWithin = range.compareBoundaryPoints(Range.END_TO_END, otherRange) <= 0
|
|
346
|
+
return startsWithin && endsWithin
|
|
347
|
+
})
|
|
348
|
+
return !isContained && !range.collapsed
|
|
349
|
+
})
|
|
350
|
+
if(key === "value") {
|
|
351
|
+
this.#value = finalValue
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
this.#solution = finalValue
|
|
355
|
+
}
|
|
356
|
+
const highlight = WebwriterMark[key === "value"? "highlightValue": "highlightSolution"]
|
|
357
|
+
for(const range of prev) {
|
|
358
|
+
highlight.delete(range)
|
|
359
|
+
}
|
|
360
|
+
for(const range of value) {
|
|
361
|
+
highlight.add(range)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
observer: MutationObserver
|
|
366
|
+
|
|
367
|
+
segments: Intl.SegmentData[] = []
|
|
368
|
+
|
|
369
|
+
connectedCallback(): void {
|
|
370
|
+
super.connectedCallback()
|
|
371
|
+
const segmenter = new Intl.Segmenter(undefined, {granularity: "word"})
|
|
372
|
+
this.segments = [...segmenter.segment(this.textContent)]
|
|
373
|
+
this.observer = new MutationObserver(() => {
|
|
374
|
+
this.value = this.value
|
|
375
|
+
this.segments = [...segmenter.segment(this.textContent)]
|
|
376
|
+
})
|
|
377
|
+
this.observer.observe(this, {characterData: true, childList: true, subtree: true})
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
disconnectedCallback(): void {
|
|
381
|
+
super.disconnectedCallback()
|
|
382
|
+
this.observer?.disconnect()
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
@eventOptions({passive: true})
|
|
386
|
+
@action({label: {_: "Toggle Highlight"}})
|
|
387
|
+
handleHighlight(e?: PointerEvent) {
|
|
388
|
+
if(e && this.isContentEditable && e.type === "click") {
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
else if(e && !this.isContentEditable && e.type === "contextmenu") {
|
|
392
|
+
return
|
|
393
|
+
}
|
|
394
|
+
// convert click to caret position
|
|
395
|
+
|
|
396
|
+
const {textNode, offset} = e
|
|
397
|
+
? getCaretPositionFromPoint(e)
|
|
398
|
+
: {textNode: document.getSelection().anchorNode, offset: document.getSelection().anchorOffset}
|
|
399
|
+
// convert caret position to segment range
|
|
400
|
+
const segment = this.segments.filter(seg => seg.isWordLike).find(({index, segment}) => index <= offset && offset <= index + segment.length)
|
|
401
|
+
// add or remove segment range from highlights
|
|
402
|
+
if(segment) {
|
|
403
|
+
const range = new SerializableRange()
|
|
404
|
+
const start = segment.index
|
|
405
|
+
const end = segment.index + segment.segment.length
|
|
406
|
+
range.setStart(textNode, start)
|
|
407
|
+
range.setEnd(textNode, end)
|
|
408
|
+
const key = this.isContentEditable? "solution": "value"
|
|
409
|
+
const sameRange = this[key].find(r => r.startContainer === textNode && r.endContainer === textNode && r.startOffset === start && r.endOffset == end)
|
|
410
|
+
if(sameRange) {
|
|
411
|
+
this[key] = this[key].filter(r => r !== sameRange)
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
this[key] = [...this[key], range]
|
|
415
|
+
}
|
|
416
|
+
this.dispatchEvent(new CustomEvent("ww-answer-change", {
|
|
417
|
+
bubbles: true,
|
|
418
|
+
composed: true
|
|
419
|
+
}))
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
reset() {
|
|
424
|
+
this.value = this.solution = []
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
reportSolution() {}
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
render() {
|
|
431
|
+
return html`
|
|
432
|
+
<slot style=${styleMap({"--ww-placeholder": `"${this.msg("Text to Highlight")}"`})} ?data-empty=${!this.textContent} @click=${this.handleHighlight} @contextmenu=${this.handleHighlight}></slot>
|
|
433
|
+
`
|
|
434
|
+
}
|
|
435
|
+
}
|