@qti-components/extended-text-interaction 1.1.5 → 2.0.0-next.1

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.
@@ -26,7 +26,9 @@ import type { CSSResultGroup } from 'lit';
26
26
  * element renders a single textarea bound to one variable, so there is nothing for the bounds
27
27
  * to count: reading them as a word or paragraph limit would invent a rule QTI does not state.
28
28
  * Implement alongside multi-string rendering, not before it.
29
- * @attr {number} min-strings - Not implemented. See `max-strings`.
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.
30
32
  * @attr {'plain'|'preformatted'|'xhtml'} [format=plain] - How the response is presented.
31
33
  * `preformatted` renders the field monospaced; `xhtml` is accepted but downgraded to plain,
32
34
  * because the field is a textarea with no rich-text editor behind it.
@@ -53,6 +55,8 @@ export declare class QtiExtendedTextInteraction extends Interaction {
53
55
  */
54
56
  format: 'plain' | 'preformatted' | 'xhtml';
55
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;
56
60
  patternMask: string;
57
61
  /** text appearing in the extended-text-interaction if it is empty */
58
62
  placeholderText: string;
@@ -47,7 +47,9 @@ let warnedAboutXhtml = false;
47
47
  * element renders a single textarea bound to one variable, so there is nothing for the bounds
48
48
  * to count: reading them as a word or paragraph limit would invent a rule QTI does not state.
49
49
  * Implement alongside multi-string rendering, not before it.
50
- * @attr {number} min-strings - Not implemented. See `max-strings`.
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.
51
53
  * @attr {'plain'|'preformatted'|'xhtml'} [format=plain] - How the response is presented.
52
54
  * `preformatted` renders the field monospaced; `xhtml` is accepted but downgraded to plain,
53
55
  * because the field is a textarea with no rich-text editor behind it.
@@ -82,6 +84,8 @@ export class QtiExtendedTextInteraction extends Interaction {
82
84
  console.warn('<qti-extended-text-interaction format="xhtml"> renders as plain text. ' +
83
85
  'The interaction has no rich-text editor; the recorded response will contain no markup.');
84
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;
85
89
  /*
86
90
  * Declared with `attribute: 'response'`, not `@state`, so `response="…"` in markup reaches the
87
91
  * field. As a `@state` it was unobserved: every sibling interaction accepts the attribute — it
@@ -168,7 +172,7 @@ export class QtiExtendedTextInteraction extends Interaction {
168
172
  else {
169
173
  isValid = textarea.checkValidity();
170
174
  }
171
- if (isValid && !this.response) {
175
+ if (isValid && !this.response && this.minStrings >= 1) {
172
176
  isValid = false;
173
177
  }
174
178
  if (!isValid && !validityMessage) {
@@ -253,6 +257,9 @@ __decorate([
253
257
  __decorate([
254
258
  watch('format')
255
259
  ], QtiExtendedTextInteraction.prototype, "_handleFormatChange", void 0);
260
+ __decorate([
261
+ property({ type: Number, attribute: 'min-strings' })
262
+ ], QtiExtendedTextInteraction.prototype, "minStrings", void 0);
256
263
  __decorate([
257
264
  property({ type: String, attribute: 'pattern-mask' })
258
265
  ], QtiExtendedTextInteraction.prototype, "patternMask", void 0);
@@ -1 +1 @@
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
+ {"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=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"]}
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.5",
4
+ "version": "2.0.0-next.1",
5
5
  "author": "",
6
6
  "dependencies": {
7
- "@qti-components/base": "^2.0.0",
8
- "@qti-components/interactions-core": "^2.0.0",
7
+ "@qti-components/base": "^2.4.1-next.0",
8
+ "@qti-components/interactions-core": "^2.1.4-next.0",
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",
@@ -58,7 +58,7 @@
58
58
  },
59
59
  "scripts": {
60
60
  "attw": "attw --profile esm-only --pack",
61
- "build": "tsc --paths null",
61
+ "build": "pnpm run clean && tsc --paths null",
62
62
  "clean": "rm -rf dist",
63
63
  "yalc": "yalc publish",
64
64
  "yalc:push": "yalc push"