@qti-components/extended-text-interaction 1.1.4 → 2.0.0-next.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.
@@ -0,0 +1,6 @@
1
+ import { QtiExtendedTextInteraction } from './qti-extended-text-interaction';
2
+ export declare const elements: readonly [{
3
+ readonly tag: "qti-extended-text-interaction";
4
+ readonly ctor: typeof QtiExtendedTextInteraction;
5
+ }];
6
+ export { QtiExtendedTextInteraction };
@@ -0,0 +1,4 @@
1
+ import { QtiExtendedTextInteraction } from './qti-extended-text-interaction';
2
+ export const elements = [{ tag: 'qti-extended-text-interaction', ctor: QtiExtendedTextInteraction }];
3
+ export { QtiExtendedTextInteraction };
4
+ //# sourceMappingURL=elements.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elements.js","sourceRoot":"","sources":["../src/elements.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAE7E,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,EAAE,GAAG,EAAE,+BAA+B,EAAE,IAAI,EAAE,0BAA0B,EAAE,CAAU,CAAC;AAE9G,OAAO,EAAE,0BAA0B,EAAE,CAAC","sourcesContent":["import { QtiExtendedTextInteraction } from './qti-extended-text-interaction';\n\nexport const elements = [{ tag: 'qti-extended-text-interaction', ctor: QtiExtendedTextInteraction }] as const;\n\nexport { QtiExtendedTextInteraction };\n"]}
@@ -1,26 +1,82 @@
1
1
  import { Interaction } from '@qti-components/base';
2
2
  import type { CSSResultGroup } from 'lit';
3
+ /**
4
+ * A multi-line text-entry interaction.
5
+ *
6
+ * @customElement qti-extended-text-interaction
7
+ *
8
+ * @attr {string} response-identifier - Required. Identifier of the bound response variable.
9
+ * @attr {number} expected-length - Hint at the expected answer length in characters; sets the
10
+ * textarea's `maxlength`. Also the last-resort row count, used only when neither a
11
+ * `qti-height-lines-*` class nor `expected-lines` is present.
12
+ * @attr {string} pattern-mask - Regular expression the value must match to be valid.
13
+ * @attr {string} placeholder-text - Placeholder shown while the textarea is empty.
14
+ * @attr {string} data-patternmask-message - Custom validation message shown when
15
+ * `pattern-mask` fails. Part of the QTI shared interaction vocabulary.
16
+ * @attr {'qti-height-lines-3'|'qti-height-lines-6'|'qti-height-lines-15'} class - QTI shared
17
+ * presentation vocabulary controlling the rendered height. Outranks both `expected-lines`
18
+ * and `expected-length`.
19
+ * @attr {number} expected-lines - Expected number of lines of response, and the rendered row
20
+ * count unless a `qti-height-lines-*` class is present.
21
+ * @attr {number} [base=10] - Not implemented. Numeric base used when recording the value.
22
+ * @attr {string} string-identifier - Not implemented. Identifier of a second, string-typed
23
+ * response variable that also receives the raw entry.
24
+ * @attr {number} max-strings - Not implemented. Maximum number of separate string responses. QTI
25
+ * defines these for a multi-string response — several bound variables, one field each. This
26
+ * element renders a single textarea bound to one variable, so there is nothing for the bounds
27
+ * to count: reading them as a word or paragraph limit would invent a rule QTI does not state.
28
+ * Implement alongside multi-string rendering, not before it.
29
+ * @attr {number} [min-strings=0] - Whether a response is required. Below 1 an empty field is valid; from 1
30
+ * it must hold a value. Only that threshold is read: the field is a single string, so the
31
+ * count itself (see `max-strings`) has nothing to bound.
32
+ * @attr {'plain'|'preformatted'|'xhtml'} [format=plain] - How the response is presented.
33
+ * `preformatted` renders the field monospaced; `xhtml` is accepted but downgraded to plain,
34
+ * because the field is a textarea with no rich-text editor behind it.
35
+ *
36
+ * @slot prompt - The prompt shown above the textarea.
37
+ *
38
+ * @csspart textarea - The textarea input element.
39
+ */
3
40
  export declare class QtiExtendedTextInteraction extends Interaction {
41
+ #private;
4
42
  static styles: CSSResultGroup;
5
43
  protected _rows: number;
6
44
  /** expected length is mapped to the property maxlength on the textarea */
7
45
  expectedLength: number;
46
+ /** QTI's own hint at the response height. Outranked by `qti-height-lines-*`; see `#resolveRows`. */
47
+ expectedLines: number;
48
+ /**
49
+ * How the response text is presented. Reflected so themes can key off
50
+ * `:host([format='preformatted'])`.
51
+ *
52
+ * `xhtml` is accepted and recorded but renders as `plain`: the field is a `<textarea>`, and
53
+ * there is no rich-text editor behind it. Downgrading loudly beats either refusing valid QTI
54
+ * or pretending to support formatting the candidate cannot actually produce.
55
+ */
56
+ format: 'plain' | 'preformatted' | 'xhtml';
57
+ protected _handleFormatChange: () => void;
58
+ /** Below 1 an empty field is valid; from 1 it must hold a value. Defaults to 0, as in the QTI spec. */
59
+ minStrings: number;
8
60
  patternMask: string;
9
61
  /** text appearing in the extended-text-interaction if it is empty */
10
62
  placeholderText: string;
11
63
  dataPatternmaskMessage: string;
12
64
  classNames: string;
13
- handleclassNamesChange(_: any, classes: string): void;
65
+ protected _handleRowsInputChange(): void;
14
66
  response: string | null;
15
67
  protected _handleResponseChange: () => void;
16
68
  get value(): string | null;
17
69
  set value(val: string | null);
18
70
  validate(): boolean;
19
- toggleCorrectResponse(): void;
20
71
  reportValidity(): boolean;
72
+ /** The prompt, above the field. */
73
+ protected renderPrompt(): unknown;
74
+ /** The field itself. */
75
+ protected renderTextarea(): unknown;
76
+ /** Hidden until `Interaction.reportValidity` shows it. */
77
+ protected renderValidationMessage(): unknown;
21
78
  render(): import("lit").TemplateResult<1>;
22
79
  protected textChanged(event: Event): void;
23
- protected setEmptyAttribute(text: string): void;
24
80
  }
25
81
  declare global {
26
82
  interface HTMLElementTagNameMap {
@@ -10,10 +10,94 @@ import { property, state } from 'lit/decorators.js';
10
10
  import { watch } from '@qti-components/utilities';
11
11
  import { Interaction } from '@qti-components/base';
12
12
  import styles from './qti-extended-text-interaction.styles';
13
+ /** Rows rendered when neither the class vocabulary nor either length hint resolves. */
14
+ const DEFAULT_ROWS = 5;
15
+ /** Rough characters-per-row rate used to turn `expected-length` into a row count. */
16
+ const CHARS_PER_ROW = 50;
17
+ /** QTI shared presentation vocabulary prefix carrying an explicit row count. */
18
+ const HEIGHT_LINES_PREFIX = 'qti-height-lines-';
19
+ /**
20
+ * Warn once per page rather than once per element: an item bank rendering many essays would
21
+ * otherwise bury the console in the same message.
22
+ */
23
+ let warnedAboutXhtml = false;
24
+ /**
25
+ * A multi-line text-entry interaction.
26
+ *
27
+ * @customElement qti-extended-text-interaction
28
+ *
29
+ * @attr {string} response-identifier - Required. Identifier of the bound response variable.
30
+ * @attr {number} expected-length - Hint at the expected answer length in characters; sets the
31
+ * textarea's `maxlength`. Also the last-resort row count, used only when neither a
32
+ * `qti-height-lines-*` class nor `expected-lines` is present.
33
+ * @attr {string} pattern-mask - Regular expression the value must match to be valid.
34
+ * @attr {string} placeholder-text - Placeholder shown while the textarea is empty.
35
+ * @attr {string} data-patternmask-message - Custom validation message shown when
36
+ * `pattern-mask` fails. Part of the QTI shared interaction vocabulary.
37
+ * @attr {'qti-height-lines-3'|'qti-height-lines-6'|'qti-height-lines-15'} class - QTI shared
38
+ * presentation vocabulary controlling the rendered height. Outranks both `expected-lines`
39
+ * and `expected-length`.
40
+ * @attr {number} expected-lines - Expected number of lines of response, and the rendered row
41
+ * count unless a `qti-height-lines-*` class is present.
42
+ * @attr {number} [base=10] - Not implemented. Numeric base used when recording the value.
43
+ * @attr {string} string-identifier - Not implemented. Identifier of a second, string-typed
44
+ * response variable that also receives the raw entry.
45
+ * @attr {number} max-strings - Not implemented. Maximum number of separate string responses. QTI
46
+ * defines these for a multi-string response — several bound variables, one field each. This
47
+ * element renders a single textarea bound to one variable, so there is nothing for the bounds
48
+ * to count: reading them as a word or paragraph limit would invent a rule QTI does not state.
49
+ * Implement alongside multi-string rendering, not before it.
50
+ * @attr {number} [min-strings=0] - Whether a response is required. Below 1 an empty field is valid; from 1
51
+ * it must hold a value. Only that threshold is read: the field is a single string, so the
52
+ * count itself (see `max-strings`) has nothing to bound.
53
+ * @attr {'plain'|'preformatted'|'xhtml'} [format=plain] - How the response is presented.
54
+ * `preformatted` renders the field monospaced; `xhtml` is accepted but downgraded to plain,
55
+ * because the field is a textarea with no rich-text editor behind it.
56
+ *
57
+ * @slot prompt - The prompt shown above the textarea.
58
+ *
59
+ * @csspart textarea - The textarea input element.
60
+ */
13
61
  export class QtiExtendedTextInteraction extends Interaction {
14
62
  constructor() {
15
63
  super(...arguments);
16
- this._rows = 5;
64
+ this._rows = DEFAULT_ROWS;
65
+ /**
66
+ * How the response text is presented. Reflected so themes can key off
67
+ * `:host([format='preformatted'])`.
68
+ *
69
+ * `xhtml` is accepted and recorded but renders as `plain`: the field is a `<textarea>`, and
70
+ * there is no rich-text editor behind it. Downgrading loudly beats either refusing valid QTI
71
+ * or pretending to support formatting the candidate cannot actually produce.
72
+ */
73
+ /*
74
+ * The vocabulary is written inline rather than as an exported `TextFormat` alias. Both text
75
+ * interactions need it in their public API, and the umbrella `@qti-components/interactions`
76
+ * re-exports both packages — two identically-named exported aliases collide at that barrel
77
+ * (TS2308). Inline, there is no name to collide, and the manifest still reads the union.
78
+ */
79
+ this.format = 'plain';
80
+ this._handleFormatChange = () => {
81
+ if (this.format !== 'xhtml' || warnedAboutXhtml)
82
+ return;
83
+ warnedAboutXhtml = true;
84
+ console.warn('<qti-extended-text-interaction format="xhtml"> renders as plain text. ' +
85
+ 'The interaction has no rich-text editor; the recorded response will contain no markup.');
86
+ };
87
+ /** Below 1 an empty field is valid; from 1 it must hold a value. Defaults to 0, as in the QTI spec. */
88
+ this.minStrings = 0;
89
+ /*
90
+ * Declared with `attribute: 'response'`, not `@state`, so `response="…"` in markup reaches the
91
+ * field. As a `@state` it was unobserved: every sibling interaction accepts the attribute — it
92
+ * is half of the codec's contract in packages/qti-base/src/lib/response.ts, which parses
93
+ * `response` and `correct-response` alike — but this one silently ignored it, so authored or
94
+ * restored prose rendered as an empty textarea.
95
+ *
96
+ * Plain `type: String`, deliberately NOT `responseAttributeConverter`. That codec splits on
97
+ * commas to build multi-value responses, which is right for identifiers, pairs and points and
98
+ * wrong for prose: an essay containing a comma would arrive as an array. text-entry, the other
99
+ * single-string interaction, declares it plainly for the same reason.
100
+ */
17
101
  this.response = null;
18
102
  this._handleResponseChange = () => {
19
103
  this._internals.setFormValue(this.value);
@@ -21,21 +105,45 @@ export class QtiExtendedTextInteraction extends Interaction {
21
105
  };
22
106
  }
23
107
  static { this.styles = styles; }
24
- handleclassNamesChange(_, classes) {
25
- const classNames = classes.split(' ');
26
- let rowsSet = false;
27
- classNames.forEach((className) => {
28
- if (className.startsWith('qti-height-lines-')) {
29
- const nrRows = className.replace('qti-height-lines-', '');
30
- this._rows = parseInt(nrRows);
31
- rowsSet = true;
32
- }
33
- });
34
- // If no qti-height-lines class is set, calculate rows based on expectedLength
35
- if (!rowsSet && this.expectedLength) {
36
- const estimatedRows = Math.ceil(this.expectedLength / 50); // '50' based on an estimate for characters per row
37
- this._rows = estimatedRows;
108
+ /*
109
+ * All three inputs feed one resolver rather than each assigning `_rows` in its own handler.
110
+ * With separate handlers the last attribute to change would win, so the same markup could
111
+ * render a different height depending on attribute order — and `expected-length` only ever
112
+ * applied when a `class` was also present, because the fallback lived inside the class watcher.
113
+ */
114
+ _handleRowsInputChange() {
115
+ this._rows = this.#resolveRows();
116
+ }
117
+ /**
118
+ * Row count, highest precedence first:
119
+ *
120
+ * 1. `qti-height-lines-N` — the QTI shared presentation vocabulary. An explicit rendering
121
+ * instruction, so it outranks the content-level hints below.
122
+ * 2. `expected-lines` — QTI's hint at how long the response should be.
123
+ * 3. `expected-length` — a character count, converted at a rough characters-per-row rate.
124
+ * 4. {@link DEFAULT_ROWS}.
125
+ */
126
+ #resolveRows() {
127
+ const fromHeightLines = this.#rowsFromHeightLinesClass();
128
+ if (fromHeightLines !== null)
129
+ return fromHeightLines;
130
+ if (this.expectedLines > 0)
131
+ return this.expectedLines;
132
+ if (this.expectedLength > 0)
133
+ return Math.ceil(this.expectedLength / CHARS_PER_ROW);
134
+ return DEFAULT_ROWS;
135
+ }
136
+ /** The `N` of the first usable `qti-height-lines-N` class, or null when none is present. */
137
+ #rowsFromHeightLinesClass() {
138
+ for (const className of (this.classNames ?? '').split(' ')) {
139
+ if (!className.startsWith(HEIGHT_LINES_PREFIX))
140
+ continue;
141
+ const rows = parseInt(className.slice(HEIGHT_LINES_PREFIX.length), 10);
142
+ // A bare `qti-height-lines-` parses to NaN; ignore it rather than render NaN rows.
143
+ if (!Number.isNaN(rows) && rows > 0)
144
+ return rows;
38
145
  }
146
+ return null;
39
147
  }
40
148
  get value() {
41
149
  return this.response || null;
@@ -47,73 +155,92 @@ export class QtiExtendedTextInteraction extends Interaction {
47
155
  const textarea = this.shadowRoot.querySelector('textarea');
48
156
  if (!textarea)
49
157
  return false;
158
+ let validityMessage = '';
159
+ let isValid = false;
50
160
  if (this.patternMask && this.dataPatternmaskMessage) {
51
161
  // Clear any custom error initially
52
- this._internals.setValidity({});
53
162
  textarea.setCustomValidity('');
54
163
  const patternSource = this.patternMask.startsWith('^') && this.patternMask.endsWith('$') ? this.patternMask : `^${this.patternMask}$`;
55
164
  const pattern = new RegExp(patternSource);
56
- const isValid = textarea.checkValidity() && pattern.test(textarea.value);
165
+ isValid = textarea.checkValidity() && pattern.test(textarea.value);
57
166
  if (!isValid) {
58
167
  // Set custom error if invalid
59
- this._internals.setValidity({ customError: true }, this.dataPatternmaskMessage);
168
+ validityMessage = this.dataPatternmaskMessage;
60
169
  textarea.setCustomValidity(this.dataPatternmaskMessage);
61
170
  }
62
171
  }
63
172
  else {
64
- const isValid = textarea.checkValidity();
65
- this._internals.setValidity(isValid ? {} : { customError: false });
173
+ isValid = textarea.checkValidity();
66
174
  }
67
- return !!this.response && textarea.checkValidity();
68
- }
69
- toggleCorrectResponse() {
70
- // No correct response possible for extended text interactions
71
- }
72
- reportValidity() {
73
- const textarea = this.shadowRoot.querySelector('textarea');
74
- if (!textarea)
75
- return false;
76
- // Run the validate function to ensure the custom validity state is up to date
77
- const isValid = this.validate();
78
- if (!isValid) {
79
- textarea.reportValidity();
175
+ if (isValid && !this.response && this.minStrings >= 1) {
176
+ isValid = false;
177
+ }
178
+ if (!isValid && !validityMessage) {
179
+ validityMessage = textarea.validationMessage || 'Invalid value.';
80
180
  }
181
+ this.setInteractionValidity(isValid, validityMessage, textarea, { suppressInline: true });
81
182
  return isValid;
82
183
  }
83
- render() {
84
- return html `<slot name="prompt"></slot
85
- ><textarea
86
- part="textarea"
87
- name="${this.responseIdentifier}"
88
- spellcheck="false"
89
- autocomplete="off"
90
- maxlength="${5000}"
91
- @keydown="${(event) => event.stopImmediatePropagation()}"
92
- @keyup="${this.textChanged}"
93
- @change="${this.textChanged}"
94
- @blur="${(_) => {
184
+ reportValidity() {
185
+ this.validate();
186
+ return super.reportValidity();
187
+ }
188
+ /*
189
+ * The template in named pieces, so a subclass can recompose it.
190
+ *
191
+ * Each piece renders one thing and returns it; `render()` below just orders them. A subclass
192
+ * overrides `render()` and calls the pieces where it wants them — which is the point: an extension
193
+ * point that only lets you APPEND has already decided the layout on the subclass's behalf, and the
194
+ * subclass is usually the one that knows better. The correction variant in
195
+ * @qti-components/corrections puts its badge between the field and the validation message;
196
+ * something else may want it above the prompt, or may want to drop a piece entirely.
197
+ *
198
+ * Override a piece to change what one part looks like; override `render()` to change the order.
199
+ */
200
+ /** The prompt, above the field. */
201
+ renderPrompt() {
202
+ return html `<slot name="prompt"></slot>`;
203
+ }
204
+ /** The field itself. */
205
+ renderTextarea() {
206
+ return html `<textarea
207
+ part="textarea"
208
+ name="${this.responseIdentifier}"
209
+ spellcheck="false"
210
+ autocomplete="off"
211
+ maxlength="${5000}"
212
+ @keydown="${(event) => event.stopImmediatePropagation()}"
213
+ @keyup="${this.textChanged}"
214
+ @change="${this.textChanged}"
215
+ @blur="${(_) => {
95
216
  this.reportValidity();
96
217
  }}"
97
- placeholder="${ifDefined(this.placeholderText ? this.placeholderText : undefined)}"
98
- rows="${this._rows}"
99
- ?disabled="${this.disabled}"
100
- ?readonly="${this.readonly}"
101
- .value=${this.response}
102
- ></textarea>`;
218
+ placeholder="${ifDefined(this.placeholderText ? this.placeholderText : undefined)}"
219
+ rows="${this._rows}"
220
+ ?disabled="${this.disabled}"
221
+ ?readonly="${this.readonly}"
222
+ .value=${this.response}
223
+ ></textarea>`;
224
+ }
225
+ /** Hidden until `Interaction.reportValidity` shows it. */
226
+ renderValidationMessage() {
227
+ return html `<div id="validation-message" part="message" role="alert" style="display:none;"></div>`;
228
+ }
229
+ render() {
230
+ /* The prompt and the field are written with NO whitespace between them, deliberately: a text
231
+ node there puts a line box between the two. The original template said `</slot\n><textarea`
232
+ to avoid exactly that, and interpolating them adjacently keeps it. */
233
+ return html `${this.renderPrompt()}${this.renderTextarea()} ${this.renderValidationMessage()}`;
103
234
  }
104
235
  textChanged(event) {
105
236
  if (this.disabled || this.readonly)
106
237
  return;
107
238
  const input = event.target;
108
- this.setEmptyAttribute(input.value);
109
239
  if (this.response !== input.value) {
110
240
  this.value = input.value;
111
241
  this.saveResponse(input.value);
112
242
  }
113
243
  }
114
- setEmptyAttribute(text) {
115
- this.setAttribute('empty', text === '' ? 'true' : 'false');
116
- }
117
244
  }
118
245
  __decorate([
119
246
  state()
@@ -121,6 +248,18 @@ __decorate([
121
248
  __decorate([
122
249
  property({ type: Number, attribute: 'expected-length' })
123
250
  ], QtiExtendedTextInteraction.prototype, "expectedLength", void 0);
251
+ __decorate([
252
+ property({ type: Number, attribute: 'expected-lines' })
253
+ ], QtiExtendedTextInteraction.prototype, "expectedLines", void 0);
254
+ __decorate([
255
+ property({ type: String, reflect: true })
256
+ ], QtiExtendedTextInteraction.prototype, "format", void 0);
257
+ __decorate([
258
+ watch('format')
259
+ ], QtiExtendedTextInteraction.prototype, "_handleFormatChange", void 0);
260
+ __decorate([
261
+ property({ type: Number, attribute: 'min-strings' })
262
+ ], QtiExtendedTextInteraction.prototype, "minStrings", void 0);
124
263
  __decorate([
125
264
  property({ type: String, attribute: 'pattern-mask' })
126
265
  ], QtiExtendedTextInteraction.prototype, "patternMask", void 0);
@@ -134,10 +273,10 @@ __decorate([
134
273
  property({ type: String, attribute: 'class' })
135
274
  ], QtiExtendedTextInteraction.prototype, "classNames", void 0);
136
275
  __decorate([
137
- watch('classNames')
138
- ], QtiExtendedTextInteraction.prototype, "handleclassNamesChange", null);
276
+ watch(['classNames', 'expectedLines', 'expectedLength'])
277
+ ], QtiExtendedTextInteraction.prototype, "_handleRowsInputChange", null);
139
278
  __decorate([
140
- state()
279
+ property({ type: String, attribute: 'response', reflect: false })
141
280
  ], QtiExtendedTextInteraction.prototype, "response", void 0);
142
281
  __decorate([
143
282
  watch('response', { waitUntilFirstUpdate: true })
@@ -1 +1 @@
1
- {"version":3,"file":"qti-extended-text-interaction.js","sourceRoot":"","sources":["../src/qti-extended-text-interaction.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAC3B,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AACzD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEnD,OAAO,MAAM,MAAM,wCAAwC,CAAC;AAG5D,MAAM,OAAO,0BAA2B,SAAQ,WAAW;IAA3D;;QAIY,UAAK,GAAG,CAAC,CAAC;QAgCpB,aAAQ,GAAkB,IAAI,CAAC;QAGrB,0BAAqB,GAAG,GAAG,EAAE;YACrC,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACzC,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClB,CAAC,CAAC;IAuFJ,CAAC;aAhIiB,WAAM,GAAmB,MAAM,AAAzB,CAA0B;IAiBhD,sBAAsB,CAAC,CAAM,EAAE,OAAe;QAC5C,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACtC,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,UAAU,CAAC,OAAO,CAAC,CAAC,SAAiB,EAAE,EAAE;YACvC,IAAI,SAAS,CAAC,UAAU,CAAC,mBAAmB,CAAC,EAAE,CAAC;gBAC9C,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC;gBAC1D,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;gBAC9B,OAAO,GAAG,IAAI,CAAC;YACjB,CAAC;QACH,CAAC,CAAC,CAAC;QACH,8EAA8E;QAC9E,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACpC,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,GAAG,EAAE,CAAC,CAAC,CAAC,oDAAoD;YAC/G,IAAI,CAAC,KAAK,GAAG,aAAa,CAAC;QAC7B,CAAC;IACH,CAAC;IAWD,IAAa,KAAK;QAChB,OAAO,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC;IAC/B,CAAC;IACD,IAAa,KAAK,CAAC,GAAkB;QACnC,IAAI,CAAC,QAAQ,GAAG,GAAG,IAAI,IAAI,CAAC;IAC9B,CAAC;IAEe,QAAQ;QACtB,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;QAC3D,IAAI,CAAC,QAAQ;YAAE,OAAO,KAAK,CAAC;QAE5B,IAAI,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,sBAAsB,EAAE,CAAC;YACpD,mCAAmC;YACnC,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;YAChC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,CAAC;YAC/B,MAAM,aAAa,GACjB,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,WAAW,GAAG,CAAC;YAElH,MAAM,OAAO,GAAG,IAAI,MAAM,CAAC,aAAa,CAAC,CAAC;YAC1C,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;YAEzE,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,8BAA8B;gBAC9B,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,EAAE,IAAI,CAAC,sBAAsB,CAAC,CAAC;gBAChF,QAAQ,CAAC,iBAAiB,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;YAC1D,CAAC;QACH,CAAC;aAAM,CAAC;YACN,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,EAAE,CAAC;YACzC,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAC;QACrE,CAAC;QAED,OAAO,CAAC,CAAC,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,aAAa,EAAE,CAAC;IACrD,CAAC;IAEe,qBAAqB;QACnC,8DAA8D;IAChE,CAAC;IAEQ,cAAc;QACrB,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;QAC3D,IAAI,CAAC,QAAQ;YAAE,OAAO,KAAK,CAAC;QAE5B,8EAA8E;QAC9E,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChC,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,QAAQ,CAAC,cAAc,EAAE,CAAC;QAC5B,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAEQ,MAAM;QACb,OAAO,IAAI,CAAA;;;gBAGC,IAAI,CAAC,kBAAkB;;;qBAGlB,IAAI;oBACL,CAAC,KAAoB,EAAE,EAAE,CAAC,KAAK,CAAC,wBAAwB,EAAE;kBAC5D,IAAI,CAAC,WAAW;mBACf,IAAI,CAAC,WAAW;iBAClB,CAAC,CAAa,EAAE,EAAE;YACzB,IAAI,CAAC,cAAc,EAAE,CAAC;QACxB,CAAC;uBACc,SAAS,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,SAAS,CAAC;gBACzE,IAAI,CAAC,KAAK;qBACL,IAAI,CAAC,QAAQ;qBACb,IAAI,CAAC,QAAQ;iBACjB,IAAI,CAAC,QAAQ;mBACX,CAAC;IAClB,CAAC;IAES,WAAW,CAAC,KAAY;QAChC,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO;QAC3C,MAAM,KAAK,GAAG,KAAK,CAAC,MAA0B,CAAC;QAC/C,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACpC,IAAI,IAAI,CAAC,QAAQ,KAAK,KAAK,CAAC,KAAK,EAAE,CAAC;YAClC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;YACzB,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;IAES,iBAAiB,CAAC,IAAY;QACtC,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IAC7D,CAAC;;AA5HS;IADT,KAAK,EAAE;yDACY;AAGsC;IAAzD,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,iBAAiB,EAAE,CAAC;kEAAwB;AAE1B;IAAtD,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC;+DAAqB;AAGhB;IAA1D,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAAC;mEAAyB;AAEhB;IAAlE,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,0BAA0B,EAAE,CAAC;0EAAgC;AAElD;IAA/C,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;8DAAoB;AAEnE;IADC,KAAK,CAAC,YAAY,CAAC;wEAgBnB;AAGD;IADC,KAAK,EAAE;4DACuB;AAGrB;IADT,KAAK,CAAC,UAAU,EAAE,EAAE,oBAAoB,EAAE,IAAI,EAAE,CAAC;yEAIhD","sourcesContent":["import { html } from 'lit';\nimport { ifDefined } from 'lit/directives/if-defined.js';\nimport { property, state } from 'lit/decorators.js';\n\nimport { watch } from '@qti-components/utilities';\nimport { Interaction } from '@qti-components/base';\n\nimport styles from './qti-extended-text-interaction.styles';\n\nimport type { CSSResultGroup } from 'lit';\nexport class QtiExtendedTextInteraction extends Interaction {\n static override styles: CSSResultGroup = styles;\n\n @state()\n protected _rows = 5;\n\n /** expected length is mapped to the property maxlength on the textarea */\n @property({ type: Number, attribute: 'expected-length' }) expectedLength: number;\n\n @property({ type: String, attribute: 'pattern-mask' }) patternMask: string;\n\n /** text appearing in the extended-text-interaction if it is empty */\n @property({ type: String, attribute: 'placeholder-text' }) placeholderText: string;\n\n @property({ type: String, attribute: 'data-patternmask-message' }) dataPatternmaskMessage: string;\n\n @property({ type: String, attribute: 'class' }) classNames: string;\n @watch('classNames')\n handleclassNamesChange(_: any, classes: string) {\n const classNames = classes.split(' ');\n let rowsSet = false;\n classNames.forEach((className: string) => {\n if (className.startsWith('qti-height-lines-')) {\n const nrRows = className.replace('qti-height-lines-', '');\n this._rows = parseInt(nrRows);\n rowsSet = true;\n }\n });\n // If no qti-height-lines class is set, calculate rows based on expectedLength\n if (!rowsSet && this.expectedLength) {\n const estimatedRows = Math.ceil(this.expectedLength / 50); // '50' based on an estimate for characters per row\n this._rows = estimatedRows;\n }\n }\n\n @state()\n response: string | null = null;\n\n @watch('response', { waitUntilFirstUpdate: true })\n protected _handleResponseChange = () => {\n this._internals.setFormValue(this.value);\n this.validate();\n };\n\n override get value(): string | null {\n return this.response || null;\n }\n override set value(val: string | null) {\n this.response = val || null;\n }\n\n public override validate() {\n const textarea = this.shadowRoot.querySelector('textarea');\n if (!textarea) return false;\n\n if (this.patternMask && this.dataPatternmaskMessage) {\n // Clear any custom error initially\n this._internals.setValidity({});\n textarea.setCustomValidity('');\n const patternSource =\n this.patternMask.startsWith('^') && this.patternMask.endsWith('$') ? this.patternMask : `^${this.patternMask}$`;\n\n const pattern = new RegExp(patternSource);\n const isValid = textarea.checkValidity() && pattern.test(textarea.value);\n\n if (!isValid) {\n // Set custom error if invalid\n this._internals.setValidity({ customError: true }, this.dataPatternmaskMessage);\n textarea.setCustomValidity(this.dataPatternmaskMessage);\n }\n } else {\n const isValid = textarea.checkValidity();\n this._internals.setValidity(isValid ? {} : { customError: false });\n }\n\n return !!this.response && textarea.checkValidity();\n }\n\n public override toggleCorrectResponse() {\n // No correct response possible for extended text interactions\n }\n\n override reportValidity() {\n const textarea = this.shadowRoot.querySelector('textarea');\n if (!textarea) return false;\n\n // Run the validate function to ensure the custom validity state is up to date\n const isValid = this.validate();\n if (!isValid) {\n textarea.reportValidity();\n }\n return isValid;\n }\n\n override render() {\n return html`<slot name=\"prompt\"></slot\n ><textarea\n part=\"textarea\"\n name=\"${this.responseIdentifier}\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n maxlength=\"${5000}\"\n @keydown=\"${(event: KeyboardEvent) => event.stopImmediatePropagation()}\"\n @keyup=\"${this.textChanged}\"\n @change=\"${this.textChanged}\"\n @blur=\"${(_: FocusEvent) => {\n this.reportValidity();\n }}\"\n placeholder=\"${ifDefined(this.placeholderText ? this.placeholderText : undefined)}\"\n rows=\"${this._rows}\"\n ?disabled=\"${this.disabled}\"\n ?readonly=\"${this.readonly}\"\n .value=${this.response}\n ></textarea>`;\n }\n\n protected textChanged(event: Event) {\n if (this.disabled || this.readonly) return;\n const input = event.target as HTMLInputElement;\n this.setEmptyAttribute(input.value);\n if (this.response !== input.value) {\n this.value = input.value;\n this.saveResponse(input.value);\n }\n }\n\n protected setEmptyAttribute(text: string) {\n this.setAttribute('empty', text === '' ? 'true' : 'false');\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'qti-extended-text-interaction': QtiExtendedTextInteraction;\n }\n}\n"]}
1
+ {"version":3,"file":"qti-extended-text-interaction.js","sourceRoot":"","sources":["../src/qti-extended-text-interaction.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAC3B,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AACzD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEnD,OAAO,MAAM,MAAM,wCAAwC,CAAC;AAG5D,uFAAuF;AACvF,MAAM,YAAY,GAAG,CAAC,CAAC;AAEvB,qFAAqF;AACrF,MAAM,aAAa,GAAG,EAAE,CAAC;AAEzB,gFAAgF;AAChF,MAAM,mBAAmB,GAAG,mBAAmB,CAAC;AAEhD;;;GAGG;AACH,IAAI,gBAAgB,GAAG,KAAK,CAAC;AAE7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,OAAO,0BAA2B,SAAQ,WAAW;IAA3D;;QAIY,UAAK,GAAG,YAAY,CAAC;QAQ/B;;;;;;;WAOG;QACH;;;;;WAKG;QACwC,WAAM,GAAuC,OAAO,CAAC;QAGtF,wBAAmB,GAAG,GAAG,EAAE;YACnC,IAAI,IAAI,CAAC,MAAM,KAAK,OAAO,IAAI,gBAAgB;gBAAE,OAAO;YAExD,gBAAgB,GAAG,IAAI,CAAC;YACxB,OAAO,CAAC,IAAI,CACV,wEAAwE;gBACtE,wFAAwF,CAC3F,CAAC;QACJ,CAAC,CAAC;QAEF,uGAAuG;QACjD,eAAU,GAAG,CAAC,CAAC;QAsDrE;;;;;;;;;;;WAWG;QAEH,aAAQ,GAAkB,IAAI,CAAC;QAGrB,0BAAqB,GAAG,GAAG,EAAE;YACrC,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACzC,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClB,CAAC,CAAC;IA8GJ,CAAC;aA9NiB,WAAM,GAAmB,MAAM,AAAzB,CAA0B;IAkDhD;;;;;OAKG;IAEO,sBAAsB;QAC9B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;IACnC,CAAC;IAED;;;;;;;;OAQG;IACH,YAAY;QACV,MAAM,eAAe,GAAG,IAAI,CAAC,yBAAyB,EAAE,CAAC;QACzD,IAAI,eAAe,KAAK,IAAI;YAAE,OAAO,eAAe,CAAC;QAErD,IAAI,IAAI,CAAC,aAAa,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC,aAAa,CAAC;QACtD,IAAI,IAAI,CAAC,cAAc,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,GAAG,aAAa,CAAC,CAAC;QAEnF,OAAO,YAAY,CAAC;IACtB,CAAC;IAED,4FAA4F;IAC5F,yBAAyB;QACvB,KAAK,MAAM,SAAS,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3D,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,mBAAmB,CAAC;gBAAE,SAAS;YAEzD,MAAM,IAAI,GAAG,QAAQ,CAAC,SAAS,CAAC,KAAK,CAAC,mBAAmB,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;YACvE,mFAAmF;YACnF,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC;gBAAE,OAAO,IAAI,CAAC;QACnD,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAuBD,IAAa,KAAK;QAChB,OAAO,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC;IAC/B,CAAC;IACD,IAAa,KAAK,CAAC,GAAkB;QACnC,IAAI,CAAC,QAAQ,GAAG,GAAG,IAAI,IAAI,CAAC;IAC9B,CAAC;IAEe,QAAQ;QACtB,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;QAC3D,IAAI,CAAC,QAAQ;YAAE,OAAO,KAAK,CAAC;QAC5B,IAAI,eAAe,GAAG,EAAE,CAAC;QACzB,IAAI,OAAO,GAAG,KAAK,CAAC;QAEpB,IAAI,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,sBAAsB,EAAE,CAAC;YACpD,mCAAmC;YACnC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,CAAC;YAC/B,MAAM,aAAa,GACjB,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,WAAW,GAAG,CAAC;YAElH,MAAM,OAAO,GAAG,IAAI,MAAM,CAAC,aAAa,CAAC,CAAC;YAC1C,OAAO,GAAG,QAAQ,CAAC,aAAa,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;YAEnE,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,8BAA8B;gBAC9B,eAAe,GAAG,IAAI,CAAC,sBAAsB,CAAC;gBAC9C,QAAQ,CAAC,iBAAiB,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;YAC1D,CAAC;QACH,CAAC;aAAM,CAAC;YACN,OAAO,GAAG,QAAQ,CAAC,aAAa,EAAE,CAAC;QACrC,CAAC;QAED,IAAI,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,UAAU,IAAI,CAAC,EAAE,CAAC;YACtD,OAAO,GAAG,KAAK,CAAC;QAClB,CAAC;QAED,IAAI,CAAC,OAAO,IAAI,CAAC,eAAe,EAAE,CAAC;YACjC,eAAe,GAAG,QAAQ,CAAC,iBAAiB,IAAI,gBAAgB,CAAC;QACnE,CAAC;QAED,IAAI,CAAC,sBAAsB,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1F,OAAO,OAAO,CAAC;IACjB,CAAC;IAEQ,cAAc;QACrB,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChB,OAAO,KAAK,CAAC,cAAc,EAAE,CAAC;IAChC,CAAC;IAED;;;;;;;;;;;OAWG;IAEH,mCAAmC;IACzB,YAAY;QACpB,OAAO,IAAI,CAAA,6BAA6B,CAAC;IAC3C,CAAC;IAED,wBAAwB;IACd,cAAc;QACtB,OAAO,IAAI,CAAA;;cAED,IAAI,CAAC,kBAAkB;;;mBAGlB,IAAI;kBACL,CAAC,KAAoB,EAAE,EAAE,CAAC,KAAK,CAAC,wBAAwB,EAAE;gBAC5D,IAAI,CAAC,WAAW;iBACf,IAAI,CAAC,WAAW;eAClB,CAAC,CAAa,EAAE,EAAE;YACzB,IAAI,CAAC,cAAc,EAAE,CAAC;QACxB,CAAC;qBACc,SAAS,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,SAAS,CAAC;cACzE,IAAI,CAAC,KAAK;mBACL,IAAI,CAAC,QAAQ;mBACb,IAAI,CAAC,QAAQ;eACjB,IAAI,CAAC,QAAQ;iBACX,CAAC;IAChB,CAAC;IAED,0DAA0D;IAChD,uBAAuB;QAC/B,OAAO,IAAI,CAAA,uFAAuF,CAAC;IACrG,CAAC;IAEQ,MAAM;QACb;;gFAEwE;QACxE,OAAO,IAAI,CAAA,GAAG,IAAI,CAAC,YAAY,EAAE,GAAG,IAAI,CAAC,cAAc,EAAE,IAAI,IAAI,CAAC,uBAAuB,EAAE,EAAE,CAAC;IAChG,CAAC;IAES,WAAW,CAAC,KAAY;QAChC,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO;QAC3C,MAAM,KAAK,GAAG,KAAK,CAAC,MAA0B,CAAC;QAC/C,IAAI,IAAI,CAAC,QAAQ,KAAK,KAAK,CAAC,KAAK,EAAE,CAAC;YAClC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;YACzB,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;;AA1NS;IADT,KAAK,EAAE;yDACuB;AAG2B;IAAzD,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,iBAAiB,EAAE,CAAC;kEAAwB;AAGxB;IAAxD,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,gBAAgB,EAAE,CAAC;iEAAuB;AAgBpC;IAA1C,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;0DAAsD;AAGtF;IADT,KAAK,CAAC,QAAQ,CAAC;uEASd;AAGoD;IAArD,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,CAAC;8DAAgB;AAEd;IAAtD,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC;+DAAqB;AAGhB;IAA1D,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAAC;mEAAyB;AAEhB;IAAlE,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,0BAA0B,EAAE,CAAC;0EAAgC;AAElD;IAA/C,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;8DAAoB;AASzD;IADT,KAAK,CAAC,CAAC,YAAY,EAAE,eAAe,EAAE,gBAAgB,CAAC,CAAC;wEAGxD;AA+CD;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;4DACnC;AAGrB;IADT,KAAK,CAAC,UAAU,EAAE,EAAE,oBAAoB,EAAE,IAAI,EAAE,CAAC;yEAIhD","sourcesContent":["import { html } from 'lit';\nimport { ifDefined } from 'lit/directives/if-defined.js';\nimport { property, state } from 'lit/decorators.js';\n\nimport { watch } from '@qti-components/utilities';\nimport { Interaction } from '@qti-components/base';\n\nimport styles from './qti-extended-text-interaction.styles';\n\nimport type { CSSResultGroup } from 'lit';\n/** Rows rendered when neither the class vocabulary nor either length hint resolves. */\nconst DEFAULT_ROWS = 5;\n\n/** Rough characters-per-row rate used to turn `expected-length` into a row count. */\nconst CHARS_PER_ROW = 50;\n\n/** QTI shared presentation vocabulary prefix carrying an explicit row count. */\nconst HEIGHT_LINES_PREFIX = 'qti-height-lines-';\n\n/**\n * Warn once per page rather than once per element: an item bank rendering many essays would\n * otherwise bury the console in the same message.\n */\nlet warnedAboutXhtml = false;\n\n/**\n * A multi-line text-entry interaction.\n *\n * @customElement qti-extended-text-interaction\n *\n * @attr {string} response-identifier - Required. Identifier of the bound response variable.\n * @attr {number} expected-length - Hint at the expected answer length in characters; sets the\n * textarea's `maxlength`. Also the last-resort row count, used only when neither a\n * `qti-height-lines-*` class nor `expected-lines` is present.\n * @attr {string} pattern-mask - Regular expression the value must match to be valid.\n * @attr {string} placeholder-text - Placeholder shown while the textarea is empty.\n * @attr {string} data-patternmask-message - Custom validation message shown when\n * `pattern-mask` fails. Part of the QTI shared interaction vocabulary.\n * @attr {'qti-height-lines-3'|'qti-height-lines-6'|'qti-height-lines-15'} class - QTI shared\n * presentation vocabulary controlling the rendered height. Outranks both `expected-lines`\n * and `expected-length`.\n * @attr {number} expected-lines - Expected number of lines of response, and the rendered row\n * count unless a `qti-height-lines-*` class is present.\n * @attr {number} [base=10] - Not implemented. Numeric base used when recording the value.\n * @attr {string} string-identifier - Not implemented. Identifier of a second, string-typed\n * response variable that also receives the raw entry.\n * @attr {number} max-strings - Not implemented. Maximum number of separate string responses. QTI\n * defines these for a multi-string response — several bound variables, one field each. This\n * element renders a single textarea bound to one variable, so there is nothing for the bounds\n * to count: reading them as a word or paragraph limit would invent a rule QTI does not state.\n * Implement alongside multi-string rendering, not before it.\n * @attr {number} [min-strings=0] - Whether a response is required. Below 1 an empty field is valid; from 1\n * it must hold a value. Only that threshold is read: the field is a single string, so the\n * count itself (see `max-strings`) has nothing to bound.\n * @attr {'plain'|'preformatted'|'xhtml'} [format=plain] - How the response is presented.\n * `preformatted` renders the field monospaced; `xhtml` is accepted but downgraded to plain,\n * because the field is a textarea with no rich-text editor behind it.\n *\n * @slot prompt - The prompt shown above the textarea.\n *\n * @csspart textarea - The textarea input element.\n */\nexport class QtiExtendedTextInteraction extends Interaction {\n static override styles: CSSResultGroup = styles;\n\n @state()\n protected _rows = DEFAULT_ROWS;\n\n /** expected length is mapped to the property maxlength on the textarea */\n @property({ type: Number, attribute: 'expected-length' }) expectedLength: number;\n\n /** QTI's own hint at the response height. Outranked by `qti-height-lines-*`; see `#resolveRows`. */\n @property({ type: Number, attribute: 'expected-lines' }) expectedLines: number;\n\n /**\n * How the response text is presented. Reflected so themes can key off\n * `:host([format='preformatted'])`.\n *\n * `xhtml` is accepted and recorded but renders as `plain`: the field is a `<textarea>`, and\n * there is no rich-text editor behind it. Downgrading loudly beats either refusing valid QTI\n * or pretending to support formatting the candidate cannot actually produce.\n */\n /*\n * The vocabulary is written inline rather than as an exported `TextFormat` alias. Both text\n * interactions need it in their public API, and the umbrella `@qti-components/interactions`\n * re-exports both packages — two identically-named exported aliases collide at that barrel\n * (TS2308). Inline, there is no name to collide, and the manifest still reads the union.\n */\n @property({ type: String, reflect: true }) format: 'plain' | 'preformatted' | 'xhtml' = 'plain';\n\n @watch('format')\n protected _handleFormatChange = () => {\n if (this.format !== 'xhtml' || warnedAboutXhtml) return;\n\n warnedAboutXhtml = true;\n console.warn(\n '<qti-extended-text-interaction format=\"xhtml\"> renders as plain text. ' +\n 'The interaction has no rich-text editor; the recorded response will contain no markup.'\n );\n };\n\n /** Below 1 an empty field is valid; from 1 it must hold a value. Defaults to 0, as in the QTI spec. */\n @property({ type: Number, attribute: 'min-strings' }) minStrings = 0;\n\n @property({ type: String, attribute: 'pattern-mask' }) patternMask: string;\n\n /** text appearing in the extended-text-interaction if it is empty */\n @property({ type: String, attribute: 'placeholder-text' }) placeholderText: string;\n\n @property({ type: String, attribute: 'data-patternmask-message' }) dataPatternmaskMessage: string;\n\n @property({ type: String, attribute: 'class' }) classNames: string;\n\n /*\n * All three inputs feed one resolver rather than each assigning `_rows` in its own handler.\n * With separate handlers the last attribute to change would win, so the same markup could\n * render a different height depending on attribute order — and `expected-length` only ever\n * applied when a `class` was also present, because the fallback lived inside the class watcher.\n */\n @watch(['classNames', 'expectedLines', 'expectedLength'])\n protected _handleRowsInputChange() {\n this._rows = this.#resolveRows();\n }\n\n /**\n * Row count, highest precedence first:\n *\n * 1. `qti-height-lines-N` — the QTI shared presentation vocabulary. An explicit rendering\n * instruction, so it outranks the content-level hints below.\n * 2. `expected-lines` — QTI's hint at how long the response should be.\n * 3. `expected-length` — a character count, converted at a rough characters-per-row rate.\n * 4. {@link DEFAULT_ROWS}.\n */\n #resolveRows(): number {\n const fromHeightLines = this.#rowsFromHeightLinesClass();\n if (fromHeightLines !== null) return fromHeightLines;\n\n if (this.expectedLines > 0) return this.expectedLines;\n if (this.expectedLength > 0) return Math.ceil(this.expectedLength / CHARS_PER_ROW);\n\n return DEFAULT_ROWS;\n }\n\n /** The `N` of the first usable `qti-height-lines-N` class, or null when none is present. */\n #rowsFromHeightLinesClass(): number | null {\n for (const className of (this.classNames ?? '').split(' ')) {\n if (!className.startsWith(HEIGHT_LINES_PREFIX)) continue;\n\n const rows = parseInt(className.slice(HEIGHT_LINES_PREFIX.length), 10);\n // A bare `qti-height-lines-` parses to NaN; ignore it rather than render NaN rows.\n if (!Number.isNaN(rows) && rows > 0) return rows;\n }\n\n return null;\n }\n\n /*\n * Declared with `attribute: 'response'`, not `@state`, so `response=\"…\"` in markup reaches the\n * field. As a `@state` it was unobserved: every sibling interaction accepts the attribute — it\n * is half of the codec's contract in packages/qti-base/src/lib/response.ts, which parses\n * `response` and `correct-response` alike — but this one silently ignored it, so authored or\n * restored prose rendered as an empty textarea.\n *\n * Plain `type: String`, deliberately NOT `responseAttributeConverter`. That codec splits on\n * commas to build multi-value responses, which is right for identifiers, pairs and points and\n * wrong for prose: an essay containing a comma would arrive as an array. text-entry, the other\n * single-string interaction, declares it plainly for the same reason.\n */\n @property({ type: String, attribute: 'response', reflect: false })\n response: string | null = null;\n\n @watch('response', { waitUntilFirstUpdate: true })\n protected _handleResponseChange = () => {\n this._internals.setFormValue(this.value);\n this.validate();\n };\n\n override get value(): string | null {\n return this.response || null;\n }\n override set value(val: string | null) {\n this.response = val || null;\n }\n\n public override validate() {\n const textarea = this.shadowRoot.querySelector('textarea');\n if (!textarea) return false;\n let validityMessage = '';\n let isValid = false;\n\n if (this.patternMask && this.dataPatternmaskMessage) {\n // Clear any custom error initially\n textarea.setCustomValidity('');\n const patternSource =\n this.patternMask.startsWith('^') && this.patternMask.endsWith('$') ? this.patternMask : `^${this.patternMask}$`;\n\n const pattern = new RegExp(patternSource);\n isValid = textarea.checkValidity() && pattern.test(textarea.value);\n\n if (!isValid) {\n // Set custom error if invalid\n validityMessage = this.dataPatternmaskMessage;\n textarea.setCustomValidity(this.dataPatternmaskMessage);\n }\n } else {\n isValid = textarea.checkValidity();\n }\n\n if (isValid && !this.response && this.minStrings >= 1) {\n isValid = false;\n }\n\n if (!isValid && !validityMessage) {\n validityMessage = textarea.validationMessage || 'Invalid value.';\n }\n\n this.setInteractionValidity(isValid, validityMessage, textarea, { suppressInline: true });\n return isValid;\n }\n\n override reportValidity() {\n this.validate();\n return super.reportValidity();\n }\n\n /*\n * The template in named pieces, so a subclass can recompose it.\n *\n * Each piece renders one thing and returns it; `render()` below just orders them. A subclass\n * overrides `render()` and calls the pieces where it wants them — which is the point: an extension\n * point that only lets you APPEND has already decided the layout on the subclass's behalf, and the\n * subclass is usually the one that knows better. The correction variant in\n * @qti-components/corrections puts its badge between the field and the validation message;\n * something else may want it above the prompt, or may want to drop a piece entirely.\n *\n * Override a piece to change what one part looks like; override `render()` to change the order.\n */\n\n /** The prompt, above the field. */\n protected renderPrompt(): unknown {\n return html`<slot name=\"prompt\"></slot>`;\n }\n\n /** The field itself. */\n protected renderTextarea(): unknown {\n return html`<textarea\n part=\"textarea\"\n name=\"${this.responseIdentifier}\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n maxlength=\"${5000}\"\n @keydown=\"${(event: KeyboardEvent) => event.stopImmediatePropagation()}\"\n @keyup=\"${this.textChanged}\"\n @change=\"${this.textChanged}\"\n @blur=\"${(_: FocusEvent) => {\n this.reportValidity();\n }}\"\n placeholder=\"${ifDefined(this.placeholderText ? this.placeholderText : undefined)}\"\n rows=\"${this._rows}\"\n ?disabled=\"${this.disabled}\"\n ?readonly=\"${this.readonly}\"\n .value=${this.response}\n ></textarea>`;\n }\n\n /** Hidden until `Interaction.reportValidity` shows it. */\n protected renderValidationMessage(): unknown {\n return html`<div id=\"validation-message\" part=\"message\" role=\"alert\" style=\"display:none;\"></div>`;\n }\n\n override render() {\n /* The prompt and the field are written with NO whitespace between them, deliberately: a text\n node there puts a line box between the two. The original template said `</slot\\n><textarea`\n to avoid exactly that, and interpolating them adjacently keeps it. */\n return html`${this.renderPrompt()}${this.renderTextarea()} ${this.renderValidationMessage()}`;\n }\n\n protected textChanged(event: Event) {\n if (this.disabled || this.readonly) return;\n const input = event.target as HTMLInputElement;\n if (this.response !== input.value) {\n this.value = input.value;\n this.saveResponse(input.value);\n }\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'qti-extended-text-interaction': QtiExtendedTextInteraction;\n }\n}\n"]}
@@ -1,2 +1,2 @@
1
- declare const _default: import("lit").CSSResult;
1
+ declare const _default: import("lit").CSSResult[];
2
2
  export default _default;
@@ -1,14 +1,20 @@
1
1
  import { css } from 'lit';
2
- export default css `
3
- /* PK: display host as block, else design will be collapsed */
4
- :host {
5
- display: block;
6
- }
7
- textarea {
8
- box-sizing: border-box;
9
- width: 100%;
10
- height: 100%;
11
- border: 0;
12
- }
13
- `;
2
+ import { boxSizing } from '@qti-components/base';
3
+ export default [
4
+ boxSizing,
5
+ css `
6
+ /* PK: display host as block, else design will be collapsed */
7
+ /* 'position: relative' anchors the correction glyph the theme draws over the textarea. */
8
+ :host {
9
+ display: block;
10
+ position: relative;
11
+ }
12
+ textarea {
13
+ box-sizing: border-box;
14
+ width: 100%;
15
+ height: 100%;
16
+ border: 0;
17
+ }
18
+ `
19
+ ];
14
20
  //# sourceMappingURL=qti-extended-text-interaction.styles.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"qti-extended-text-interaction.styles.js","sourceRoot":"","sources":["../src/qti-extended-text-interaction.styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE1B,eAAe,GAAG,CAAA;;;;;;;;;;;CAWjB,CAAC","sourcesContent":["import { css } from 'lit';\n\nexport default css`\n /* PK: display host as block, else design will be collapsed */\n :host {\n display: block;\n }\n textarea {\n box-sizing: border-box;\n width: 100%;\n height: 100%;\n border: 0;\n }\n`;\n"]}
1
+ {"version":3,"file":"qti-extended-text-interaction.styles.js","sourceRoot":"","sources":["../src/qti-extended-text-interaction.styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE1B,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD,eAAe;IACb,SAAS;IACT,GAAG,CAAA;;;;;;;;;;;;;GAaF;CACF,CAAC","sourcesContent":["import { css } from 'lit';\n\nimport { boxSizing } from '@qti-components/base';\n\nexport default [\n boxSizing,\n css`\n /* PK: display host as block, else design will be collapsed */\n /* 'position: relative' anchors the correction glyph the theme draws over the textarea. */\n :host {\n display: block;\n position: relative;\n }\n textarea {\n box-sizing: border-box;\n width: 100%;\n height: 100%;\n border: 0;\n }\n `\n];\n"]}
package/dist/register.js CHANGED
@@ -1,3 +1,7 @@
1
- import { QtiExtendedTextInteraction } from './qti-extended-text-interaction';
2
- customElements.define('qti-extended-text-interaction', QtiExtendedTextInteraction);
1
+ import { elements } from './elements';
2
+ for (const { tag, ctor } of elements) {
3
+ if (!customElements.get(tag)) {
4
+ customElements.define(tag, ctor);
5
+ }
6
+ }
3
7
  //# sourceMappingURL=register.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"register.js","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAC7E,cAAc,CAAC,MAAM,CAAC,+BAA+B,EAAE,0BAA0B,CAAC,CAAC","sourcesContent":["import { QtiExtendedTextInteraction } from './qti-extended-text-interaction';\ncustomElements.define('qti-extended-text-interaction', QtiExtendedTextInteraction);\n"]}
1
+ {"version":3,"file":"register.js","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,KAAK,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,QAAQ,EAAE,CAAC;IACrC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QAC7B,cAAc,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACnC,CAAC;AACH,CAAC","sourcesContent":["import { elements } from './elements';\n\nfor (const { tag, ctor } of elements) {\n if (!customElements.get(tag)) {\n customElements.define(tag, ctor);\n }\n}\n"]}
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@qti-components/extended-text-interaction",
3
3
  "description": "QTI extended-text-interaction interaction",
4
- "version": "1.1.4",
4
+ "version": "2.0.0-next.0",
5
5
  "author": "",
6
6
  "dependencies": {
7
- "@qti-components/base": "^1.4.3",
8
- "@qti-components/interactions-core": "^1.1.4",
7
+ "@qti-components/base": "^2.4.0",
8
+ "@qti-components/interactions-core": "^2.1.3",
9
9
  "@qti-components/utilities": "^1.3.3"
10
10
  },
11
11
  "devDependencies": {
12
12
  "@lit/context": "^1.1.6",
13
- "lit": "^3.3.2"
13
+ "lit": "^3.3.1"
14
14
  },
15
15
  "keywords": [
16
16
  "interactions",
@@ -45,6 +45,10 @@
45
45
  "./styles": {
46
46
  "types": "./dist/qti-extended-text-interaction.styles.d.ts",
47
47
  "import": "./dist/qti-extended-text-interaction.styles.js"
48
+ },
49
+ "./elements": {
50
+ "types": "./dist/elements.d.ts",
51
+ "import": "./dist/elements.js"
48
52
  }
49
53
  },
50
54
  "repository": {
@@ -54,7 +58,9 @@
54
58
  },
55
59
  "scripts": {
56
60
  "attw": "attw --profile esm-only --pack",
57
- "build": "tsc --paths null",
58
- "clean": "rm -rf dist"
61
+ "build": "pnpm run clean && tsc --paths null",
62
+ "clean": "rm -rf dist",
63
+ "yalc": "yalc publish",
64
+ "yalc:push": "yalc push"
59
65
  }
60
66
  }