chrometools-mcp 2.4.2 → 3.1.2

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +540 -0
  2. package/COMPONENT_MAPPING_SPEC.md +1217 -0
  3. package/README.md +494 -38
  4. package/bridge/bridge-client.js +472 -0
  5. package/bridge/bridge-service.js +399 -0
  6. package/bridge/install.js +241 -0
  7. package/browser/browser-manager.js +107 -2
  8. package/browser/page-manager.js +226 -69
  9. package/docs/CHROME_EXTENSION.md +219 -0
  10. package/docs/PAGE_OBJECT_MODEL_CONCEPT.md +1756 -0
  11. package/element-finder-utils.js +138 -28
  12. package/extension/background.js +643 -0
  13. package/extension/content.js +715 -0
  14. package/extension/icons/create-icons.js +164 -0
  15. package/extension/icons/icon128.png +0 -0
  16. package/extension/icons/icon16.png +0 -0
  17. package/extension/icons/icon48.png +0 -0
  18. package/extension/manifest.json +58 -0
  19. package/extension/popup/popup.css +437 -0
  20. package/extension/popup/popup.html +102 -0
  21. package/extension/popup/popup.js +415 -0
  22. package/extension/recorder-overlay.css +93 -0
  23. package/figma-tools.js +120 -0
  24. package/index.js +3347 -2518
  25. package/models/BaseInputModel.js +93 -0
  26. package/models/CheckboxGroupModel.js +199 -0
  27. package/models/CheckboxModel.js +103 -0
  28. package/models/ColorInputModel.js +53 -0
  29. package/models/DateInputModel.js +67 -0
  30. package/models/RadioGroupModel.js +126 -0
  31. package/models/RangeInputModel.js +60 -0
  32. package/models/SelectModel.js +97 -0
  33. package/models/TextInputModel.js +34 -0
  34. package/models/TextareaModel.js +59 -0
  35. package/models/TimeInputModel.js +49 -0
  36. package/models/index.js +122 -0
  37. package/package.json +3 -2
  38. package/pom/apom-converter.js +267 -0
  39. package/pom/apom-tree-converter.js +515 -0
  40. package/pom/element-id-generator.js +175 -0
  41. package/recorder/page-object-generator.js +16 -0
  42. package/recorder/scenario-executor.js +80 -2
  43. package/server/tool-definitions.js +839 -656
  44. package/server/tool-groups.js +3 -2
  45. package/server/tool-schemas.js +367 -296
  46. package/server/websocket-bridge.js +447 -0
  47. package/utils/selector-resolver.js +186 -0
  48. package/utils/ui-framework-detector.js +392 -0
@@ -0,0 +1,93 @@
1
+ /**
2
+ * models/BaseInputModel.js
3
+ *
4
+ * Base class for all input element models.
5
+ * Provides common interface for interacting with form elements.
6
+ */
7
+
8
+ export class BaseInputModel {
9
+ constructor(element, page) {
10
+ this.element = element;
11
+ this.page = page;
12
+ }
13
+
14
+ /**
15
+ * Get the input type (to be overridden by subclasses)
16
+ */
17
+ static get inputTypes() {
18
+ return [];
19
+ }
20
+
21
+ /**
22
+ * Check if this model handles the given input type
23
+ */
24
+ static handles(inputType) {
25
+ return this.inputTypes.includes(inputType?.toLowerCase());
26
+ }
27
+
28
+ /**
29
+ * Set value on the element
30
+ * @param {string} value - Value to set
31
+ * @param {object} options - Additional options (delay, clearFirst, etc.)
32
+ */
33
+ async setValue(value, options = {}) {
34
+ throw new Error('setValue must be implemented by subclass');
35
+ }
36
+
37
+ /**
38
+ * Get current value from the element
39
+ */
40
+ async getValue() {
41
+ return await this.element.evaluate(el => el.value);
42
+ }
43
+
44
+ /**
45
+ * Clear the element value
46
+ */
47
+ async clear() {
48
+ await this.element.evaluate(el => {
49
+ el.value = '';
50
+ el.dispatchEvent(new Event('input', { bubbles: true }));
51
+ el.dispatchEvent(new Event('change', { bubbles: true }));
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Focus on the element
57
+ */
58
+ async focus() {
59
+ await this.element.focus();
60
+ }
61
+
62
+ /**
63
+ * Dispatch input and change events (for framework compatibility)
64
+ */
65
+ async dispatchEvents() {
66
+ await this.element.evaluate(el => {
67
+ el.dispatchEvent(new Event('input', { bubbles: true }));
68
+ el.dispatchEvent(new Event('change', { bubbles: true }));
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Get element metadata
74
+ */
75
+ async getMetadata() {
76
+ return await this.element.evaluate(el => ({
77
+ tagName: el.tagName.toLowerCase(),
78
+ type: el.type,
79
+ name: el.name,
80
+ id: el.id,
81
+ disabled: el.disabled,
82
+ required: el.required,
83
+ value: el.value,
84
+ }));
85
+ }
86
+
87
+ /**
88
+ * Get description of the action performed (for logging)
89
+ */
90
+ getActionDescription(value, identifier) {
91
+ return `Set value "${value}" on ${identifier}`;
92
+ }
93
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * models/CheckboxGroupModel.js
3
+ *
4
+ * Model for checkbox groups.
5
+ * Allows selecting multiple options from a group by name, values, or label texts.
6
+ * Supports set (replace all), add, remove, and toggle modes.
7
+ */
8
+
9
+ import { BaseInputModel } from './BaseInputModel.js';
10
+
11
+ export class CheckboxGroupModel extends BaseInputModel {
12
+ /**
13
+ * Create a CheckboxGroupModel
14
+ * @param {string} groupName - The name attribute of the checkbox group
15
+ * @param {Page} page - Puppeteer page
16
+ */
17
+ constructor(groupName, page) {
18
+ super(null, page);
19
+ this.groupName = groupName;
20
+ }
21
+
22
+ /**
23
+ * Get all options in this checkbox group
24
+ */
25
+ async getOptions() {
26
+ return await this.page.evaluate((name) => {
27
+ const inputs = document.querySelectorAll(`input[type="checkbox"][name="${name}"]`);
28
+ return Array.from(inputs).map(input => {
29
+ // Find label text
30
+ let label = null;
31
+ const parentLabel = input.closest('label');
32
+ if (parentLabel) {
33
+ label = parentLabel.textContent?.trim();
34
+ } else if (input.id) {
35
+ const labelFor = document.querySelector(`label[for="${input.id}"]`);
36
+ if (labelFor) {
37
+ label = labelFor.textContent?.trim();
38
+ }
39
+ }
40
+ label = label || input.getAttribute('aria-label') || input.value;
41
+
42
+ return {
43
+ value: input.value,
44
+ label,
45
+ checked: input.checked,
46
+ disabled: input.disabled
47
+ };
48
+ });
49
+ }, this.groupName);
50
+ }
51
+
52
+ /**
53
+ * Get currently selected values
54
+ */
55
+ async getValue() {
56
+ return await this.page.evaluate((name) => {
57
+ const checked = document.querySelectorAll(`input[type="checkbox"][name="${name}"]:checked`);
58
+ return Array.from(checked).map(input => input.value);
59
+ }, this.groupName);
60
+ }
61
+
62
+ /**
63
+ * Set checkbox values
64
+ * @param {string|string[]} values - Value(s) or label text(s) to select
65
+ * @param {object} options - { mode: 'set'|'add'|'remove'|'toggle', by: 'value'|'text'|'auto' }
66
+ */
67
+ async setValue(values, options = {}) {
68
+ const { mode = 'set', by = 'auto' } = options;
69
+ const valueList = Array.isArray(values) ? values : [values];
70
+
71
+ const result = await this.page.evaluate((name, vals, selectMode, selectBy) => {
72
+ const inputs = document.querySelectorAll(`input[type="checkbox"][name="${name}"]`);
73
+ const changes = [];
74
+
75
+ // Helper to check if value matches
76
+ const matches = (input, val) => {
77
+ // Get label text
78
+ let label = null;
79
+ const parentLabel = input.closest('label');
80
+ if (parentLabel) {
81
+ label = parentLabel.textContent?.trim();
82
+ } else if (input.id) {
83
+ const labelFor = document.querySelector(`label[for="${input.id}"]`);
84
+ if (labelFor) {
85
+ label = labelFor.textContent?.trim();
86
+ }
87
+ }
88
+ label = label || input.getAttribute('aria-label');
89
+
90
+ switch (selectBy) {
91
+ case 'value':
92
+ return input.value === val;
93
+ case 'text':
94
+ return label && (label === val || label.toLowerCase().includes(val.toLowerCase()));
95
+ case 'auto':
96
+ default:
97
+ return input.value === val ||
98
+ (label && (label === val || label.toLowerCase().includes(val.toLowerCase())));
99
+ }
100
+ };
101
+
102
+ // If mode is 'set', first uncheck all (that are not in values)
103
+ if (selectMode === 'set') {
104
+ for (const input of inputs) {
105
+ if (input.disabled) continue;
106
+ const shouldBeChecked = vals.some(val => matches(input, val));
107
+ if (input.checked !== shouldBeChecked) {
108
+ input.checked = shouldBeChecked;
109
+ input.dispatchEvent(new Event('input', { bubbles: true }));
110
+ input.dispatchEvent(new Event('change', { bubbles: true }));
111
+ changes.push({ value: input.value, checked: shouldBeChecked });
112
+ }
113
+ }
114
+ } else {
115
+ // For add/remove/toggle modes
116
+ for (const val of vals) {
117
+ for (const input of inputs) {
118
+ if (input.disabled) continue;
119
+ if (!matches(input, val)) continue;
120
+
121
+ let newState = input.checked;
122
+ switch (selectMode) {
123
+ case 'add':
124
+ newState = true;
125
+ break;
126
+ case 'remove':
127
+ newState = false;
128
+ break;
129
+ case 'toggle':
130
+ newState = !input.checked;
131
+ break;
132
+ }
133
+
134
+ if (input.checked !== newState) {
135
+ input.checked = newState;
136
+ input.dispatchEvent(new Event('input', { bubbles: true }));
137
+ input.dispatchEvent(new Event('change', { bubbles: true }));
138
+ changes.push({ value: input.value, checked: newState });
139
+ }
140
+ break; // Found matching input for this value
141
+ }
142
+ }
143
+ }
144
+
145
+ // Get final state
146
+ const checked = document.querySelectorAll(`input[type="checkbox"][name="${name}"]:checked`);
147
+ return {
148
+ success: true,
149
+ changes,
150
+ selectedValues: Array.from(checked).map(input => input.value)
151
+ };
152
+ }, this.groupName, valueList, mode, by);
153
+
154
+ return result;
155
+ }
156
+
157
+ /**
158
+ * Check specific values (add mode)
159
+ */
160
+ async check(values) {
161
+ return await this.setValue(values, { mode: 'add' });
162
+ }
163
+
164
+ /**
165
+ * Uncheck specific values (remove mode)
166
+ */
167
+ async uncheck(values) {
168
+ return await this.setValue(values, { mode: 'remove' });
169
+ }
170
+
171
+ /**
172
+ * Toggle specific values
173
+ */
174
+ async toggle(values) {
175
+ return await this.setValue(values, { mode: 'toggle' });
176
+ }
177
+
178
+ /**
179
+ * Clear all checkboxes
180
+ */
181
+ async clear() {
182
+ return await this.page.evaluate((name) => {
183
+ const inputs = document.querySelectorAll(`input[type="checkbox"][name="${name}"]:checked`);
184
+ inputs.forEach(input => {
185
+ if (!input.disabled) {
186
+ input.checked = false;
187
+ input.dispatchEvent(new Event('input', { bubbles: true }));
188
+ input.dispatchEvent(new Event('change', { bubbles: true }));
189
+ }
190
+ });
191
+ return { success: true, cleared: inputs.length };
192
+ }, this.groupName);
193
+ }
194
+
195
+ getActionDescription(values, identifier) {
196
+ const valueList = Array.isArray(values) ? values : [values];
197
+ return `Selected [${valueList.join(', ')}] in checkbox group "${this.groupName}"`;
198
+ }
199
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * models/CheckboxModel.js
3
+ *
4
+ * Model for checkbox and radio button inputs.
5
+ * Supports check, uncheck, toggle, and click operations.
6
+ */
7
+
8
+ import { BaseInputModel } from './BaseInputModel.js';
9
+
10
+ export class CheckboxModel extends BaseInputModel {
11
+ static get inputTypes() {
12
+ return ['checkbox', 'radio'];
13
+ }
14
+
15
+ /**
16
+ * Set checkbox/radio state
17
+ * @param {string|boolean} value - 'check', 'uncheck', 'toggle', true, false
18
+ * @param {object} options - (unused, for interface compatibility)
19
+ */
20
+ async setValue(value, options = {}) {
21
+ const normalizedValue = this._normalizeValue(value);
22
+
23
+ await this.element.evaluate((el, shouldBeChecked) => {
24
+ if (shouldBeChecked === 'toggle') {
25
+ el.checked = !el.checked;
26
+ } else {
27
+ el.checked = shouldBeChecked;
28
+ }
29
+ el.dispatchEvent(new Event('input', { bubbles: true }));
30
+ el.dispatchEvent(new Event('change', { bubbles: true }));
31
+ }, normalizedValue);
32
+ }
33
+
34
+ _normalizeValue(value) {
35
+ if (typeof value === 'boolean') {
36
+ return value;
37
+ }
38
+
39
+ const strValue = String(value).toLowerCase();
40
+
41
+ if (['check', 'checked', 'true', 'yes', '1', 'on'].includes(strValue)) {
42
+ return true;
43
+ }
44
+ if (['uncheck', 'unchecked', 'false', 'no', '0', 'off'].includes(strValue)) {
45
+ return false;
46
+ }
47
+ if (strValue === 'toggle') {
48
+ return 'toggle';
49
+ }
50
+
51
+ // Default: treat as check
52
+ return true;
53
+ }
54
+
55
+ /**
56
+ * Check the checkbox/radio
57
+ */
58
+ async check() {
59
+ await this.setValue(true);
60
+ }
61
+
62
+ /**
63
+ * Uncheck the checkbox (not applicable to radio)
64
+ */
65
+ async uncheck() {
66
+ await this.setValue(false);
67
+ }
68
+
69
+ /**
70
+ * Toggle the checkbox state
71
+ */
72
+ async toggle() {
73
+ await this.setValue('toggle');
74
+ }
75
+
76
+ /**
77
+ * Check if currently checked
78
+ */
79
+ async isChecked() {
80
+ return await this.element.evaluate(el => el.checked);
81
+ }
82
+
83
+ async getValue() {
84
+ return await this.isChecked();
85
+ }
86
+
87
+ async getMetadata() {
88
+ const base = await super.getMetadata();
89
+ const extra = await this.element.evaluate(el => ({
90
+ checked: el.checked,
91
+ inputValue: el.value, // The value attribute (sent when checked)
92
+ }));
93
+ return { ...base, ...extra };
94
+ }
95
+
96
+ getActionDescription(value, identifier) {
97
+ const normalizedValue = this._normalizeValue(value);
98
+ if (normalizedValue === 'toggle') {
99
+ return `Toggled ${identifier}`;
100
+ }
101
+ return normalizedValue ? `Checked ${identifier}` : `Unchecked ${identifier}`;
102
+ }
103
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * models/ColorInputModel.js
3
+ *
4
+ * Model for color picker inputs.
5
+ * Sets value directly via JavaScript.
6
+ *
7
+ * Expected format: "#RRGGBB" (hex color)
8
+ */
9
+
10
+ import { BaseInputModel } from './BaseInputModel.js';
11
+
12
+ export class ColorInputModel extends BaseInputModel {
13
+ static get inputTypes() {
14
+ return ['color'];
15
+ }
16
+
17
+ /**
18
+ * Set color value directly via JavaScript
19
+ * @param {string} value - Color in hex format "#RRGGBB"
20
+ * @param {object} options - (unused, for interface compatibility)
21
+ */
22
+ async setValue(value, options = {}) {
23
+ // Normalize color format
24
+ let normalizedValue = value;
25
+
26
+ // Add # if missing
27
+ if (!normalizedValue.startsWith('#')) {
28
+ normalizedValue = '#' + normalizedValue;
29
+ }
30
+
31
+ // Convert 3-char hex to 6-char
32
+ if (/^#[0-9A-Fa-f]{3}$/.test(normalizedValue)) {
33
+ normalizedValue = '#' + normalizedValue[1] + normalizedValue[1] +
34
+ normalizedValue[2] + normalizedValue[2] +
35
+ normalizedValue[3] + normalizedValue[3];
36
+ }
37
+
38
+ // Validate hex color format
39
+ if (!/^#[0-9A-Fa-f]{6}$/.test(normalizedValue)) {
40
+ console.warn(`ColorInputModel: Value "${value}" may not be a valid hex color`);
41
+ }
42
+
43
+ await this.element.evaluate((el, val) => {
44
+ el.value = val;
45
+ el.dispatchEvent(new Event('input', { bubbles: true }));
46
+ el.dispatchEvent(new Event('change', { bubbles: true }));
47
+ }, normalizedValue.toLowerCase());
48
+ }
49
+
50
+ getActionDescription(value, identifier) {
51
+ return `Set color "${value}" on ${identifier}`;
52
+ }
53
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * models/DateInputModel.js
3
+ *
4
+ * Model for date-related input fields.
5
+ * Handles: date, datetime-local, month, week
6
+ * Sets value directly via JavaScript since keyboard input doesn't work reliably.
7
+ *
8
+ * Expected formats:
9
+ * - date: "YYYY-MM-DD"
10
+ * - datetime-local: "YYYY-MM-DDTHH:MM" or "YYYY-MM-DDTHH:MM:SS"
11
+ * - month: "YYYY-MM"
12
+ * - week: "YYYY-Www" (e.g., "2024-W01")
13
+ */
14
+
15
+ import { BaseInputModel } from './BaseInputModel.js';
16
+
17
+ export class DateInputModel extends BaseInputModel {
18
+ static get inputTypes() {
19
+ return ['date', 'datetime-local', 'month', 'week'];
20
+ }
21
+
22
+ /**
23
+ * Set date value directly via JavaScript
24
+ * @param {string} value - Date in appropriate format for input type
25
+ * @param {object} options - (unused, for interface compatibility)
26
+ */
27
+ async setValue(value, options = {}) {
28
+ const inputType = await this.element.evaluate(el => el.type);
29
+
30
+ // Validate format based on input type
31
+ this._validateFormat(value, inputType);
32
+
33
+ await this.element.evaluate((el, val) => {
34
+ el.value = val;
35
+ el.dispatchEvent(new Event('input', { bubbles: true }));
36
+ el.dispatchEvent(new Event('change', { bubbles: true }));
37
+ }, value);
38
+ }
39
+
40
+ _validateFormat(value, inputType) {
41
+ const formats = {
42
+ 'date': /^\d{4}-\d{2}-\d{2}$/,
43
+ 'datetime-local': /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?$/,
44
+ 'month': /^\d{4}-\d{2}$/,
45
+ 'week': /^\d{4}-W\d{2}$/,
46
+ };
47
+
48
+ const regex = formats[inputType];
49
+ if (regex && !regex.test(value)) {
50
+ console.warn(`DateInputModel: Value "${value}" may not be in correct format for ${inputType}`);
51
+ }
52
+ }
53
+
54
+ async getMetadata() {
55
+ const base = await super.getMetadata();
56
+ const extra = await this.element.evaluate(el => ({
57
+ min: el.min || null,
58
+ max: el.max || null,
59
+ step: el.step || null,
60
+ }));
61
+ return { ...base, ...extra };
62
+ }
63
+
64
+ getActionDescription(value, identifier) {
65
+ return `Set date "${value}" on ${identifier}`;
66
+ }
67
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * models/RadioGroupModel.js
3
+ *
4
+ * Model for radio button groups.
5
+ * Allows selecting one option from a group by name, value, or label text.
6
+ */
7
+
8
+ import { BaseInputModel } from './BaseInputModel.js';
9
+
10
+ export class RadioGroupModel extends BaseInputModel {
11
+ /**
12
+ * Create a RadioGroupModel
13
+ * @param {string} groupName - The name attribute of the radio group
14
+ * @param {Page} page - Puppeteer page
15
+ */
16
+ constructor(groupName, page) {
17
+ super(null, page);
18
+ this.groupName = groupName;
19
+ }
20
+
21
+ /**
22
+ * Get all options in this radio group
23
+ */
24
+ async getOptions() {
25
+ return await this.page.evaluate((name) => {
26
+ const inputs = document.querySelectorAll(`input[type="radio"][name="${name}"]`);
27
+ return Array.from(inputs).map(input => {
28
+ // Find label text
29
+ let label = null;
30
+ const parentLabel = input.closest('label');
31
+ if (parentLabel) {
32
+ label = parentLabel.textContent?.trim();
33
+ } else if (input.id) {
34
+ const labelFor = document.querySelector(`label[for="${input.id}"]`);
35
+ if (labelFor) {
36
+ label = labelFor.textContent?.trim();
37
+ }
38
+ }
39
+ label = label || input.getAttribute('aria-label') || input.value;
40
+
41
+ return {
42
+ value: input.value,
43
+ label,
44
+ checked: input.checked,
45
+ disabled: input.disabled
46
+ };
47
+ });
48
+ }, this.groupName);
49
+ }
50
+
51
+ /**
52
+ * Get currently selected value
53
+ */
54
+ async getValue() {
55
+ return await this.page.evaluate((name) => {
56
+ const checked = document.querySelector(`input[type="radio"][name="${name}"]:checked`);
57
+ return checked ? checked.value : null;
58
+ }, this.groupName);
59
+ }
60
+
61
+ /**
62
+ * Select a radio option by value or label text
63
+ * @param {string} value - Value or label text to select
64
+ * @param {object} options - { by: 'value'|'text'|'auto' }
65
+ */
66
+ async setValue(value, options = {}) {
67
+ const { by = 'auto' } = options;
68
+
69
+ const result = await this.page.evaluate((name, val, selectBy) => {
70
+ const inputs = document.querySelectorAll(`input[type="radio"][name="${name}"]`);
71
+
72
+ for (const input of inputs) {
73
+ if (input.disabled) continue;
74
+
75
+ let match = false;
76
+
77
+ // Get label text
78
+ let label = null;
79
+ const parentLabel = input.closest('label');
80
+ if (parentLabel) {
81
+ label = parentLabel.textContent?.trim();
82
+ } else if (input.id) {
83
+ const labelFor = document.querySelector(`label[for="${input.id}"]`);
84
+ if (labelFor) {
85
+ label = labelFor.textContent?.trim();
86
+ }
87
+ }
88
+ label = label || input.getAttribute('aria-label');
89
+
90
+ switch (selectBy) {
91
+ case 'value':
92
+ match = input.value === val;
93
+ break;
94
+ case 'text':
95
+ match = label && (label === val || label.toLowerCase().includes(val.toLowerCase()));
96
+ break;
97
+ case 'auto':
98
+ default:
99
+ // Try value first, then label
100
+ match = input.value === val ||
101
+ (label && (label === val || label.toLowerCase().includes(val.toLowerCase())));
102
+ break;
103
+ }
104
+
105
+ if (match) {
106
+ input.checked = true;
107
+ input.dispatchEvent(new Event('input', { bubbles: true }));
108
+ input.dispatchEvent(new Event('change', { bubbles: true }));
109
+ return { success: true, selectedValue: input.value, selectedLabel: label };
110
+ }
111
+ }
112
+
113
+ return { success: false, error: `No matching option found for "${val}" in radio group "${name}"` };
114
+ }, this.groupName, value, by);
115
+
116
+ if (!result.success) {
117
+ throw new Error(result.error);
118
+ }
119
+
120
+ return result;
121
+ }
122
+
123
+ getActionDescription(value, identifier) {
124
+ return `Selected "${value}" in radio group "${this.groupName}"`;
125
+ }
126
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * models/RangeInputModel.js
3
+ *
4
+ * Model for range/slider inputs.
5
+ * Sets value directly via JavaScript.
6
+ *
7
+ * Expected format: numeric value within min/max range
8
+ */
9
+
10
+ import { BaseInputModel } from './BaseInputModel.js';
11
+
12
+ export class RangeInputModel extends BaseInputModel {
13
+ static get inputTypes() {
14
+ return ['range'];
15
+ }
16
+
17
+ /**
18
+ * Set range value directly via JavaScript
19
+ * @param {string|number} value - Numeric value
20
+ * @param {object} options - (unused, for interface compatibility)
21
+ */
22
+ async setValue(value, options = {}) {
23
+ const numericValue = parseFloat(value);
24
+
25
+ if (isNaN(numericValue)) {
26
+ console.warn(`RangeInputModel: Value "${value}" is not a valid number`);
27
+ }
28
+
29
+ // Get min/max and clamp value
30
+ const { min, max } = await this.element.evaluate(el => ({
31
+ min: parseFloat(el.min) || 0,
32
+ max: parseFloat(el.max) || 100,
33
+ }));
34
+
35
+ const clampedValue = Math.max(min, Math.min(max, numericValue));
36
+ if (clampedValue !== numericValue) {
37
+ console.warn(`RangeInputModel: Value ${numericValue} clamped to ${clampedValue} (range: ${min}-${max})`);
38
+ }
39
+
40
+ await this.element.evaluate((el, val) => {
41
+ el.value = val;
42
+ el.dispatchEvent(new Event('input', { bubbles: true }));
43
+ el.dispatchEvent(new Event('change', { bubbles: true }));
44
+ }, clampedValue.toString());
45
+ }
46
+
47
+ async getMetadata() {
48
+ const base = await super.getMetadata();
49
+ const extra = await this.element.evaluate(el => ({
50
+ min: el.min || '0',
51
+ max: el.max || '100',
52
+ step: el.step || '1',
53
+ }));
54
+ return { ...base, ...extra };
55
+ }
56
+
57
+ getActionDescription(value, identifier) {
58
+ return `Set range value "${value}" on ${identifier}`;
59
+ }
60
+ }