@qti-components/extended-text-interaction 1.1.3 → 1.1.5

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,78 @@
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 - Not implemented. See `max-strings`.
30
+ * @attr {'plain'|'preformatted'|'xhtml'} [format=plain] - How the response is presented.
31
+ * `preformatted` renders the field monospaced; `xhtml` is accepted but downgraded to plain,
32
+ * because the field is a textarea with no rich-text editor behind it.
33
+ *
34
+ * @slot prompt - The prompt shown above the textarea.
35
+ *
36
+ * @csspart textarea - The textarea input element.
37
+ */
3
38
  export declare class QtiExtendedTextInteraction extends Interaction {
39
+ #private;
4
40
  static styles: CSSResultGroup;
5
41
  protected _rows: number;
6
42
  /** expected length is mapped to the property maxlength on the textarea */
7
43
  expectedLength: number;
44
+ /** QTI's own hint at the response height. Outranked by `qti-height-lines-*`; see `#resolveRows`. */
45
+ expectedLines: number;
46
+ /**
47
+ * How the response text is presented. Reflected so themes can key off
48
+ * `:host([format='preformatted'])`.
49
+ *
50
+ * `xhtml` is accepted and recorded but renders as `plain`: the field is a `<textarea>`, and
51
+ * there is no rich-text editor behind it. Downgrading loudly beats either refusing valid QTI
52
+ * or pretending to support formatting the candidate cannot actually produce.
53
+ */
54
+ format: 'plain' | 'preformatted' | 'xhtml';
55
+ protected _handleFormatChange: () => void;
8
56
  patternMask: string;
9
57
  /** text appearing in the extended-text-interaction if it is empty */
10
58
  placeholderText: string;
11
59
  dataPatternmaskMessage: string;
12
60
  classNames: string;
13
- handleclassNamesChange(_: any, classes: string): void;
61
+ protected _handleRowsInputChange(): void;
14
62
  response: string | null;
15
63
  protected _handleResponseChange: () => void;
16
64
  get value(): string | null;
17
65
  set value(val: string | null);
18
66
  validate(): boolean;
19
- toggleCorrectResponse(): void;
20
67
  reportValidity(): boolean;
68
+ /** The prompt, above the field. */
69
+ protected renderPrompt(): unknown;
70
+ /** The field itself. */
71
+ protected renderTextarea(): unknown;
72
+ /** Hidden until `Interaction.reportValidity` shows it. */
73
+ protected renderValidationMessage(): unknown;
21
74
  render(): import("lit").TemplateResult<1>;
22
75
  protected textChanged(event: Event): void;
23
- protected setEmptyAttribute(text: string): void;
24
76
  }
25
77
  declare global {
26
78
  interface HTMLElementTagNameMap {
@@ -10,10 +10,90 @@ 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 - Not implemented. See `max-strings`.
51
+ * @attr {'plain'|'preformatted'|'xhtml'} [format=plain] - How the response is presented.
52
+ * `preformatted` renders the field monospaced; `xhtml` is accepted but downgraded to plain,
53
+ * because the field is a textarea with no rich-text editor behind it.
54
+ *
55
+ * @slot prompt - The prompt shown above the textarea.
56
+ *
57
+ * @csspart textarea - The textarea input element.
58
+ */
13
59
  export class QtiExtendedTextInteraction extends Interaction {
14
60
  constructor() {
15
61
  super(...arguments);
16
- this._rows = 5;
62
+ this._rows = DEFAULT_ROWS;
63
+ /**
64
+ * How the response text is presented. Reflected so themes can key off
65
+ * `:host([format='preformatted'])`.
66
+ *
67
+ * `xhtml` is accepted and recorded but renders as `plain`: the field is a `<textarea>`, and
68
+ * there is no rich-text editor behind it. Downgrading loudly beats either refusing valid QTI
69
+ * or pretending to support formatting the candidate cannot actually produce.
70
+ */
71
+ /*
72
+ * The vocabulary is written inline rather than as an exported `TextFormat` alias. Both text
73
+ * interactions need it in their public API, and the umbrella `@qti-components/interactions`
74
+ * re-exports both packages — two identically-named exported aliases collide at that barrel
75
+ * (TS2308). Inline, there is no name to collide, and the manifest still reads the union.
76
+ */
77
+ this.format = 'plain';
78
+ this._handleFormatChange = () => {
79
+ if (this.format !== 'xhtml' || warnedAboutXhtml)
80
+ return;
81
+ warnedAboutXhtml = true;
82
+ console.warn('<qti-extended-text-interaction format="xhtml"> renders as plain text. ' +
83
+ 'The interaction has no rich-text editor; the recorded response will contain no markup.');
84
+ };
85
+ /*
86
+ * Declared with `attribute: 'response'`, not `@state`, so `response="…"` in markup reaches the
87
+ * field. As a `@state` it was unobserved: every sibling interaction accepts the attribute — it
88
+ * is half of the codec's contract in packages/qti-base/src/lib/response.ts, which parses
89
+ * `response` and `correct-response` alike — but this one silently ignored it, so authored or
90
+ * restored prose rendered as an empty textarea.
91
+ *
92
+ * Plain `type: String`, deliberately NOT `responseAttributeConverter`. That codec splits on
93
+ * commas to build multi-value responses, which is right for identifiers, pairs and points and
94
+ * wrong for prose: an essay containing a comma would arrive as an array. text-entry, the other
95
+ * single-string interaction, declares it plainly for the same reason.
96
+ */
17
97
  this.response = null;
18
98
  this._handleResponseChange = () => {
19
99
  this._internals.setFormValue(this.value);
@@ -21,21 +101,45 @@ export class QtiExtendedTextInteraction extends Interaction {
21
101
  };
22
102
  }
23
103
  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;
104
+ /*
105
+ * All three inputs feed one resolver rather than each assigning `_rows` in its own handler.
106
+ * With separate handlers the last attribute to change would win, so the same markup could
107
+ * render a different height depending on attribute order — and `expected-length` only ever
108
+ * applied when a `class` was also present, because the fallback lived inside the class watcher.
109
+ */
110
+ _handleRowsInputChange() {
111
+ this._rows = this.#resolveRows();
112
+ }
113
+ /**
114
+ * Row count, highest precedence first:
115
+ *
116
+ * 1. `qti-height-lines-N` — the QTI shared presentation vocabulary. An explicit rendering
117
+ * instruction, so it outranks the content-level hints below.
118
+ * 2. `expected-lines` — QTI's hint at how long the response should be.
119
+ * 3. `expected-length` — a character count, converted at a rough characters-per-row rate.
120
+ * 4. {@link DEFAULT_ROWS}.
121
+ */
122
+ #resolveRows() {
123
+ const fromHeightLines = this.#rowsFromHeightLinesClass();
124
+ if (fromHeightLines !== null)
125
+ return fromHeightLines;
126
+ if (this.expectedLines > 0)
127
+ return this.expectedLines;
128
+ if (this.expectedLength > 0)
129
+ return Math.ceil(this.expectedLength / CHARS_PER_ROW);
130
+ return DEFAULT_ROWS;
131
+ }
132
+ /** The `N` of the first usable `qti-height-lines-N` class, or null when none is present. */
133
+ #rowsFromHeightLinesClass() {
134
+ for (const className of (this.classNames ?? '').split(' ')) {
135
+ if (!className.startsWith(HEIGHT_LINES_PREFIX))
136
+ continue;
137
+ const rows = parseInt(className.slice(HEIGHT_LINES_PREFIX.length), 10);
138
+ // A bare `qti-height-lines-` parses to NaN; ignore it rather than render NaN rows.
139
+ if (!Number.isNaN(rows) && rows > 0)
140
+ return rows;
38
141
  }
142
+ return null;
39
143
  }
40
144
  get value() {
41
145
  return this.response || null;
@@ -47,73 +151,92 @@ export class QtiExtendedTextInteraction extends Interaction {
47
151
  const textarea = this.shadowRoot.querySelector('textarea');
48
152
  if (!textarea)
49
153
  return false;
154
+ let validityMessage = '';
155
+ let isValid = false;
50
156
  if (this.patternMask && this.dataPatternmaskMessage) {
51
157
  // Clear any custom error initially
52
- this._internals.setValidity({});
53
158
  textarea.setCustomValidity('');
54
159
  const patternSource = this.patternMask.startsWith('^') && this.patternMask.endsWith('$') ? this.patternMask : `^${this.patternMask}$`;
55
160
  const pattern = new RegExp(patternSource);
56
- const isValid = textarea.checkValidity() && pattern.test(textarea.value);
161
+ isValid = textarea.checkValidity() && pattern.test(textarea.value);
57
162
  if (!isValid) {
58
163
  // Set custom error if invalid
59
- this._internals.setValidity({ customError: true }, this.dataPatternmaskMessage);
164
+ validityMessage = this.dataPatternmaskMessage;
60
165
  textarea.setCustomValidity(this.dataPatternmaskMessage);
61
166
  }
62
167
  }
63
168
  else {
64
- const isValid = textarea.checkValidity();
65
- this._internals.setValidity(isValid ? {} : { customError: false });
169
+ isValid = textarea.checkValidity();
66
170
  }
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();
171
+ if (isValid && !this.response) {
172
+ isValid = false;
173
+ }
174
+ if (!isValid && !validityMessage) {
175
+ validityMessage = textarea.validationMessage || 'Invalid value.';
80
176
  }
177
+ this.setInteractionValidity(isValid, validityMessage, textarea, { suppressInline: true });
81
178
  return isValid;
82
179
  }
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="${(_) => {
180
+ reportValidity() {
181
+ this.validate();
182
+ return super.reportValidity();
183
+ }
184
+ /*
185
+ * The template in named pieces, so a subclass can recompose it.
186
+ *
187
+ * Each piece renders one thing and returns it; `render()` below just orders them. A subclass
188
+ * overrides `render()` and calls the pieces where it wants them — which is the point: an extension
189
+ * point that only lets you APPEND has already decided the layout on the subclass's behalf, and the
190
+ * subclass is usually the one that knows better. The correction variant in
191
+ * @qti-components/corrections puts its badge between the field and the validation message;
192
+ * something else may want it above the prompt, or may want to drop a piece entirely.
193
+ *
194
+ * Override a piece to change what one part looks like; override `render()` to change the order.
195
+ */
196
+ /** The prompt, above the field. */
197
+ renderPrompt() {
198
+ return html `<slot name="prompt"></slot>`;
199
+ }
200
+ /** The field itself. */
201
+ renderTextarea() {
202
+ return html `<textarea
203
+ part="textarea"
204
+ name="${this.responseIdentifier}"
205
+ spellcheck="false"
206
+ autocomplete="off"
207
+ maxlength="${5000}"
208
+ @keydown="${(event) => event.stopImmediatePropagation()}"
209
+ @keyup="${this.textChanged}"
210
+ @change="${this.textChanged}"
211
+ @blur="${(_) => {
95
212
  this.reportValidity();
96
213
  }}"
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>`;
214
+ placeholder="${ifDefined(this.placeholderText ? this.placeholderText : undefined)}"
215
+ rows="${this._rows}"
216
+ ?disabled="${this.disabled}"
217
+ ?readonly="${this.readonly}"
218
+ .value=${this.response}
219
+ ></textarea>`;
220
+ }
221
+ /** Hidden until `Interaction.reportValidity` shows it. */
222
+ renderValidationMessage() {
223
+ return html `<div id="validation-message" part="message" role="alert" style="display:none;"></div>`;
224
+ }
225
+ render() {
226
+ /* The prompt and the field are written with NO whitespace between them, deliberately: a text
227
+ node there puts a line box between the two. The original template said `</slot\n><textarea`
228
+ to avoid exactly that, and interpolating them adjacently keeps it. */
229
+ return html `${this.renderPrompt()}${this.renderTextarea()} ${this.renderValidationMessage()}`;
103
230
  }
104
231
  textChanged(event) {
105
232
  if (this.disabled || this.readonly)
106
233
  return;
107
234
  const input = event.target;
108
- this.setEmptyAttribute(input.value);
109
235
  if (this.response !== input.value) {
110
236
  this.value = input.value;
111
237
  this.saveResponse(input.value);
112
238
  }
113
239
  }
114
- setEmptyAttribute(text) {
115
- this.setAttribute('empty', text === '' ? 'true' : 'false');
116
- }
117
240
  }
118
241
  __decorate([
119
242
  state()
@@ -121,6 +244,15 @@ __decorate([
121
244
  __decorate([
122
245
  property({ type: Number, attribute: 'expected-length' })
123
246
  ], QtiExtendedTextInteraction.prototype, "expectedLength", void 0);
247
+ __decorate([
248
+ property({ type: Number, attribute: 'expected-lines' })
249
+ ], QtiExtendedTextInteraction.prototype, "expectedLines", void 0);
250
+ __decorate([
251
+ property({ type: String, reflect: true })
252
+ ], QtiExtendedTextInteraction.prototype, "format", void 0);
253
+ __decorate([
254
+ watch('format')
255
+ ], QtiExtendedTextInteraction.prototype, "_handleFormatChange", void 0);
124
256
  __decorate([
125
257
  property({ type: String, attribute: 'pattern-mask' })
126
258
  ], QtiExtendedTextInteraction.prototype, "patternMask", void 0);
@@ -134,10 +266,10 @@ __decorate([
134
266
  property({ type: String, attribute: 'class' })
135
267
  ], QtiExtendedTextInteraction.prototype, "classNames", void 0);
136
268
  __decorate([
137
- watch('classNames')
138
- ], QtiExtendedTextInteraction.prototype, "handleclassNamesChange", null);
269
+ watch(['classNames', 'expectedLines', 'expectedLength'])
270
+ ], QtiExtendedTextInteraction.prototype, "_handleRowsInputChange", null);
139
271
  __decorate([
140
- state()
272
+ property({ type: String, attribute: 'response', reflect: false })
141
273
  ], QtiExtendedTextInteraction.prototype, "response", void 0);
142
274
  __decorate([
143
275
  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":"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","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 - Not implemented. See `max-strings`.\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 @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) {\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,12 +1,12 @@
1
1
  {
2
2
  "name": "@qti-components/extended-text-interaction",
3
3
  "description": "QTI extended-text-interaction interaction",
4
- "version": "1.1.3",
4
+ "version": "1.1.5",
5
5
  "author": "",
6
6
  "dependencies": {
7
- "@qti-components/base": "^1.4.2",
8
- "@qti-components/interactions-core": "^1.1.3",
9
- "@qti-components/utilities": "^1.3.2"
7
+ "@qti-components/base": "^2.0.0",
8
+ "@qti-components/interactions-core": "^2.0.0",
9
+ "@qti-components/utilities": "^1.3.3"
10
10
  },
11
11
  "devDependencies": {
12
12
  "@lit/context": "^1.1.6",
@@ -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": {
@@ -55,6 +59,8 @@
55
59
  "scripts": {
56
60
  "attw": "attw --profile esm-only --pack",
57
61
  "build": "tsc --paths null",
58
- "clean": "rm -rf dist"
62
+ "clean": "rm -rf dist",
63
+ "yalc": "yalc publish",
64
+ "yalc:push": "yalc push"
59
65
  }
60
66
  }