@qti-components/graphic-gap-match-interaction 1.1.6 → 1.1.7

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.
@@ -15,6 +15,20 @@ declare const QtiGraphicGapMatchInteraction_base: (abstract new (...args: any[])
15
15
  export declare class QtiGraphicGapMatchInteraction extends QtiGraphicGapMatchInteraction_base {
16
16
  #private;
17
17
  static styles: CSSResultGroup;
18
+ /**
19
+ * A hotspot's box is its authored `coords`, always (CONFIGURABILITY.md §4). The measured
20
+ * `--qti-dropzone-min-*` reservation is a floor on the hotspot's `min-width`/`min-height`, and it
21
+ * beat the percentage box from `positionShapes` as soon as the graphic rendered narrower than its
22
+ * declared width: a 275px chip kept a 17% hotspot at 275px over a 482px map. Chips are sized to
23
+ * their hotspots through `--qti-graphic-scale` instead.
24
+ */
25
+ autoSizeDropzones: boolean;
26
+ /**
27
+ * Rendered width of the graphic over its declared width: the factor the drop areas are scaled by.
28
+ * Published as `--qti-graphic-scale` on both slots, so it reaches a chip in the bank and one placed
29
+ * in a hotspot (qti-gap-img.styles.ts), without writing to the light-DOM host.
30
+ */
31
+ private _graphicScale;
18
32
  render(): import("lit").TemplateResult<1>;
19
33
  shouldReturnToInventoryOnInventoryDrop(): boolean;
20
34
  connectedCallback(): void;
@@ -1,4 +1,11 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
1
7
  import { html } from 'lit';
8
+ import { state } from 'lit/decorators.js';
2
9
  import { Interaction } from '@qti-components/base';
3
10
  import { positionShapes } from '@qti-components/interactions-core/internal/hotspots/hotspot';
4
11
  import { DragDropSlottedMixin, DragDropSlottedSortableMixin } from '@qti-components/interactions-core/mixins/drag-drop-observables';
@@ -16,11 +23,36 @@ const SlottedBase = DragDropSlottedMixin(Interaction, 'qti-gap-img, qti-gap-text
16
23
  * @csspart message - Live validation message region (role="alert").
17
24
  */
18
25
  export class QtiGraphicGapMatchInteraction extends DragDropSlottedSortableMixin(SlottedBase, '[qti-draggable="true"]') {
26
+ constructor() {
27
+ super(...arguments);
28
+ /**
29
+ * A hotspot's box is its authored `coords`, always (CONFIGURABILITY.md §4). The measured
30
+ * `--qti-dropzone-min-*` reservation is a floor on the hotspot's `min-width`/`min-height`, and it
31
+ * beat the percentage box from `positionShapes` as soon as the graphic rendered narrower than its
32
+ * declared width: a 275px chip kept a 17% hotspot at 275px over a 482px map. Chips are sized to
33
+ * their hotspots through `--qti-graphic-scale` instead.
34
+ */
35
+ this.autoSizeDropzones = false;
36
+ /**
37
+ * Rendered width of the graphic over its declared width: the factor the drop areas are scaled by.
38
+ * Published as `--qti-graphic-scale` on both slots, so it reaches a chip in the bank and one placed
39
+ * in a hotspot (qti-gap-img.styles.ts), without writing to the light-DOM host.
40
+ */
41
+ this._graphicScale = 1;
42
+ this.#graphicObserver = null;
43
+ this.#observedGraphic = null;
44
+ this.#positionHotspotOnRegister = (e) => {
45
+ this.#positionHotspot(e.target);
46
+ };
47
+ }
19
48
  static { this.styles = styles; }
49
+ #graphicObserver;
50
+ #observedGraphic;
20
51
  render() {
52
+ const scale = `--qti-graphic-scale: ${this._graphicScale}`;
21
53
  return html ` <slot name="prompt"></slot>
22
- <slot part="image"></slot>
23
- <slot part="drags" name="drags" class="hover-border"></slot>
54
+ <slot part="image" style=${scale}></slot>
55
+ <slot part="drags" name="drags" class="hover-border" style=${scale}></slot>
24
56
  <div role="alert" part="message" id="validation-message"></div>`;
25
57
  }
26
58
  shouldReturnToInventoryOnInventoryDrop() {
@@ -68,11 +100,31 @@ export class QtiGraphicGapMatchInteraction extends DragDropSlottedSortableMixin(
68
100
  if (!graphic || !coords || !shape)
69
101
  return;
70
102
  this.#keepDeclaredRatio(graphic);
103
+ this.#observeGraphic(graphic);
71
104
  positionShapes(shape, coords.split(',').map(s => parseFloat(s)), graphic, hotspot);
72
105
  }
73
- #positionHotspotOnRegister = (e) => {
74
- this.#positionHotspot(e.target);
75
- };
106
+ /**
107
+ * Tracks the graphic's rendered width. The authored chip sizes live in the same coordinate space
108
+ * as the `coords` — an item that draws a 275x126 hotspot for a 275x126 tile means the tile to fill
109
+ * it — so the chips scale by the same factor as the drop areas.
110
+ */
111
+ #observeGraphic(graphic) {
112
+ if (this.#observedGraphic === graphic)
113
+ return;
114
+ this.#graphicObserver?.disconnect();
115
+ this.#observedGraphic = graphic;
116
+ this.#graphicObserver = new ResizeObserver(() => {
117
+ const declared = parseFloat(graphic.getAttribute('width') ?? '');
118
+ const rendered = graphic.getBoundingClientRect().width;
119
+ const scale = declared > 0 && rendered > 0 ? Math.min(1, rendered / declared) : 1;
120
+ // Rounded, so sub-pixel jitter does not re-render on every observation.
121
+ const rounded = Math.round(scale * 1000) / 1000;
122
+ if (rounded !== this._graphicScale)
123
+ this._graphicScale = rounded;
124
+ });
125
+ this.#graphicObserver.observe(graphic);
126
+ }
127
+ #positionHotspotOnRegister;
76
128
  connectedCallback() {
77
129
  super.connectedCallback();
78
130
  this.addEventListener('qti-register-hotspot', this.#positionHotspotOnRegister);
@@ -83,6 +135,12 @@ export class QtiGraphicGapMatchInteraction extends DragDropSlottedSortableMixin(
83
135
  disconnectedCallback() {
84
136
  super.disconnectedCallback();
85
137
  this.removeEventListener('qti-register-hotspot', this.#positionHotspotOnRegister);
138
+ this.#graphicObserver?.disconnect();
139
+ this.#graphicObserver = null;
140
+ this.#observedGraphic = null;
86
141
  }
87
142
  }
143
+ __decorate([
144
+ state()
145
+ ], QtiGraphicGapMatchInteraction.prototype, "_graphicScale", void 0);
88
146
  //# sourceMappingURL=qti-graphic-gap-match-interaction.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"qti-graphic-gap-match-interaction.js","sourceRoot":"","sources":["../src/qti-graphic-gap-match-interaction.ts"],"names":[],"mappings":"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","sourcesContent":["import { html } from 'lit';\n\nimport { Interaction } from '@qti-components/base';\nimport { positionShapes } from '@qti-components/interactions-core/internal/hotspots/hotspot';\nimport {\n DragDropSlottedMixin,\n DragDropSlottedSortableMixin\n} from '@qti-components/interactions-core/mixins/drag-drop-observables';\n\nimport styles from './qti-graphic-gap-match-interaction.styles';\n\nimport type { Graphic } from '@qti-components/interactions-core/internal/hotspots/hotspot';\nimport type { CSSResultGroup } from 'lit';\nconst SlottedBase = DragDropSlottedMixin(\n Interaction,\n 'qti-gap-img, qti-gap-text',\n 'qti-associable-hotspot',\n `slot[part~='drags']`\n);\n\n/**\n * Graphic gap-match interaction: candidates drag choices onto image hotspots.\n *\n * @slot prompt - The prompt shown above the image.\n * @slot drags - The draggable choice sources.\n * @slot - Default slot for the base image and hotspots.\n *\n * @csspart image - Wrapper around the image slot.\n * @csspart drags - Wrapper around the drag sources slot.\n * @csspart message - Live validation message region (role=\"alert\").\n */\nexport class QtiGraphicGapMatchInteraction extends DragDropSlottedSortableMixin(SlottedBase, '[qti-draggable=\"true\"]') {\n static override styles: CSSResultGroup = styles;\n\n override render() {\n return html` <slot name=\"prompt\"></slot>\n <slot part=\"image\"></slot>\n <slot part=\"drags\" name=\"drags\" class=\"hover-border\"></slot>\n <div role=\"alert\" part=\"message\" id=\"validation-message\"></div>`;\n }\n\n public override shouldReturnToInventoryOnInventoryDrop(): boolean {\n return true;\n }\n\n /**\n * The background graphic: a direct child, as QTI places it. Not `findGraphic(this)`, which\n * searches every descendant and would pick up a `qti-gap-img`'s picture (or one in the prompt)\n * when it comes first.\n */\n #graphic(): Graphic | null {\n return this.querySelector<Graphic>(':scope > img, :scope > object[type^=\"image\"]');\n }\n\n /**\n * Pins the graphic's box to the aspect ratio its width/height attributes declare: the coordinate\n * space the drop areas are percentages of. With the fluid `height: auto` (see the styles) the box\n * would otherwise follow the bitmap — an `<object>` gets no ratio from its attributes at all, and\n * an `<img>` only `auto w / h`, where the loaded bitmap's own ratio wins. Either way a picture\n * whose pixels differ from its declared size would slide out from under its drop areas. Pinned,\n * it fills the declared box as it does at its fixed size.\n */\n #keepDeclaredRatio(graphic: Graphic): void {\n if (graphic.style.aspectRatio) return;\n const length = (name: 'width' | 'height') => {\n const raw = (graphic.getAttribute(name) ?? '').trim();\n const value = parseFloat(raw);\n return raw.endsWith('%') || !(value > 0) ? null : value;\n };\n const width = length('width');\n const height = length('height');\n if (width && height) graphic.style.aspectRatio = `${width} / ${height}`;\n }\n\n /**\n * Places a drop area over the graphic with `positionShapes`, the hotspot interaction's helper:\n * percentages of the coordinate space the `coords` were authored in, so the areas scale with the\n * graphic when it is narrower than its declared width (see the styles). They used to be written\n * in pixels, which only held while the graphic rendered at exactly that width.\n */\n #positionHotspot(hotspot: HTMLElement): void {\n const graphic = this.#graphic();\n const coords = hotspot.getAttribute('coords');\n const shape = hotspot.getAttribute('shape');\n if (!graphic || !coords || !shape) return;\n this.#keepDeclaredRatio(graphic);\n positionShapes(\n shape,\n coords.split(',').map(s => parseFloat(s)),\n graphic,\n hotspot\n );\n }\n\n #positionHotspotOnRegister = (e: Event): void => {\n this.#positionHotspot(e.target as HTMLElement);\n };\n\n override connectedCallback(): void {\n super.connectedCallback();\n this.addEventListener('qti-register-hotspot', this.#positionHotspotOnRegister);\n // A hotspot that connected before this element was upgraded has already dispatched its\n // register event (the hotspot interaction has the same catch-up).\n this.querySelectorAll<HTMLElement>(':scope > qti-associable-hotspot').forEach(hotspot =>\n this.#positionHotspot(hotspot)\n );\n }\n override disconnectedCallback() {\n super.disconnectedCallback();\n this.removeEventListener('qti-register-hotspot', this.#positionHotspotOnRegister);\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'qti-graphic-gap-match-interaction': QtiGraphicGapMatchInteraction;\n }\n}\n"]}
1
+ {"version":3,"file":"qti-graphic-gap-match-interaction.js","sourceRoot":"","sources":["../src/qti-graphic-gap-match-interaction.ts"],"names":[],"mappings":"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","sourcesContent":["import { html } from 'lit';\nimport { state } from 'lit/decorators.js';\n\nimport { Interaction } from '@qti-components/base';\nimport { positionShapes } from '@qti-components/interactions-core/internal/hotspots/hotspot';\nimport {\n DragDropSlottedMixin,\n DragDropSlottedSortableMixin\n} from '@qti-components/interactions-core/mixins/drag-drop-observables';\n\nimport styles from './qti-graphic-gap-match-interaction.styles';\n\nimport type { Graphic } from '@qti-components/interactions-core/internal/hotspots/hotspot';\nimport type { CSSResultGroup } from 'lit';\nconst SlottedBase = DragDropSlottedMixin(\n Interaction,\n 'qti-gap-img, qti-gap-text',\n 'qti-associable-hotspot',\n `slot[part~='drags']`\n);\n\n/**\n * Graphic gap-match interaction: candidates drag choices onto image hotspots.\n *\n * @slot prompt - The prompt shown above the image.\n * @slot drags - The draggable choice sources.\n * @slot - Default slot for the base image and hotspots.\n *\n * @csspart image - Wrapper around the image slot.\n * @csspart drags - Wrapper around the drag sources slot.\n * @csspart message - Live validation message region (role=\"alert\").\n */\nexport class QtiGraphicGapMatchInteraction extends DragDropSlottedSortableMixin(SlottedBase, '[qti-draggable=\"true\"]') {\n static override styles: CSSResultGroup = styles;\n\n /**\n * A hotspot's box is its authored `coords`, always (CONFIGURABILITY.md §4). The measured\n * `--qti-dropzone-min-*` reservation is a floor on the hotspot's `min-width`/`min-height`, and it\n * beat the percentage box from `positionShapes` as soon as the graphic rendered narrower than its\n * declared width: a 275px chip kept a 17% hotspot at 275px over a 482px map. Chips are sized to\n * their hotspots through `--qti-graphic-scale` instead.\n */\n public override autoSizeDropzones = false;\n\n /**\n * Rendered width of the graphic over its declared width: the factor the drop areas are scaled by.\n * Published as `--qti-graphic-scale` on both slots, so it reaches a chip in the bank and one placed\n * in a hotspot (qti-gap-img.styles.ts), without writing to the light-DOM host.\n */\n @state() private _graphicScale = 1;\n\n #graphicObserver: ResizeObserver | null = null;\n #observedGraphic: Graphic | null = null;\n\n override render() {\n const scale = `--qti-graphic-scale: ${this._graphicScale}`;\n return html` <slot name=\"prompt\"></slot>\n <slot part=\"image\" style=${scale}></slot>\n <slot part=\"drags\" name=\"drags\" class=\"hover-border\" style=${scale}></slot>\n <div role=\"alert\" part=\"message\" id=\"validation-message\"></div>`;\n }\n\n public override shouldReturnToInventoryOnInventoryDrop(): boolean {\n return true;\n }\n\n /**\n * The background graphic: a direct child, as QTI places it. Not `findGraphic(this)`, which\n * searches every descendant and would pick up a `qti-gap-img`'s picture (or one in the prompt)\n * when it comes first.\n */\n #graphic(): Graphic | null {\n return this.querySelector<Graphic>(':scope > img, :scope > object[type^=\"image\"]');\n }\n\n /**\n * Pins the graphic's box to the aspect ratio its width/height attributes declare: the coordinate\n * space the drop areas are percentages of. With the fluid `height: auto` (see the styles) the box\n * would otherwise follow the bitmap — an `<object>` gets no ratio from its attributes at all, and\n * an `<img>` only `auto w / h`, where the loaded bitmap's own ratio wins. Either way a picture\n * whose pixels differ from its declared size would slide out from under its drop areas. Pinned,\n * it fills the declared box as it does at its fixed size.\n */\n #keepDeclaredRatio(graphic: Graphic): void {\n if (graphic.style.aspectRatio) return;\n const length = (name: 'width' | 'height') => {\n const raw = (graphic.getAttribute(name) ?? '').trim();\n const value = parseFloat(raw);\n return raw.endsWith('%') || !(value > 0) ? null : value;\n };\n const width = length('width');\n const height = length('height');\n if (width && height) graphic.style.aspectRatio = `${width} / ${height}`;\n }\n\n /**\n * Places a drop area over the graphic with `positionShapes`, the hotspot interaction's helper:\n * percentages of the coordinate space the `coords` were authored in, so the areas scale with the\n * graphic when it is narrower than its declared width (see the styles). They used to be written\n * in pixels, which only held while the graphic rendered at exactly that width.\n */\n #positionHotspot(hotspot: HTMLElement): void {\n const graphic = this.#graphic();\n const coords = hotspot.getAttribute('coords');\n const shape = hotspot.getAttribute('shape');\n if (!graphic || !coords || !shape) return;\n this.#keepDeclaredRatio(graphic);\n this.#observeGraphic(graphic);\n positionShapes(\n shape,\n coords.split(',').map(s => parseFloat(s)),\n graphic,\n hotspot\n );\n }\n\n /**\n * Tracks the graphic's rendered width. The authored chip sizes live in the same coordinate space\n * as the `coords` — an item that draws a 275x126 hotspot for a 275x126 tile means the tile to fill\n * it — so the chips scale by the same factor as the drop areas.\n */\n #observeGraphic(graphic: Graphic): void {\n if (this.#observedGraphic === graphic) return;\n this.#graphicObserver?.disconnect();\n this.#observedGraphic = graphic;\n this.#graphicObserver = new ResizeObserver(() => {\n const declared = parseFloat(graphic.getAttribute('width') ?? '');\n const rendered = graphic.getBoundingClientRect().width;\n const scale = declared > 0 && rendered > 0 ? Math.min(1, rendered / declared) : 1;\n // Rounded, so sub-pixel jitter does not re-render on every observation.\n const rounded = Math.round(scale * 1000) / 1000;\n if (rounded !== this._graphicScale) this._graphicScale = rounded;\n });\n this.#graphicObserver.observe(graphic);\n }\n\n #positionHotspotOnRegister = (e: Event): void => {\n this.#positionHotspot(e.target as HTMLElement);\n };\n\n override connectedCallback(): void {\n super.connectedCallback();\n this.addEventListener('qti-register-hotspot', this.#positionHotspotOnRegister);\n // A hotspot that connected before this element was upgraded has already dispatched its\n // register event (the hotspot interaction has the same catch-up).\n this.querySelectorAll<HTMLElement>(':scope > qti-associable-hotspot').forEach(hotspot =>\n this.#positionHotspot(hotspot)\n );\n }\n override disconnectedCallback() {\n super.disconnectedCallback();\n this.removeEventListener('qti-register-hotspot', this.#positionHotspotOnRegister);\n this.#graphicObserver?.disconnect();\n this.#graphicObserver = null;\n this.#observedGraphic = null;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'qti-graphic-gap-match-interaction': QtiGraphicGapMatchInteraction;\n }\n}\n"]}
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@qti-components/graphic-gap-match-interaction",
3
3
  "description": "QTI graphic-gap-match-interaction interaction",
4
- "version": "1.1.6",
4
+ "version": "1.1.7",
5
5
  "author": "",
6
6
  "dependencies": {
7
- "@qti-components/base": "^2.4.1",
8
- "@qti-components/interactions-core": "^2.1.3",
7
+ "@qti-components/base": "^2.5.0",
8
+ "@qti-components/interactions-core": "^2.1.4",
9
9
  "@qti-components/utilities": "^1.3.3"
10
10
  },
11
11
  "devDependencies": {