huibo-ui 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/cjs/hb-form-item.cjs.entry.js +59 -9
  2. package/dist/cjs/hb-form-item.cjs.entry.js.map +1 -1
  3. package/dist/cjs/hb-form.cjs.entry.js +105 -2
  4. package/dist/cjs/hb-form.cjs.entry.js.map +1 -1
  5. package/dist/cjs/hb-select.cjs.entry.js +82 -10
  6. package/dist/cjs/hb-select.cjs.entry.js.map +1 -1
  7. package/dist/cjs/hb-steps.cjs.entry.js +1 -1
  8. package/dist/cjs/hb-steps.cjs.entry.js.map +1 -1
  9. package/dist/cjs/hb-table.cjs.entry.js +195 -27
  10. package/dist/cjs/hb-table.cjs.entry.js.map +1 -1
  11. package/dist/cjs/huibo-ui.cjs.js +1 -1
  12. package/dist/cjs/loader.cjs.js +1 -1
  13. package/dist/collection/components/Form/Form.js +205 -2
  14. package/dist/collection/components/Form/Form.js.map +1 -1
  15. package/dist/collection/components/Form/FormItem.js +117 -10
  16. package/dist/collection/components/Form/FormItem.js.map +1 -1
  17. package/dist/collection/components/Select/Select.js +105 -10
  18. package/dist/collection/components/Select/Select.js.map +1 -1
  19. package/dist/collection/components/Table/Table.js +273 -27
  20. package/dist/collection/components/Table/Table.js.map +1 -1
  21. package/dist/collection/utils/virtual-scroll.js +39 -0
  22. package/dist/collection/utils/virtual-scroll.js.map +1 -0
  23. package/dist/components/hb-form-item.js +62 -9
  24. package/dist/components/hb-form-item.js.map +1 -1
  25. package/dist/components/hb-form.js +110 -2
  26. package/dist/components/hb-form.js.map +1 -1
  27. package/dist/components/hb-select.js +87 -11
  28. package/dist/components/hb-select.js.map +1 -1
  29. package/dist/components/hb-steps.js +1 -1
  30. package/dist/components/hb-steps.js.map +1 -1
  31. package/dist/components/hb-table.js +203 -29
  32. package/dist/components/hb-table.js.map +1 -1
  33. package/dist/esm/hb-form-item.entry.js +59 -9
  34. package/dist/esm/hb-form-item.entry.js.map +1 -1
  35. package/dist/esm/hb-form.entry.js +105 -2
  36. package/dist/esm/hb-form.entry.js.map +1 -1
  37. package/dist/esm/hb-select.entry.js +82 -10
  38. package/dist/esm/hb-select.entry.js.map +1 -1
  39. package/dist/esm/hb-steps.entry.js +1 -1
  40. package/dist/esm/hb-steps.entry.js.map +1 -1
  41. package/dist/esm/hb-table.entry.js +195 -27
  42. package/dist/esm/hb-table.entry.js.map +1 -1
  43. package/dist/esm/huibo-ui.js +1 -1
  44. package/dist/esm/loader.js +1 -1
  45. package/dist/huibo-ui/huibo-ui.esm.js +1 -1
  46. package/dist/huibo-ui/huibo-ui.esm.js.map +1 -1
  47. package/dist/huibo-ui/{p-79b24b83.entry.js → p-2cf5bf20.entry.js} +2 -2
  48. package/dist/huibo-ui/{p-79b24b83.entry.js.map → p-2cf5bf20.entry.js.map} +1 -1
  49. package/dist/huibo-ui/p-4148d875.entry.js +2 -0
  50. package/dist/huibo-ui/p-4148d875.entry.js.map +1 -0
  51. package/dist/huibo-ui/{p-54a28052.entry.js → p-6bfe1954.entry.js} +2 -2
  52. package/dist/huibo-ui/p-6bfe1954.entry.js.map +1 -0
  53. package/dist/huibo-ui/{p-ac18c68b.entry.js → p-e8824b2c.entry.js} +2 -2
  54. package/dist/huibo-ui/p-e8824b2c.entry.js.map +1 -0
  55. package/dist/huibo-ui/p-f69599fa.entry.js +2 -0
  56. package/dist/huibo-ui/p-f69599fa.entry.js.map +1 -0
  57. package/dist/types/components/Form/Form.d.ts +57 -0
  58. package/dist/types/components/Form/FormItem.d.ts +23 -0
  59. package/dist/types/components/Select/Select.d.ts +19 -0
  60. package/dist/types/components/Table/Table.d.ts +103 -8
  61. package/dist/types/components.d.ts +148 -2
  62. package/dist/types/utils/virtual-scroll.d.ts +38 -0
  63. package/package.json +1 -1
  64. package/dist/huibo-ui/p-29092b85.entry.js +0 -2
  65. package/dist/huibo-ui/p-29092b85.entry.js.map +0 -1
  66. package/dist/huibo-ui/p-2bc30b1b.entry.js +0 -2
  67. package/dist/huibo-ui/p-2bc30b1b.entry.js.map +0 -1
  68. package/dist/huibo-ui/p-54a28052.entry.js.map +0 -1
  69. package/dist/huibo-ui/p-ac18c68b.entry.js.map +0 -1
@@ -24,6 +24,18 @@ const FormItem = class {
24
24
  rules = [];
25
25
  /** 表单尺寸 */
26
26
  size = 'default';
27
+ /**
28
+ * F6:手动校验态('success' | 'warning' | 'error' | 'validating' | '')。
29
+ * 设置后覆盖内部校验结果驱动的错误显示(对齐 antd validateStatus)。
30
+ */
31
+ validateStatus;
32
+ /** F6:手动错误/帮助文案(配合 validateStatus='error' 显示自定义提示) */
33
+ help;
34
+ /**
35
+ * F7:标签栅格占比(1-24)。未设置时用 labelWidth。
36
+ * 对齐 antd labelCol(简化为纯数字栅格,labelCol.span)。
37
+ */
38
+ labelCol;
27
39
  errors = [];
28
40
  isvalidating = false;
29
41
  /** hb-form 在 componentWillLoad 注册的事件监听;FormItem 在 componentDidLoad 派发,
@@ -70,6 +82,24 @@ const FormItem = class {
70
82
  const form = this.el.closest('hb-form');
71
83
  return (form && form.labelPosition) || 'right';
72
84
  }
85
+ /**
86
+ * F4:继承父 hb-form 的 validateMessages(校验提示模板)。
87
+ * 支持占位符 {label}。仅对 rule 未显式指定 message 的规则生效。
88
+ */
89
+ get resolvedValidateMessages() {
90
+ const form = this.el.closest('hb-form');
91
+ return form && form.validateMessages;
92
+ }
93
+ /** 用模板生成错误文案({label} 替换为字段标签),优先级:rule.message > 模板 > 内置默认 */
94
+ msg(rule, key, fallback) {
95
+ if (rule.message)
96
+ return rule.message;
97
+ const tmpl = this.resolvedValidateMessages;
98
+ const label = this.label || this.prop;
99
+ if (tmpl && tmpl[key])
100
+ return tmpl[key].replace('{label}', label);
101
+ return fallback.replace('{label}', label);
102
+ }
73
103
  /**
74
104
  * 验证此字段
75
105
  */
@@ -86,7 +116,7 @@ const FormItem = class {
86
116
  // 必填校验:空字符串 / null / undefined / 空数组均视为"未填"
87
117
  const isEmpty = value === undefined || value === null || value === '' || (Array.isArray(value) && value.length === 0);
88
118
  if (rule.required && isEmpty) {
89
- errors.push(rule.message || `${this.label || this.prop}不能为空`);
119
+ errors.push(this.msg(rule, 'required', `${this.label || this.prop}不能为空`));
90
120
  continue;
91
121
  }
92
122
  // 非必填且为空时,跳过后续格式校验(与 antd 行为一致)
@@ -95,22 +125,22 @@ const FormItem = class {
95
125
  if (rule.pattern) {
96
126
  const ok = typeof rule.pattern === 'string' ? new RegExp(rule.pattern).test(String(value)) : rule.pattern.test(String(value));
97
127
  if (!ok) {
98
- errors.push(rule.message || `${this.label || this.prop}格式不正确`);
128
+ errors.push(this.msg(rule, 'pattern', `${this.label || this.prop}格式不正确`));
99
129
  continue;
100
130
  }
101
131
  }
102
132
  if (rule.type === 'email') {
103
133
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value))) {
104
- errors.push(rule.message || `${this.label || this.prop}格式不正确`);
134
+ errors.push(this.msg(rule, 'email', `${this.label || this.prop}格式不正确`));
105
135
  continue;
106
136
  }
107
137
  }
108
138
  if (rule.min !== undefined && typeof value === 'string' && value.length < rule.min) {
109
- errors.push(rule.message || `${this.label || this.prop}最少${rule.min}个字符`);
139
+ errors.push(this.msg(rule, 'min', `${this.label || this.prop}最少${rule.min}个字符`));
110
140
  continue;
111
141
  }
112
142
  if (rule.max !== undefined && typeof value === 'string' && value.length > rule.max) {
113
- errors.push(rule.message || `${this.label || this.prop}最多${rule.max}个字符`);
143
+ errors.push(this.msg(rule, 'max', `${this.label || this.prop}最多${rule.max}个字符`));
114
144
  continue;
115
145
  }
116
146
  if (rule.validator) {
@@ -139,22 +169,42 @@ const FormItem = class {
139
169
  }
140
170
  return undefined;
141
171
  };
172
+ /** F6:实际显示的错误文案。手动 help 优先;否则取内部 errors[0] */
173
+ get displayError() {
174
+ if (this.help !== undefined)
175
+ return this.help;
176
+ return this.errors[0] || '';
177
+ }
178
+ /** F6:实际是否显示错误(手动 validateStatus='error' 或内部有错) */
179
+ get showError() {
180
+ if (this.validateStatus === 'error')
181
+ return true;
182
+ if (this.validateStatus === 'success' || this.validateStatus === 'validating')
183
+ return false;
184
+ return this.errors.length > 0;
185
+ }
142
186
  render() {
143
187
  const labelTop = this.resolvedLabelPosition === 'top';
144
- const hasError = this.errors.length > 0;
188
+ const hasError = this.showError;
145
189
  // A4:label 与内容关联。子控件是 slot(id 未知),无法用 label.for;
146
190
  // 改用 content 容器 role=group + aria-labelledby 指向 label id,
147
191
  // 把 label 语义绑到整组;并用 aria-required / aria-invalid 暴露必填与校验态(读屏可感知,
148
192
  // 不再仅靠红色 + 隐藏星号)。
149
193
  const labelId = this.label ? `hb-form-item__label-${this.prop || 'x'}` : undefined;
150
- return (index.h("div", { key: 'ad51782227c637557a1cc0a4be2397b20f63b2e2', class: {
194
+ // F7:labelCol 栅格占比(1-24),未设置时回退 labelWidth
195
+ const labelStyle = this.labelCol
196
+ ? { width: `${(this.labelCol / 24) * 100}%` }
197
+ : this.labelWidth
198
+ ? { width: this.labelWidth }
199
+ : undefined;
200
+ return (index.h("div", { key: '8c6bd451a2c673a66d3a98b08fdf30ac74f2fb2e', class: {
151
201
  'hb-form-item': true,
152
202
  'hb-form-item--error': hasError,
153
- 'hb-form-item--validating': this.isvalidating,
203
+ 'hb-form-item--validating': this.isvalidating || this.validateStatus === 'validating',
154
204
  'hb-form-item--required': this.isRequired,
155
205
  'hb-form-item--label-top': labelTop,
156
206
  'hb-form-item--label-left': this.resolvedLabelPosition === 'left',
157
- } }, this.label && (index.h("label", { key: '22b4830775644a315a797c91ca77045015632cf5', id: labelId, class: "hb-form-item__label", style: this.labelWidth ? { width: this.labelWidth } : undefined, title: this.label }, this.isRequired && (index.h("span", { key: '43c4db5f65f68f8a9db2362c8cdf724069895238', class: "hb-form-item__required", "aria-hidden": "true" }, "*")), this.label)), index.h("div", { key: '0fe8dda2a5b0363c55ca6e19d1c2b0e3a44e503f', class: "hb-form-item__content", role: "group", "aria-labelledby": labelId, "aria-required": this.isRequired ? 'true' : undefined, "aria-invalid": hasError ? 'true' : undefined }, index.h("slot", { key: '6b714ffde9c6a0f9112aef9a28519ba42df0fa96' }), hasError && (index.h("div", { key: '41a97ac3f7f0dccd9938d9dbf844852c085d0b72', class: "hb-form-item__error", role: "alert" }, this.errors[0])))));
207
+ } }, this.label && (index.h("label", { key: '98db5eae626555e4ae442b116bff048189209b5d', id: labelId, class: "hb-form-item__label", style: labelStyle, title: this.label }, this.isRequired && (index.h("span", { key: '05f771e79623e3eeac28fb8b3d100501c5c94f6b', class: "hb-form-item__required", "aria-hidden": "true" }, "*")), this.label)), index.h("div", { key: '3dc24c2eb1f33ed16f14cca93a644ece209d6c13', class: "hb-form-item__content", role: "group", "aria-labelledby": labelId, "aria-required": this.isRequired ? 'true' : undefined, "aria-invalid": hasError ? 'true' : undefined }, index.h("slot", { key: '3a143de305f27a70c5e775a0c24c16da77f7fe3a' }), hasError && (index.h("div", { key: '9d5708648cb0ec43b9c8a274145221ec0edec86e', class: "hb-form-item__error", role: "alert" }, this.displayError)))));
158
208
  }
159
209
  };
160
210
  FormItem.style = HbFormItemStyle0;
@@ -1 +1 @@
1
- {"file":"hb-form-item.entry.cjs.js","mappings":"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","names":["h"],"sources":["src/components/Form/form-item.css?tag=hb-form-item&encapsulation=shadow","src/components/Form/FormItem.tsx"],"sourcesContent":[":host {\n display: block;\n}\n\n.hb-form-item {\n display: flex;\n align-items: flex-start;\n gap: var(--hb-spacing-sm);\n}\n\n/* label 位置:顶部(对齐 antd label-position=\"top\")——\n 需要 Form 通过 class 下发,但 FormItem 在独立使用时也支持自身包装。\n 这里用 :host([data-label-position=\"top\"]) 兜底,Form 可通过 setting attribute 生效。 */\n:host([data-label-top]) .hb-form-item,\n.hb-form-item--label-top {\n flex-direction: column;\n align-items: stretch;\n gap: var(--hb-spacing-xs);\n}\n\n.hb-form-item__label {\n display: inline-flex;\n align-items: center;\n justify-content: flex-end;\n height: var(--hb-size-default, 32px);\n font-size: var(--hb-font-size-sm, 14px);\n color: var(--hb-color-text);\n white-space: nowrap;\n flex-shrink: 0;\n}\n\n/* label-position=top 时标签左对齐 */\n:host([data-label-top]) .hb-form-item__label,\n.hb-form-item--label-top .hb-form-item__label {\n justify-content: flex-start;\n height: auto;\n padding-bottom: var(--hb-spacing-xxs, 2px);\n}\n\n/* label-position=left 时标签左对齐 */\n.hb-form-item--label-left .hb-form-item__label {\n justify-content: flex-start;\n}\n\n.hb-form-item__required {\n color: var(--hb-color-danger);\n margin-right: 4px;\n font-size: var(--hb-font-size-sm, 14px);\n font-family: SimSun, sans-serif;\n}\n\n.hb-form-item__content {\n flex: 1;\n position: relative;\n min-width: 0;\n}\n\n.hb-form-item__error {\n font-size: var(--hb-font-size-xs, 12px);\n color: var(--hb-color-danger);\n margin-top: 4px;\n line-height: 1.5;\n min-height: 0;\n animation: hb-form-error-fade-in 0.2s ease-out;\n}\n\n@keyframes hb-form-error-fade-in {\n from {\n opacity: 0;\n transform: translateY(-4px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n/* ============================================================\n * 响应式(移动端):小屏强制 label 顶部布局,\n * 避免标签列在窄屏被挤压。覆盖 hb-form-item--label-left/right。\n * ============================================================ */\n@media (max-width: 575.98px) {\n .hb-form-item {\n flex-direction: column !important;\n align-items: stretch !important;\n gap: var(--hb-spacing-xs) !important;\n }\n\n .hb-form-item__label {\n justify-content: flex-start !important;\n height: auto !important;\n padding-bottom: var(--hb-spacing-xxs, 2px) !important;\n }\n}\n","import { Component, h, Prop, State, Element } from '@stencil/core';\n\n/**\n * FormItem 表单项组件\n * 表单中的每一项\n */\n@Component({\n tag: 'hb-form-item',\n styleUrl: 'form-item.css',\n shadow: true,\n})\nexport class FormItem {\n @Element() el: HTMLElement;\n\n /** 字段名 */\n @Prop() prop: string = '';\n\n /** 标签文本 */\n @Prop() label: string = '';\n\n /** 标签宽度 */\n @Prop() labelWidth?: string;\n\n /** 是否必填 */\n @Prop() required: boolean = false;\n\n /** 该表单项的验证规则 */\n @Prop() rules: any[] = [];\n\n /** 表单尺寸 */\n @Prop() size: 'large' | 'default' | 'small' = 'default';\n\n @State() errors: string[] = [];\n @State() isvalidating: boolean = false;\n\n /** hb-form 在 componentWillLoad 注册的事件监听;FormItem 在 componentDidLoad 派发,\n * 但 componentWillLoad 早于子组件的 componentDidLoad,因此注册一定先到。\n * 这里还监听 hbFieldReset,便于 Form.resetFields 主动广播。 */\n\n componentDidLoad() {\n // 向父级 hb-form 注册\n this.el.dispatchEvent(\n new CustomEvent('hbFormFieldRegister', {\n detail: {\n prop: this.prop,\n validate: this.validate,\n resetValue: this.resetValue,\n getValue: this.getValue,\n },\n bubbles: true,\n composed: true, // 穿越 Shadow DOM 边界(FormItem 自身 shadow -> Form host)\n }),\n );\n\n // 子控件值变化时清错(对齐 antd:合法输入即移除错误提示)\n this.el.addEventListener('hbChange', this.handleChildChange);\n }\n\n disconnectedCallback() {\n this.el.removeEventListener('hbChange', this.handleChildChange);\n }\n\n /** 子控件 hbChange 时,若当前有错误则重新校验以尽早清除错误提示 */\n private handleChildChange = () => {\n if (this.errors.length > 0) {\n void this.validate();\n }\n };\n\n private get formRules(): any[] {\n // 合并 prop 对应的 form rules 和自身 rules\n const form = this.el.closest('hb-form');\n const formLevelRules = form ? (form as any).rules?.[this.prop] || [] : [];\n return [...formLevelRules, ...this.rules];\n }\n\n /** 是否渲染必填星号:显式 required 或任一规则带 required(对齐 antd 自动推断) */\n private get isRequired(): boolean {\n if (this.required) return true;\n return this.formRules.some(r => r && r.required);\n }\n\n /** 继承父 hb-form 的 labelPosition(FormItem 自身无此 prop,避免与 Form 冲突时以 Form 为准) */\n private get resolvedLabelPosition(): 'left' | 'right' | 'top' {\n const form = this.el.closest('hb-form') as any;\n return (form && form.labelPosition) || 'right';\n }\n\n /**\n * 验证此字段\n */\n @Prop() validate = async (): Promise<string[]> => {\n const value = this.getValue();\n const rules = this.formRules;\n\n if (rules.length === 0) {\n this.errors = [];\n return [];\n }\n\n this.isvalidating = true;\n const errors: string[] = [];\n\n for (const rule of rules) {\n // 必填校验:空字符串 / null / undefined / 空数组均视为\"未填\"\n const isEmpty = value === undefined || value === null || value === '' || (Array.isArray(value) && value.length === 0);\n if (rule.required && isEmpty) {\n errors.push(rule.message || `${this.label || this.prop}不能为空`);\n continue;\n }\n // 非必填且为空时,跳过后续格式校验(与 antd 行为一致)\n if (isEmpty) continue;\n if (rule.pattern) {\n const ok = typeof rule.pattern === 'string' ? new RegExp(rule.pattern).test(String(value)) : rule.pattern.test(String(value));\n if (!ok) {\n errors.push(rule.message || `${this.label || this.prop}格式不正确`);\n continue;\n }\n }\n if (rule.type === 'email') {\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(String(value))) {\n errors.push(rule.message || `${this.label || this.prop}格式不正确`);\n continue;\n }\n }\n if (rule.min !== undefined && typeof value === 'string' && value.length < rule.min) {\n errors.push(rule.message || `${this.label || this.prop}最少${rule.min}个字符`);\n continue;\n }\n if (rule.max !== undefined && typeof value === 'string' && value.length > rule.max) {\n errors.push(rule.message || `${this.label || this.prop}最多${rule.max}个字符`);\n continue;\n }\n if (rule.validator) {\n try {\n await rule.validator(value, this.formRules);\n } catch (err: any) {\n errors.push(err.message || rule.message || `${this.label || this.prop}验证失败`);\n }\n }\n }\n\n this.errors = errors;\n this.isvalidating = false;\n return errors;\n };\n\n @Prop() resetValue = () => {\n this.errors = [];\n };\n\n @Prop() getValue = (): any => {\n // 覆盖所有表单控件:input/select/cascader/date 系列/数字/checkbox/radio/switch/slider/color-picker/rate/upload\n const input = this.el.querySelector(\n 'hb-input, hb-select, hb-cascader, hb-date-picker, hb-date-time-picker, hb-date-range-picker, ' +\n 'hb-input-number, hb-textarea, hb-checkbox, hb-checkbox-group, hb-radio-group, hb-switch, ' +\n 'hb-slider, hb-color-picker, hb-rate, hb-time-picker, hb-time-select',\n );\n if (input) {\n return (input as any).modelValue;\n }\n return undefined;\n };\n\n render() {\n const labelTop = this.resolvedLabelPosition === 'top';\n const hasError = this.errors.length > 0;\n // A4:label 与内容关联。子控件是 slot(id 未知),无法用 label.for;\n // 改用 content 容器 role=group + aria-labelledby 指向 label id,\n // 把 label 语义绑到整组;并用 aria-required / aria-invalid 暴露必填与校验态(读屏可感知,\n // 不再仅靠红色 + 隐藏星号)。\n const labelId = this.label ? `hb-form-item__label-${this.prop || 'x'}` : undefined;\n return (\n <div\n class={{\n 'hb-form-item': true,\n 'hb-form-item--error': hasError,\n 'hb-form-item--validating': this.isvalidating,\n 'hb-form-item--required': this.isRequired,\n 'hb-form-item--label-top': labelTop,\n 'hb-form-item--label-left': this.resolvedLabelPosition === 'left',\n }}\n >\n {this.label && (\n <label id={labelId} class=\"hb-form-item__label\" style={this.labelWidth ? { width: this.labelWidth } : undefined} title={this.label}>\n {this.isRequired && (\n <span class=\"hb-form-item__required\" aria-hidden=\"true\">\n *\n </span>\n )}\n {this.label}\n </label>\n )}\n <div class=\"hb-form-item__content\" role=\"group\" aria-labelledby={labelId} aria-required={this.isRequired ? 'true' : undefined} aria-invalid={hasError ? 'true' : undefined}>\n <slot />\n {hasError && (\n <div class=\"hb-form-item__error\" role=\"alert\">\n {this.errors[0]}\n </div>\n )}\n </div>\n </div>\n );\n }\n}\n"],"version":3}
1
+ {"file":"hb-form-item.entry.cjs.js","mappings":"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","names":["h"],"sources":["src/components/Form/form-item.css?tag=hb-form-item&encapsulation=shadow","src/components/Form/FormItem.tsx"],"sourcesContent":[":host {\n display: block;\n}\n\n.hb-form-item {\n display: flex;\n align-items: flex-start;\n gap: var(--hb-spacing-sm);\n}\n\n/* label 位置:顶部(对齐 antd label-position=\"top\")——\n 需要 Form 通过 class 下发,但 FormItem 在独立使用时也支持自身包装。\n 这里用 :host([data-label-position=\"top\"]) 兜底,Form 可通过 setting attribute 生效。 */\n:host([data-label-top]) .hb-form-item,\n.hb-form-item--label-top {\n flex-direction: column;\n align-items: stretch;\n gap: var(--hb-spacing-xs);\n}\n\n.hb-form-item__label {\n display: inline-flex;\n align-items: center;\n justify-content: flex-end;\n height: var(--hb-size-default, 32px);\n font-size: var(--hb-font-size-sm, 14px);\n color: var(--hb-color-text);\n white-space: nowrap;\n flex-shrink: 0;\n}\n\n/* label-position=top 时标签左对齐 */\n:host([data-label-top]) .hb-form-item__label,\n.hb-form-item--label-top .hb-form-item__label {\n justify-content: flex-start;\n height: auto;\n padding-bottom: var(--hb-spacing-xxs, 2px);\n}\n\n/* label-position=left 时标签左对齐 */\n.hb-form-item--label-left .hb-form-item__label {\n justify-content: flex-start;\n}\n\n.hb-form-item__required {\n color: var(--hb-color-danger);\n margin-right: 4px;\n font-size: var(--hb-font-size-sm, 14px);\n font-family: SimSun, sans-serif;\n}\n\n.hb-form-item__content {\n flex: 1;\n position: relative;\n min-width: 0;\n}\n\n.hb-form-item__error {\n font-size: var(--hb-font-size-xs, 12px);\n color: var(--hb-color-danger);\n margin-top: 4px;\n line-height: 1.5;\n min-height: 0;\n animation: hb-form-error-fade-in 0.2s ease-out;\n}\n\n@keyframes hb-form-error-fade-in {\n from {\n opacity: 0;\n transform: translateY(-4px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n/* ============================================================\n * 响应式(移动端):小屏强制 label 顶部布局,\n * 避免标签列在窄屏被挤压。覆盖 hb-form-item--label-left/right。\n * ============================================================ */\n@media (max-width: 575.98px) {\n .hb-form-item {\n flex-direction: column !important;\n align-items: stretch !important;\n gap: var(--hb-spacing-xs) !important;\n }\n\n .hb-form-item__label {\n justify-content: flex-start !important;\n height: auto !important;\n padding-bottom: var(--hb-spacing-xxs, 2px) !important;\n }\n}\n","import { Component, h, Prop, State, Element } from '@stencil/core';\n\n/**\n * FormItem 表单项组件\n * 表单中的每一项\n */\n@Component({\n tag: 'hb-form-item',\n styleUrl: 'form-item.css',\n shadow: true,\n})\nexport class FormItem {\n @Element() el: HTMLElement;\n\n /** 字段名 */\n @Prop() prop: string = '';\n\n /** 标签文本 */\n @Prop() label: string = '';\n\n /** 标签宽度 */\n @Prop() labelWidth?: string;\n\n /** 是否必填 */\n @Prop() required: boolean = false;\n\n /** 该表单项的验证规则 */\n @Prop() rules: any[] = [];\n\n /** 表单尺寸 */\n @Prop() size: 'large' | 'default' | 'small' = 'default';\n\n /**\n * F6:手动校验态('success' | 'warning' | 'error' | 'validating' | '')。\n * 设置后覆盖内部校验结果驱动的错误显示(对齐 antd validateStatus)。\n */\n @Prop() validateStatus?: '' | 'success' | 'warning' | 'error' | 'validating';\n\n /** F6:手动错误/帮助文案(配合 validateStatus='error' 显示自定义提示) */\n @Prop() help?: string;\n\n /**\n * F7:标签栅格占比(1-24)。未设置时用 labelWidth。\n * 对齐 antd labelCol(简化为纯数字栅格,labelCol.span)。\n */\n @Prop() labelCol?: number;\n\n @State() errors: string[] = [];\n @State() isvalidating: boolean = false;\n\n /** hb-form 在 componentWillLoad 注册的事件监听;FormItem 在 componentDidLoad 派发,\n * 但 componentWillLoad 早于子组件的 componentDidLoad,因此注册一定先到。\n * 这里还监听 hbFieldReset,便于 Form.resetFields 主动广播。 */\n\n componentDidLoad() {\n // 向父级 hb-form 注册\n this.el.dispatchEvent(\n new CustomEvent('hbFormFieldRegister', {\n detail: {\n prop: this.prop,\n validate: this.validate,\n resetValue: this.resetValue,\n getValue: this.getValue,\n },\n bubbles: true,\n composed: true, // 穿越 Shadow DOM 边界(FormItem 自身 shadow -> Form host)\n }),\n );\n\n // 子控件值变化时清错(对齐 antd:合法输入即移除错误提示)\n this.el.addEventListener('hbChange', this.handleChildChange);\n }\n\n disconnectedCallback() {\n this.el.removeEventListener('hbChange', this.handleChildChange);\n }\n\n /** 子控件 hbChange 时,若当前有错误则重新校验以尽早清除错误提示 */\n private handleChildChange = () => {\n if (this.errors.length > 0) {\n void this.validate();\n }\n };\n\n private get formRules(): any[] {\n // 合并 prop 对应的 form rules 和自身 rules\n const form = this.el.closest('hb-form');\n const formLevelRules = form ? (form as any).rules?.[this.prop] || [] : [];\n return [...formLevelRules, ...this.rules];\n }\n\n /** 是否渲染必填星号:显式 required 或任一规则带 required(对齐 antd 自动推断) */\n private get isRequired(): boolean {\n if (this.required) return true;\n return this.formRules.some(r => r && r.required);\n }\n\n /** 继承父 hb-form 的 labelPosition(FormItem 自身无此 prop,避免与 Form 冲突时以 Form 为准) */\n private get resolvedLabelPosition(): 'left' | 'right' | 'top' {\n const form = this.el.closest('hb-form') as any;\n return (form && form.labelPosition) || 'right';\n }\n\n /**\n * F4:继承父 hb-form 的 validateMessages(校验提示模板)。\n * 支持占位符 {label}。仅对 rule 未显式指定 message 的规则生效。\n */\n private get resolvedValidateMessages(): Record<string, string> | undefined {\n const form = this.el.closest('hb-form') as any;\n return form && form.validateMessages;\n }\n\n /** 用模板生成错误文案({label} 替换为字段标签),优先级:rule.message > 模板 > 内置默认 */\n private msg(rule: any, key: string, fallback: string): string {\n if (rule.message) return rule.message;\n const tmpl = this.resolvedValidateMessages;\n const label = this.label || this.prop;\n if (tmpl && tmpl[key]) return tmpl[key].replace('{label}', label);\n return fallback.replace('{label}', label);\n }\n\n /**\n * 验证此字段\n */\n @Prop() validate = async (): Promise<string[]> => {\n const value = this.getValue();\n const rules = this.formRules;\n\n if (rules.length === 0) {\n this.errors = [];\n return [];\n }\n\n this.isvalidating = true;\n const errors: string[] = [];\n\n for (const rule of rules) {\n // 必填校验:空字符串 / null / undefined / 空数组均视为\"未填\"\n const isEmpty = value === undefined || value === null || value === '' || (Array.isArray(value) && value.length === 0);\n if (rule.required && isEmpty) {\n errors.push(this.msg(rule, 'required', `${this.label || this.prop}不能为空`));\n continue;\n }\n // 非必填且为空时,跳过后续格式校验(与 antd 行为一致)\n if (isEmpty) continue;\n if (rule.pattern) {\n const ok = typeof rule.pattern === 'string' ? new RegExp(rule.pattern).test(String(value)) : rule.pattern.test(String(value));\n if (!ok) {\n errors.push(this.msg(rule, 'pattern', `${this.label || this.prop}格式不正确`));\n continue;\n }\n }\n if (rule.type === 'email') {\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(String(value))) {\n errors.push(this.msg(rule, 'email', `${this.label || this.prop}格式不正确`));\n continue;\n }\n }\n if (rule.min !== undefined && typeof value === 'string' && value.length < rule.min) {\n errors.push(this.msg(rule, 'min', `${this.label || this.prop}最少${rule.min}个字符`));\n continue;\n }\n if (rule.max !== undefined && typeof value === 'string' && value.length > rule.max) {\n errors.push(this.msg(rule, 'max', `${this.label || this.prop}最多${rule.max}个字符`));\n continue;\n }\n if (rule.validator) {\n try {\n await rule.validator(value, this.formRules);\n } catch (err: any) {\n errors.push(err.message || rule.message || `${this.label || this.prop}验证失败`);\n }\n }\n }\n\n this.errors = errors;\n this.isvalidating = false;\n return errors;\n };\n\n @Prop() resetValue = () => {\n this.errors = [];\n };\n\n @Prop() getValue = (): any => {\n // 覆盖所有表单控件:input/select/cascader/date 系列/数字/checkbox/radio/switch/slider/color-picker/rate/upload\n const input = this.el.querySelector(\n 'hb-input, hb-select, hb-cascader, hb-date-picker, hb-date-time-picker, hb-date-range-picker, ' +\n 'hb-input-number, hb-textarea, hb-checkbox, hb-checkbox-group, hb-radio-group, hb-switch, ' +\n 'hb-slider, hb-color-picker, hb-rate, hb-time-picker, hb-time-select',\n );\n if (input) {\n return (input as any).modelValue;\n }\n return undefined;\n };\n\n /** F6:实际显示的错误文案。手动 help 优先;否则取内部 errors[0] */\n private get displayError(): string {\n if (this.help !== undefined) return this.help;\n return this.errors[0] || '';\n }\n\n /** F6:实际是否显示错误(手动 validateStatus='error' 或内部有错) */\n private get showError(): boolean {\n if (this.validateStatus === 'error') return true;\n if (this.validateStatus === 'success' || this.validateStatus === 'validating') return false;\n return this.errors.length > 0;\n }\n\n render() {\n const labelTop = this.resolvedLabelPosition === 'top';\n const hasError = this.showError;\n // A4:label 与内容关联。子控件是 slot(id 未知),无法用 label.for;\n // 改用 content 容器 role=group + aria-labelledby 指向 label id,\n // 把 label 语义绑到整组;并用 aria-required / aria-invalid 暴露必填与校验态(读屏可感知,\n // 不再仅靠红色 + 隐藏星号)。\n const labelId = this.label ? `hb-form-item__label-${this.prop || 'x'}` : undefined;\n // F7:labelCol 栅格占比(1-24),未设置时回退 labelWidth\n const labelStyle = this.labelCol\n ? { width: `${(this.labelCol / 24) * 100}%` }\n : this.labelWidth\n ? { width: this.labelWidth }\n : undefined;\n return (\n <div\n class={{\n 'hb-form-item': true,\n 'hb-form-item--error': hasError,\n 'hb-form-item--validating': this.isvalidating || this.validateStatus === 'validating',\n 'hb-form-item--required': this.isRequired,\n 'hb-form-item--label-top': labelTop,\n 'hb-form-item--label-left': this.resolvedLabelPosition === 'left',\n }}\n >\n {this.label && (\n <label id={labelId} class=\"hb-form-item__label\" style={labelStyle} title={this.label}>\n {this.isRequired && (\n <span class=\"hb-form-item__required\" aria-hidden=\"true\">\n *\n </span>\n )}\n {this.label}\n </label>\n )}\n <div class=\"hb-form-item__content\" role=\"group\" aria-labelledby={labelId} aria-required={this.isRequired ? 'true' : undefined} aria-invalid={hasError ? 'true' : undefined}>\n <slot />\n {hasError && (\n <div class=\"hb-form-item__error\" role=\"alert\">\n {this.displayError}\n </div>\n )}\n </div>\n </div>\n );\n }\n}\n"],"version":3}
@@ -26,6 +26,30 @@ const Form = class {
26
26
  size = 'default';
27
27
  /** 是否禁用 */
28
28
  disabled = false;
29
+ /**
30
+ * F1:初始值(非受控)。挂载后把这些值写到对应字段的子控件 modelValue。
31
+ * key = FormItem 的 prop。
32
+ */
33
+ initialValues;
34
+ /**
35
+ * F4:校验提示模板。覆盖默认的「{label}不能为空」「{label}格式不正确」等文案。
36
+ * 支持 {label} 占位符。仅对未在 rule.message 显式指定消息的规则生效。
37
+ */
38
+ validateMessages;
39
+ /**
40
+ * F2:提交且校验通过后的回调。参数为各字段当前值组成的对象。
41
+ * 由表单 submit(form onSubmit)触发——表单内放 type="submit" 的按钮即可。
42
+ */
43
+ onFinish;
44
+ /**
45
+ * F2:提交且校验失败后的回调。
46
+ */
47
+ onFinishFailed;
48
+ /**
49
+ * F3:任意字段值变化时的回调。参数为 { prop, value, values }。
50
+ * 由子控件冒泡的 hbChange 触发。
51
+ */
52
+ onValuesChange;
29
53
  fields = [];
30
54
  fieldRegistry = new Map();
31
55
  // 用 componentWillLoad 而非 componentDidLoad 注册监听器:
@@ -37,11 +61,69 @@ const Form = class {
37
61
  const { prop, validate, resetValue, getValue } = e.detail;
38
62
  this.fieldRegistry.set(prop, { prop, validate, resetValue, getValue });
39
63
  });
64
+ /**
65
+ * F3:监听子控件冒泡的 hbChange,识别来源 FormItem 并触发 onValuesChange。
66
+ * 注:hbChange 由 hb-input/hb-select 等控件发出(composed 冒泡穿过 shadow),
67
+ * 这里在 host 上捕获,通过 closest('hb-form-item') 定位字段 prop。
68
+ */
69
+ handleFieldChange = ((e) => {
70
+ if (!this.onValuesChange)
71
+ return;
72
+ const target = e.target;
73
+ const item = target.closest?.('hb-form-item');
74
+ if (!item)
75
+ return;
76
+ const prop = item.prop;
77
+ if (!prop)
78
+ return;
79
+ // 取该字段最新值
80
+ const reg = this.fieldRegistry.get(prop);
81
+ const value = reg ? reg.getValue() : undefined;
82
+ this.onValuesChange({ prop, value, values: this.getFieldsValue() });
83
+ });
40
84
  componentWillLoad() {
41
85
  this.el.addEventListener('hbFormFieldRegister', this.handleFieldRegister);
86
+ // F3:捕获子控件值变化
87
+ this.el.addEventListener('hbChange', this.handleFieldChange);
88
+ }
89
+ componentDidLoad() {
90
+ // F1:把 initialValues 写到对应字段的子控件
91
+ if (this.initialValues) {
92
+ this.fieldRegistry.forEach((_reg, prop) => {
93
+ if (this.initialValues && prop in this.initialValues) {
94
+ this.setFieldValue(prop, this.initialValues[prop]);
95
+ }
96
+ });
97
+ }
42
98
  }
43
99
  disconnectedCallback() {
44
100
  this.el.removeEventListener('hbFormFieldRegister', this.handleFieldRegister);
101
+ this.el.removeEventListener('hbChange', this.handleFieldChange);
102
+ }
103
+ /**
104
+ * 收集所有字段的当前值(getFieldsValue)。
105
+ */
106
+ getFieldsValue() {
107
+ const values = {};
108
+ this.fieldRegistry.forEach((reg, prop) => {
109
+ values[prop] = reg.getValue();
110
+ });
111
+ return values;
112
+ }
113
+ /**
114
+ * 设置某字段子控件的值(setFieldValue)。
115
+ * 子控件统一用 modelValue 受控。
116
+ */
117
+ setFieldValue(prop, value) {
118
+ const formItem = this.el.querySelector(`hb-form-item[prop="${prop}"]`);
119
+ if (!formItem)
120
+ return;
121
+ const control = formItem.querySelector('hb-input, hb-select, hb-cascader, hb-date-picker, hb-date-time-picker, hb-date-range-picker, ' +
122
+ 'hb-input-number, hb-textarea, hb-checkbox, hb-checkbox-group, hb-radio-group, hb-switch, ' +
123
+ 'hb-slider, hb-color-picker, hb-rate, hb-time-picker, hb-time-select');
124
+ if (control) {
125
+ control.modelValue = value;
126
+ }
45
127
  }
46
128
  /**
47
129
  * 验证整个表单
@@ -54,6 +136,27 @@ const Form = class {
54
136
  }));
55
137
  return results.every(Boolean);
56
138
  };
139
+ /**
140
+ * F2:提交表单——校验全部字段,通过则 onFinish(values),失败则 onFinishFailed(errors)。
141
+ */
142
+ handleSubmit = async (e) => {
143
+ e.preventDefault();
144
+ const errorList = [];
145
+ let allOk = true;
146
+ for (const field of this.fieldRegistry.values()) {
147
+ const errors = await field.validate();
148
+ if (errors.length > 0) {
149
+ allOk = false;
150
+ errorList.push({ prop: field.prop, errors });
151
+ }
152
+ }
153
+ if (allOk) {
154
+ this.onFinish?.(this.getFieldsValue());
155
+ }
156
+ else {
157
+ this.onFinishFailed?.(errorList);
158
+ }
159
+ };
57
160
  /**
58
161
  * 重置表单
59
162
  */
@@ -61,11 +164,11 @@ const Form = class {
61
164
  this.fieldRegistry.forEach(field => field.resetValue());
62
165
  };
63
166
  render() {
64
- return (index.h("form", { key: '4e5a8f5d54c6760936fa8735e9a41d7956d631b1', class: {
167
+ return (index.h("form", { key: '876ec07f044efed1a9aa92dbb76642b95013e9a5', class: {
65
168
  'hb-form': true,
66
169
  [`hb-form--label-${this.labelPosition}`]: true,
67
170
  'hb-form--inline': this.inline,
68
- }, onSubmit: e => e.preventDefault(), novalidate: true }, index.h("slot", { key: 'd2e9a364e36387770dc8deb4c42c0e66542cd6a0' })));
171
+ }, onSubmit: this.handleSubmit, novalidate: true }, index.h("slot", { key: '16efaff38a6f97ff1f339eef37f55b7e4aff0f4c' })));
69
172
  }
70
173
  };
71
174
  Form.style = HbFormStyle0;
@@ -1 +1 @@
1
- {"file":"hb-form.entry.cjs.js","mappings":";;;;;;AAAA,MAAM,OAAO,GAAG,i0LAAi0L,CAAC;AACl1L,qBAAe,OAAO;;MCiBT,IAAI;;;;;;IAIP,KAAK,GAAwB,EAAE,CAAC;;IAGhC,KAAK,GAA0B,EAAE,CAAC;;IAGlC,aAAa,GAA6B,OAAO,CAAC;;IAGlD,UAAU,GAAW,MAAM,CAAC;;IAG5B,MAAM,GAAY,KAAK,CAAC;;IAGxB,IAAI,GAAkC,SAAS,CAAC;;IAGhD,QAAQ,GAAY,KAAK,CAAC;IAEzB,MAAM,GAA4B,EAAE,CAAC;IAEtC,aAAa,GAAG,IAAI,GAAG,EAAiC,CAAC;;;;;;IAOzD,mBAAmB,IAAI,CAAC,CAAc;QAC5C,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC;QAC1D,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC;KACxE,EAAmB;IAEpB,iBAAiB;QACf,IAAI,CAAC,EAAE,CAAC,gBAAgB,CAAC,qBAAqB,EAAE,IAAI,CAAC,mBAAmB,CAAC,CAAC;KAC3E;IAED,oBAAoB;QAClB,IAAI,CAAC,EAAE,CAAC,mBAAmB,CAAC,qBAAqB,EAAE,IAAI,CAAC,mBAAmB,CAAC,CAAC;KAC9E;;;;;IAMO,QAAQ,GAAG;QACjB,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAC/B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,OAAM,KAAK;YACrD,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,QAAQ,EAAE,CAAC;YACtC,OAAO,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC;SAC5B,CAAC,CACH,CAAC;QACF,OAAO,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;KAC/B,CAAC;;;;IAKM,WAAW,GAAG;QACpB,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,KAAK,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC;KACzD,CAAC;IAEF,MAAM;QACJ,QACEA,mEACE,KAAK,EAAE;gBACL,SAAS,EAAE,IAAI;gBACf,CAAC,kBAAkB,IAAI,CAAC,aAAa,EAAE,GAAG,IAAI;gBAC9C,iBAAiB,EAAE,IAAI,CAAC,MAAM;aAC/B,EACD,QAAQ,EAAE,CAAC,IAAI,CAAC,CAAC,cAAc,EAAE,EACjC,UAAU,UAEVA,oEAAQ,CACH,EACP;KACH;;;;;;","names":["h"],"sources":["src/components/Form/form.css?tag=hb-form&encapsulation=shadow","src/components/Form/Form.tsx"],"sourcesContent":[":host {\n display: block;\n}\n\n.hb-form {\n display: flex;\n flex-direction: column;\n gap: var(--hb-spacing-md);\n}\n\n.hb-form--inline {\n flex-direction: row;\n flex-wrap: wrap;\n gap: var(--hb-spacing-sm) var(--hb-spacing-lg);\n align-items: flex-start;\n}\n","import { Component, h, Prop, State, Element } from '@stencil/core';\n\ninterface FormFieldRegistration {\n prop: string;\n validate: () => Promise<string[]>;\n resetValue: () => void;\n getValue: () => any;\n}\n\n/**\n * Form 表单组件\n * 由输入框、选择器、单选框、多选框等控件组成,配合表单校验\n */\n@Component({\n tag: 'hb-form',\n styleUrl: 'form.css',\n shadow: true,\n})\nexport class Form {\n @Element() el: HTMLElement;\n\n /** 表单数据对象 */\n @Prop() model: Record<string, any> = {};\n\n /** 表单验证规则 */\n @Prop() rules: Record<string, any[]> = {};\n\n /** 标签位置 */\n @Prop() labelPosition: 'left' | 'right' | 'top' = 'right';\n\n /** 标签宽度 */\n @Prop() labelWidth: string = '80px';\n\n /** 是否行内表单 */\n @Prop() inline: boolean = false;\n\n /** 表单尺寸 */\n @Prop() size: 'large' | 'default' | 'small' = 'default';\n\n /** 是否禁用 */\n @Prop() disabled: boolean = false;\n\n @State() fields: FormFieldRegistration[] = [];\n\n private fieldRegistry = new Map<string, FormFieldRegistration>();\n\n // 用 componentWillLoad 而非 componentDidLoad 注册监听器:\n // Stencil 生命周期里子组件(FormItem)的 componentDidLoad 先于父组件(Form)触发,\n // 若在 componentDidLoad 才挂监听,会错过子项派发的 hbFormFieldRegister,\n // 导致 fieldRegistry 为空、validate() 永远返回 true(真实浏览器里复现,mock-doc 被 waitForChanges 掩盖)。\n // L1:抽命名 handler 以便 disconnectedCallback 正确 remove(修复前匿名箭头无法 remove)。\n private handleFieldRegister = ((e: CustomEvent) => {\n const { prop, validate, resetValue, getValue } = e.detail;\n this.fieldRegistry.set(prop, { prop, validate, resetValue, getValue });\n }) as EventListener;\n\n componentWillLoad() {\n this.el.addEventListener('hbFormFieldRegister', this.handleFieldRegister);\n }\n\n disconnectedCallback() {\n this.el.removeEventListener('hbFormFieldRegister', this.handleFieldRegister);\n }\n\n /**\n * 验证整个表单\n * @returns 是否验证通过\n */\n @Prop() validate = async (): Promise<boolean> => {\n const results = await Promise.all(\n Array.from(this.fieldRegistry.values()).map(async field => {\n const errors = await field.validate();\n return errors.length === 0;\n }),\n );\n return results.every(Boolean);\n };\n\n /**\n * 重置表单\n */\n @Prop() resetFields = (): void => {\n this.fieldRegistry.forEach(field => field.resetValue());\n };\n\n render() {\n return (\n <form\n class={{\n 'hb-form': true,\n [`hb-form--label-${this.labelPosition}`]: true,\n 'hb-form--inline': this.inline,\n }}\n onSubmit={e => e.preventDefault()}\n novalidate\n >\n <slot />\n </form>\n );\n }\n}\n"],"version":3}
1
+ {"file":"hb-form.entry.cjs.js","mappings":"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","names":["h"],"sources":["src/components/Form/form.css?tag=hb-form&encapsulation=shadow","src/components/Form/Form.tsx"],"sourcesContent":[":host {\n display: block;\n}\n\n.hb-form {\n display: flex;\n flex-direction: column;\n gap: var(--hb-spacing-md);\n}\n\n.hb-form--inline {\n flex-direction: row;\n flex-wrap: wrap;\n gap: var(--hb-spacing-sm) var(--hb-spacing-lg);\n align-items: flex-start;\n}\n","import { Component, h, Prop, State, Element } from '@stencil/core';\n\ninterface FormFieldRegistration {\n prop: string;\n validate: () => Promise<string[]>;\n resetValue: () => void;\n getValue: () => any;\n}\n\n/**\n * Form 表单组件\n * 由输入框、选择器、单选框、多选框等控件组成,配合表单校验\n */\n@Component({\n tag: 'hb-form',\n styleUrl: 'form.css',\n shadow: true,\n})\nexport class Form {\n @Element() el: HTMLElement;\n\n /** 表单数据对象 */\n @Prop() model: Record<string, any> = {};\n\n /** 表单验证规则 */\n @Prop() rules: Record<string, any[]> = {};\n\n /** 标签位置 */\n @Prop() labelPosition: 'left' | 'right' | 'top' = 'right';\n\n /** 标签宽度 */\n @Prop() labelWidth: string = '80px';\n\n /** 是否行内表单 */\n @Prop() inline: boolean = false;\n\n /** 表单尺寸 */\n @Prop() size: 'large' | 'default' | 'small' = 'default';\n\n /** 是否禁用 */\n @Prop() disabled: boolean = false;\n\n /**\n * F1:初始值(非受控)。挂载后把这些值写到对应字段的子控件 modelValue。\n * key = FormItem 的 prop。\n */\n @Prop() initialValues?: Record<string, any>;\n\n /**\n * F4:校验提示模板。覆盖默认的「{label}不能为空」「{label}格式不正确」等文案。\n * 支持 {label} 占位符。仅对未在 rule.message 显式指定消息的规则生效。\n */\n @Prop() validateMessages?: { required?: string; pattern?: string; email?: string; min?: string; max?: string };\n\n /**\n * F2:提交且校验通过后的回调。参数为各字段当前值组成的对象。\n * 由表单 submit(form onSubmit)触发——表单内放 type=\"submit\" 的按钮即可。\n */\n @Prop() onFinish?: (values: Record<string, any>) => void;\n\n /**\n * F2:提交且校验失败后的回调。\n */\n @Prop() onFinishFailed?: (errors: { prop: string; errors: string[] }[]) => void;\n\n /**\n * F3:任意字段值变化时的回调。参数为 { prop, value, values }。\n * 由子控件冒泡的 hbChange 触发。\n */\n @Prop() onValuesChange?: (info: { prop: string; value: any; values: Record<string, any> }) => void;\n\n @State() fields: FormFieldRegistration[] = [];\n\n private fieldRegistry = new Map<string, FormFieldRegistration>();\n\n // 用 componentWillLoad 而非 componentDidLoad 注册监听器:\n // Stencil 生命周期里子组件(FormItem)的 componentDidLoad 先于父组件(Form)触发,\n // 若在 componentDidLoad 才挂监听,会错过子项派发的 hbFormFieldRegister,\n // 导致 fieldRegistry 为空、validate() 永远返回 true(真实浏览器里复现,mock-doc 被 waitForChanges 掩盖)。\n // L1:抽命名 handler 以便 disconnectedCallback 正确 remove(修复前匿名箭头无法 remove)。\n private handleFieldRegister = ((e: CustomEvent) => {\n const { prop, validate, resetValue, getValue } = e.detail;\n this.fieldRegistry.set(prop, { prop, validate, resetValue, getValue });\n }) as EventListener;\n\n /**\n * F3:监听子控件冒泡的 hbChange,识别来源 FormItem 并触发 onValuesChange。\n * 注:hbChange 由 hb-input/hb-select 等控件发出(composed 冒泡穿过 shadow),\n * 这里在 host 上捕获,通过 closest('hb-form-item') 定位字段 prop。\n */\n private handleFieldChange = ((e: Event) => {\n if (!this.onValuesChange) return;\n const target = e.target as HTMLElement;\n const item = target.closest?.('hb-form-item') as any;\n if (!item) return;\n const prop = item.prop;\n if (!prop) return;\n // 取该字段最新值\n const reg = this.fieldRegistry.get(prop);\n const value = reg ? reg.getValue() : undefined;\n this.onValuesChange({ prop, value, values: this.getFieldsValue() });\n }) as EventListener;\n\n componentWillLoad() {\n this.el.addEventListener('hbFormFieldRegister', this.handleFieldRegister);\n // F3:捕获子控件值变化\n this.el.addEventListener('hbChange', this.handleFieldChange);\n }\n\n componentDidLoad() {\n // F1:把 initialValues 写到对应字段的子控件\n if (this.initialValues) {\n this.fieldRegistry.forEach((_reg, prop) => {\n if (this.initialValues && prop in this.initialValues) {\n this.setFieldValue(prop, this.initialValues[prop]);\n }\n });\n }\n }\n\n disconnectedCallback() {\n this.el.removeEventListener('hbFormFieldRegister', this.handleFieldRegister);\n this.el.removeEventListener('hbChange', this.handleFieldChange);\n }\n\n /**\n * 收集所有字段的当前值(getFieldsValue)。\n */\n private getFieldsValue(): Record<string, any> {\n const values: Record<string, any> = {};\n this.fieldRegistry.forEach((reg, prop) => {\n values[prop] = reg.getValue();\n });\n return values;\n }\n\n /**\n * 设置某字段子控件的值(setFieldValue)。\n * 子控件统一用 modelValue 受控。\n */\n private setFieldValue(prop: string, value: any) {\n const formItem = this.el.querySelector(`hb-form-item[prop=\"${prop}\"]`) as HTMLElement | null;\n if (!formItem) return;\n const control = formItem.querySelector(\n 'hb-input, hb-select, hb-cascader, hb-date-picker, hb-date-time-picker, hb-date-range-picker, ' +\n 'hb-input-number, hb-textarea, hb-checkbox, hb-checkbox-group, hb-radio-group, hb-switch, ' +\n 'hb-slider, hb-color-picker, hb-rate, hb-time-picker, hb-time-select',\n ) as any;\n if (control) {\n control.modelValue = value;\n }\n }\n\n /**\n * 验证整个表单\n * @returns 是否验证通过\n */\n @Prop() validate = async (): Promise<boolean> => {\n const results = await Promise.all(\n Array.from(this.fieldRegistry.values()).map(async field => {\n const errors = await field.validate();\n return errors.length === 0;\n }),\n );\n return results.every(Boolean);\n };\n\n /**\n * F2:提交表单——校验全部字段,通过则 onFinish(values),失败则 onFinishFailed(errors)。\n */\n private handleSubmit = async (e: Event) => {\n e.preventDefault();\n const errorList: { prop: string; errors: string[] }[] = [];\n let allOk = true;\n for (const field of this.fieldRegistry.values()) {\n const errors = await field.validate();\n if (errors.length > 0) {\n allOk = false;\n errorList.push({ prop: field.prop, errors });\n }\n }\n if (allOk) {\n this.onFinish?.(this.getFieldsValue());\n } else {\n this.onFinishFailed?.(errorList);\n }\n };\n\n /**\n * 重置表单\n */\n @Prop() resetFields = (): void => {\n this.fieldRegistry.forEach(field => field.resetValue());\n };\n\n render() {\n return (\n <form\n class={{\n 'hb-form': true,\n [`hb-form--label-${this.labelPosition}`]: true,\n 'hb-form--inline': this.inline,\n }}\n onSubmit={this.handleSubmit}\n novalidate\n >\n <slot />\n </form>\n );\n }\n}\n"],"version":3}
@@ -6,7 +6,46 @@ const index = require('./index-5aa5c817.js');
6
6
  const clickOutside = require('./click-outside-3fcc00dd.js');
7
7
  const a11y = require('./a11y-4385c871.js');
8
8
 
9
- const selectCss = "/*! tailwindcss v4.1.16 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,::backdrop,:after,:before{--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:host,:root{--font-sans:ui-sans-serif,system-ui,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,::backdrop,:after,:before{border:0 solid;box-sizing:border-box;margin:0;padding:0}::file-selector-button{appearance:button;background-color:#0000;border:0 solid;border-radius:0;box-sizing:border-box;color:inherit;font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;margin:0;margin-inline-end:4px;opacity:1;padding:0}:host,html{-webkit-text-size-adjust:100%;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);line-height:1.5;tab-size:4;-webkit-tap-highlight-color:transparent}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-size:1em;font-variation-settings:var(--default-mono-font-variation-settings,normal)}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}menu,ol,ul{list-style:none}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}button,input,optgroup,select,textarea{background-color:#0000;border-radius:0;color:inherit;font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;opacity:1}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex;padding-block:0}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}} /*! tailwindcss v4.1.16 | MIT License | https://tailwindcss.com */@layer properties{}@layer base{}@layer components;:host{--hb-select-font-size:14px;--hb-select-height:32px;--hb-select-border-color:var(--hb-border-color,#dcdfe6);--hb-select-border-color-hover:var(--hb-color-primary);--hb-select-bg-color:var(--hb-color-white,#fff);--hb-select-text-color:var(--hb-color-text-regular,#606266);--hb-select-placeholder-color:var(--hb-color-text-placeholder,#c0c4cc);display:inline-block;position:relative}.hb-select{display:inline-block;position:relative;width:240px}.hb-select__input-wrapper{position:relative}.hb-select__input,.hb-select__input-wrapper{cursor:pointer;display:inline-block;width:100%}.hb-select__input{background-color:var(--hb-select-bg-color);border:1px solid var(--hb-select-border-color);border-radius:4px;box-sizing:border-box;color:var(--hb-select-text-color);font-size:var(--hb-select-font-size);height:var(--hb-select-height);line-height:var(--hb-select-height);outline:none;padding:0 30px 0 15px;transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.hb-select__input::placeholder{color:var(--hb-select-placeholder-color)}.hb-select__input:focus,.hb-select__input:hover:not(:disabled){border-color:var(--hb-select-border-color-hover)}.hb-select__tags{align-items:center;background-color:var(--hb-select-bg-color);border:1px solid var(--hb-select-border-color);border-radius:4px;display:flex;flex-wrap:wrap;min-height:var(--hb-select-height);padding:0 30px 0 5px}.hb-select__tag{align-items:center;background-color:var(--hb-fill-color-light,#f0f2f5);border:1px solid var(--hb-border-color-lighter,#e4e7ed);border-radius:4px;color:var(--hb-color-text-regular,#606266);display:inline-flex;font-size:12px;height:24px;margin:2px 0 2px 6px;padding:0 8px}.hb-select__tag-text{margin-right:4px}.hb-select__tag-close{align-items:center;color:var(--hb-color-text-placeholder,#c0c4cc);cursor:pointer;display:inline-flex;height:12px;justify-content:center;transition:color .2s;width:12px}.hb-select__tag-close svg{height:100%;width:100%}.hb-select__tag-close:hover{color:var(--hb-color-text-regular,#606266)}.hb-select__suffix{align-items:center;color:var(--hb-select-text-color);display:flex;position:absolute;right:8px;top:50%;transform:translateY(-50%)}.hb-select__clear{align-items:center;color:var(--hb-select-placeholder-color);cursor:pointer;display:inline-flex;height:14px;justify-content:center;margin-right:8px;transition:color .2s;width:14px}.hb-select__clear svg{height:100%;width:100%}.hb-select__clear:hover{color:var(--hb-select-text-color)}.hb-select__arrow{align-items:center;color:var(--hb-select-placeholder-color);display:inline-flex;height:12px;justify-content:center;transition:transform .3s;width:12px}.hb-select__arrow svg{height:100%;width:100%}.hb-select--open .hb-select__arrow{transform:rotate(180deg)}.hb-select__dropdown{background-color:var(--hb-select-bg-color);border:1px solid var(--hb-select-border-color);border-radius:4px;box-shadow:0 2px 12px #0000001a;left:0;margin-top:4px;max-height:300px;min-width:100%;overflow-y:auto;position:absolute;top:100%;z-index:1000}.hb-select__menu{list-style:none;margin:0;padding:6px 0}.hb-select__menu-item{align-items:center;color:var(--hb-select-text-color);cursor:pointer;display:flex;font-size:var(--hb-select-font-size);padding:8px 20px;transition:background-color .2s}.hb-select__menu-item:hover:not(.hb-select__menu-item--disabled):not(.hb-select__menu-item--divided){background-color:var(--hb-fill-color-light,#f5f7fa)}.hb-select__menu-item--selected{color:var(--hb-color-primary);font-weight:600}.hb-select__menu-item--disabled{color:var(--hb-color-text-disabled,#c0c4cc);cursor:not-allowed}.hb-select__menu-item--divided{border-top:1px solid var(--hb-border-color-lighter,#e4e7ed);margin-top:6px;padding-top:6px}.hb-select__menu-item--empty{color:var(--hb-select-placeholder-color);cursor:default}.hb-select__menu-item-checkbox{align-items:center;color:var(--hb-color-primary,#1677ff);display:inline-flex;height:14px;justify-content:center;margin-right:8px;width:14px}.hb-select__menu-item-checkbox svg{height:100%;width:100%}.hb-select__menu-item--create{color:var(--hb-color-primary,#1677ff);font-weight:500}.hb-select__menu-item--create:hover{background-color:var(--hb-color-primary-bg,#e6f4ff)}.hb-select__menu-item-label{flex:1}.hb-select--small{font-size:12px}.hb-select--small .hb-select__input{font-size:12px;height:24px;line-height:24px;padding:0 25px 0 12px}.hb-select--large{font-size:16px}.hb-select--large .hb-select__input{font-size:16px;height:40px;line-height:40px;padding:0 35px 0 18px}.hb-select--disabled{cursor:not-allowed}.hb-select--disabled .hb-select__input{background-color:var(--hb-fill-color-light,#f5f7fa);border-color:var(--hb-select-border-color);color:var(--hb-color-text-disabled,#c0c4cc);cursor:not-allowed}@property --tw-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:\"*\";inherits:false}@property --tw-shadow-alpha{syntax:\"<percentage>\";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:\"*\";inherits:false}@property --tw-inset-shadow-alpha{syntax:\"<percentage>\";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:\"*\";inherits:false}@property --tw-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:\"*\";inherits:false}@property --tw-inset-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:\"*\";inherits:false}@property --tw-ring-offset-width{syntax:\"<length>\";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:\"*\";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}";
9
+ /**
10
+ * 通用虚拟滚动工具(V1,包2)。
11
+ *
12
+ * 从 Table 的虚拟滚动逻辑提取,供 Select/Cascader/Tree/TreeSelect/Menu 复用。
13
+ * 仅渲染「可见窗口 + 上下缓冲」范围的项,而非全量列表——1000+ 选项时 DOM 节点数恒定。
14
+ *
15
+ * 用法(组件内):
16
+ * 1. 维护 scrollTop state(onScroll 回调更新)
17
+ * 2. 调 getVirtualRange({ total, itemHeight, viewportHeight, scrollTop, buffer }) 得 [start, end)
18
+ * 3. 仅渲染 list.slice(start, end),用 getOffsetTop(start, itemHeight) 撑出顶部空白
19
+ */
20
+ /**
21
+ * 计算可见窗口 [startIndex, endIndex)。
22
+ * startIndex 夹到 [0, total),endIndex 夹到 [0, total]。
23
+ */
24
+ function getVirtualRange(opts) {
25
+ const { total, viewportHeight, scrollTop, buffer = 5 } = opts;
26
+ if (total <= 0)
27
+ return { startIndex: 0, endIndex: 0 };
28
+ const itemHeight = opts.itemHeight > 0 ? opts.itemHeight : 1;
29
+ let startIndex = Math.floor(scrollTop / itemHeight) - buffer;
30
+ if (startIndex < 0)
31
+ startIndex = 0;
32
+ let endIndex = Math.ceil((scrollTop + viewportHeight) / itemHeight) + buffer;
33
+ if (endIndex > total)
34
+ endIndex = total;
35
+ return { startIndex, endIndex };
36
+ }
37
+ /** 撑出顶部空白高度(px),用于偏移可见窗口到正确滚动位置。 */
38
+ function getOffsetTop(startIndex, itemHeight) {
39
+ const h = itemHeight > 0 ? itemHeight : 1;
40
+ return startIndex * h;
41
+ }
42
+ /** 容器总高度(px),用于撑出滚动条。 */
43
+ function getTotalHeight(total, itemHeight) {
44
+ const h = itemHeight > 0 ? itemHeight : 1;
45
+ return total * h;
46
+ }
47
+
48
+ const selectCss = "/*! tailwindcss v4.1.16 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,::backdrop,:after,:before{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:host,:root{--font-sans:ui-sans-serif,system-ui,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,::backdrop,:after,:before{border:0 solid;box-sizing:border-box;margin:0;padding:0}::file-selector-button{appearance:button;background-color:#0000;border:0 solid;border-radius:0;box-sizing:border-box;color:inherit;font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;margin:0;margin-inline-end:4px;opacity:1;padding:0}:host,html{-webkit-text-size-adjust:100%;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);line-height:1.5;tab-size:4;-webkit-tap-highlight-color:transparent}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-size:1em;font-variation-settings:var(--default-mono-font-variation-settings,normal)}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}menu,ol,ul{list-style:none}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}button,input,optgroup,select,textarea{background-color:#0000;border-radius:0;color:inherit;font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;opacity:1}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex;padding-block:0}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.relative{position:relative}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}} /*! tailwindcss v4.1.16 | MIT License | https://tailwindcss.com */@layer properties{}@layer base{}@layer components;:host{--hb-select-font-size:14px;--hb-select-height:32px;--hb-select-border-color:var(--hb-border-color,#dcdfe6);--hb-select-border-color-hover:var(--hb-color-primary);--hb-select-bg-color:var(--hb-color-white,#fff);--hb-select-text-color:var(--hb-color-text-regular,#606266);--hb-select-placeholder-color:var(--hb-color-text-placeholder,#c0c4cc);display:inline-block;position:relative}.hb-select{display:inline-block;position:relative;width:240px}.hb-select__input-wrapper{position:relative}.hb-select__input,.hb-select__input-wrapper{cursor:pointer;display:inline-block;width:100%}.hb-select__input{background-color:var(--hb-select-bg-color);border:1px solid var(--hb-select-border-color);border-radius:4px;box-sizing:border-box;color:var(--hb-select-text-color);font-size:var(--hb-select-font-size);height:var(--hb-select-height);line-height:var(--hb-select-height);outline:none;padding:0 30px 0 15px;transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.hb-select__input::placeholder{color:var(--hb-select-placeholder-color)}.hb-select__input:focus,.hb-select__input:hover:not(:disabled){border-color:var(--hb-select-border-color-hover)}.hb-select__tags{align-items:center;background-color:var(--hb-select-bg-color);border:1px solid var(--hb-select-border-color);border-radius:4px;display:flex;flex-wrap:wrap;min-height:var(--hb-select-height);padding:0 30px 0 5px}.hb-select__tag{align-items:center;background-color:var(--hb-fill-color-light,#f0f2f5);border:1px solid var(--hb-border-color-lighter,#e4e7ed);border-radius:4px;color:var(--hb-color-text-regular,#606266);display:inline-flex;font-size:12px;height:24px;margin:2px 0 2px 6px;padding:0 8px}.hb-select__tag-text{margin-right:4px}.hb-select__tag-close{align-items:center;color:var(--hb-color-text-placeholder,#c0c4cc);cursor:pointer;display:inline-flex;height:12px;justify-content:center;transition:color .2s;width:12px}.hb-select__tag-close svg{height:100%;width:100%}.hb-select__tag-close:hover{color:var(--hb-color-text-regular,#606266)}.hb-select__suffix{align-items:center;color:var(--hb-select-text-color);display:flex;position:absolute;right:8px;top:50%;transform:translateY(-50%)}.hb-select__clear{align-items:center;color:var(--hb-select-placeholder-color);cursor:pointer;display:inline-flex;height:14px;justify-content:center;margin-right:8px;transition:color .2s;width:14px}.hb-select__clear svg{height:100%;width:100%}.hb-select__clear:hover{color:var(--hb-select-text-color)}.hb-select__arrow{align-items:center;color:var(--hb-select-placeholder-color);display:inline-flex;height:12px;justify-content:center;transition:transform .3s;width:12px}.hb-select__arrow svg{height:100%;width:100%}.hb-select--open .hb-select__arrow{transform:rotate(180deg)}.hb-select__dropdown{background-color:var(--hb-select-bg-color);border:1px solid var(--hb-select-border-color);border-radius:4px;box-shadow:0 2px 12px #0000001a;left:0;margin-top:4px;max-height:300px;min-width:100%;overflow-y:auto;position:absolute;top:100%;z-index:1000}.hb-select__menu{list-style:none;margin:0;padding:6px 0}.hb-select__menu-item{align-items:center;color:var(--hb-select-text-color);cursor:pointer;display:flex;font-size:var(--hb-select-font-size);padding:8px 20px;transition:background-color .2s}.hb-select__menu-item:hover:not(.hb-select__menu-item--disabled):not(.hb-select__menu-item--divided){background-color:var(--hb-fill-color-light,#f5f7fa)}.hb-select__menu-item--selected{color:var(--hb-color-primary);font-weight:600}.hb-select__menu-item--disabled{color:var(--hb-color-text-disabled,#c0c4cc);cursor:not-allowed}.hb-select__menu-item--divided{border-top:1px solid var(--hb-border-color-lighter,#e4e7ed);margin-top:6px;padding-top:6px}.hb-select__menu-item--empty{color:var(--hb-select-placeholder-color);cursor:default}.hb-select__menu-item-checkbox{align-items:center;color:var(--hb-color-primary,#1677ff);display:inline-flex;height:14px;justify-content:center;margin-right:8px;width:14px}.hb-select__menu-item-checkbox svg{height:100%;width:100%}.hb-select__menu-item--create{color:var(--hb-color-primary,#1677ff);font-weight:500}.hb-select__menu-item--create:hover{background-color:var(--hb-color-primary-bg,#e6f4ff)}.hb-select__menu-item-label{flex:1}.hb-select--small{font-size:12px}.hb-select--small .hb-select__input{font-size:12px;height:24px;line-height:24px;padding:0 25px 0 12px}.hb-select--large{font-size:16px}.hb-select--large .hb-select__input{font-size:16px;height:40px;line-height:40px;padding:0 35px 0 18px}.hb-select--disabled{cursor:not-allowed}.hb-select--disabled .hb-select__input{background-color:var(--hb-fill-color-light,#f5f7fa);border-color:var(--hb-select-border-color);color:var(--hb-color-text-disabled,#c0c4cc);cursor:not-allowed}@property --tw-rotate-x{syntax:\"*\";inherits:false}@property --tw-rotate-y{syntax:\"*\";inherits:false}@property --tw-rotate-z{syntax:\"*\";inherits:false}@property --tw-skew-x{syntax:\"*\";inherits:false}@property --tw-skew-y{syntax:\"*\";inherits:false}@property --tw-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:\"*\";inherits:false}@property --tw-shadow-alpha{syntax:\"<percentage>\";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:\"*\";inherits:false}@property --tw-inset-shadow-alpha{syntax:\"<percentage>\";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:\"*\";inherits:false}@property --tw-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:\"*\";inherits:false}@property --tw-inset-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:\"*\";inherits:false}@property --tw-ring-offset-width{syntax:\"<length>\";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:\"*\";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}";
10
49
  const HbSelectStyle0 = selectCss;
11
50
 
12
51
  const Select = class {
@@ -75,12 +114,32 @@ const Select = class {
75
114
  * 多选时最多显示多少个tag
76
115
  */
77
116
  maxCollapseTags;
117
+ /**
118
+ * V1:是否开启虚拟滚动(1000+ 选项时推荐)。默认 false。
119
+ * 开启后下拉仅渲染可见窗口内的选项,DOM 节点数恒定。
120
+ */
121
+ virtual = false;
122
+ /**
123
+ * V1:下拉最大高度(px)。virtual=true 时必填(定义视口高度)。默认 256。
124
+ */
125
+ dropdownMaxHeight = 256;
126
+ /**
127
+ * V1:虚拟滚动每项预估高度(px)。默认 34。
128
+ */
129
+ virtualItemHeight = 34;
78
130
  isOpen = false;
79
131
  inputValue = '';
80
132
  searchValue = '';
81
133
  filteredOptions = [];
82
134
  /** 键盘高亮的选项索引(-1 表示无) */
83
135
  activeOptionIndex = -1;
136
+ /** V1:下拉滚动位置(px),驱动虚拟滚动可见窗口重算 */
137
+ dropdownScrollTop = 0;
138
+ /** V1:下拉滚动回调 */
139
+ handleDropdownScroll = (e) => {
140
+ const target = e.target;
141
+ this.dropdownScrollTop = target.scrollTop;
142
+ };
84
143
  /**
85
144
  * O4:实例级稳定 id,用于 combobox 的 aria-controls / aria-activedescendant 关联,
86
145
  * 让读屏在键盘导航时播报当前高亮项。
@@ -276,27 +335,40 @@ const Select = class {
276
335
  this.updateInputValue();
277
336
  this.hbChange.emit(this.modelValue);
278
337
  };
338
+ /** 渲染单个选项 <li>(普通 + 虚拟滚动共用,V1 提取) */
339
+ renderOption(option, index$1) {
340
+ return (index.h("li", { id: `${this.listboxId}-opt-${index$1}`, class: {
341
+ 'hb-select__menu-item': true,
342
+ 'hb-select__menu-item--selected': this.isSelected(option),
343
+ 'hb-select__menu-item--disabled': option.disabled,
344
+ 'hb-select__menu-item--divided': option.divided,
345
+ 'hb-select__menu-item--active': index$1 === this.activeOptionIndex,
346
+ }, style: this.virtual ? { height: `${this.virtualItemHeight}px` } : undefined, role: "option", "aria-selected": this.isSelected(option) ? 'true' : 'false', "aria-disabled": option.disabled ? 'true' : undefined, onClick: () => this.handleSelect(option) }, this.multiple && (index.h("span", { class: "hb-select__menu-item-checkbox", "aria-hidden": "true" }, this.isSelected(option) ? (index.h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": 3, "stroke-linecap": "round", "stroke-linejoin": "round" }, index.h("path", { d: "M5 12l5 5L20 7" }))) : null)), index.h("span", { class: "hb-select__menu-item-label" }, option.label)));
347
+ }
279
348
  render() {
280
349
  const displayOptions = this.filterable ? this.filteredOptions : this.options;
281
350
  const showClear = this.clearable && this.modelValue !== undefined && (this.multiple ? this.modelValue.length > 0 : true);
282
- return (index.h("div", { key: 'b4a502d0791ec418009f85cb7f56e28dd883a29d', class: {
351
+ return (index.h("div", { key: '711a2660ec0ff788a027b7097a64f74ee535ae58', class: {
283
352
  'hb-select': true,
284
353
  'hb-select--open': this.isOpen,
285
354
  'hb-select--disabled': this.disabled,
286
355
  [`hb-select--${this.size}`]: true,
287
- }, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": this.isOpen ? 'true' : 'false', "aria-controls": this.isOpen ? this.listboxId : undefined, "aria-activedescendant": this.isOpen && this.activeOptionIndex >= 0 ? `${this.listboxId}-opt-${this.activeOptionIndex}` : undefined }, index.h("div", { key: '1d94bc1d92b716386d6f791e5cb0b4aa5f4e7c6b', class: "hb-select__input-wrapper", onClick: this.handleInputClick, onKeyDown: this.handleWrapperKeydown }, this.multiple && Array.isArray(this.modelValue) && this.modelValue.length > 0 ? (index.h("div", { class: "hb-select__tags" }, this.modelValue.slice(0, this.collapseTags && this.maxCollapseTags ? this.maxCollapseTags : undefined).map(val => {
356
+ }, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": this.isOpen ? 'true' : 'false', "aria-controls": this.isOpen ? this.listboxId : undefined, "aria-activedescendant": this.isOpen && this.activeOptionIndex >= 0 ? `${this.listboxId}-opt-${this.activeOptionIndex}` : undefined }, index.h("div", { key: '7719c10931fd36306a7bc302b167c43a782158bd', class: "hb-select__input-wrapper", onClick: this.handleInputClick, onKeyDown: this.handleWrapperKeydown }, this.multiple && Array.isArray(this.modelValue) && this.modelValue.length > 0 ? (index.h("div", { class: "hb-select__tags" }, this.modelValue.slice(0, this.collapseTags && this.maxCollapseTags ? this.maxCollapseTags : undefined).map(val => {
288
357
  const option = this.options.find(opt => opt.value === val);
289
358
  return (index.h("span", { class: "hb-select__tag" }, index.h("span", { class: "hb-select__tag-text" }, option ? option.label : val), index.h("span", { class: "hb-select__tag-close", role: "button", "aria-label": "\u79FB\u9664\u6807\u7B7E", tabIndex: 0, onClick: e => {
290
359
  e.stopPropagation();
291
360
  this.handleSelect(option || { value: val, label: String(val) });
292
361
  }, onKeyDown: a11y.activationClickHandler }, index.h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": 2, "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true" }, index.h("path", { d: "M6 6l12 12M18 6L6 18" })))));
293
- }), this.collapseTags && this.maxCollapseTags && this.modelValue.length > this.maxCollapseTags && (index.h("span", { class: "hb-select__tag" }, index.h("span", { class: "hb-select__tag-text" }, "+", this.modelValue.length - this.maxCollapseTags))))) : (index.h("input", { type: "text", class: "hb-select__input", placeholder: this.placeholder, value: this.filterable && this.isOpen ? this.searchValue : this.inputValue, disabled: this.disabled, readonly: !this.filterable || !this.isOpen, onInput: this.handleSearch, "aria-haspopup": "listbox", "aria-expanded": this.isOpen ? 'true' : 'false', role: "combobox" })), index.h("span", { key: 'a3baeacc8cdbf27eba98f6598691bda8d2eb05a1', class: "hb-select__suffix" }, showClear && (index.h("span", { key: 'a6949d1ab21d12029532dc74d8211075d2eef3cc', class: "hb-select__clear", role: "button", "aria-label": "\u6E05\u7A7A", tabIndex: 0, onClick: this.handleClear, onKeyDown: a11y.activationClickHandler }, index.h("svg", { key: 'e9c4b5432f7880392485135eeab6ea704a6b8276', viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": 2, "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true" }, index.h("path", { key: '2a8a040a65bb299cad10023d22f4875c46dfb532', d: "M6 6l12 12M18 6L6 18" })))), index.h("span", { key: '5b7c7a3cf6adfc64b7310e5564b447256cf317cb', class: "hb-select__arrow", "aria-hidden": "true" }, index.h("svg", { key: '72d1d5fe5ec09baf891abc60c0c08e1803c82979', viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": 2, "stroke-linecap": "round", "stroke-linejoin": "round" }, index.h("path", { key: '136724fc7a387c0831a703f5561e40b9681f7c08', d: "M6 9l6 6 6-6" }))))), this.isOpen && (index.h("div", { key: '25f7454d0e3bbcaacc1032d97fbcb3a7bd40300b', class: "hb-select__dropdown" }, index.h("ul", { key: '41037c823d64c4c5b4fc7dac74c7ae39a8557867', class: "hb-select__menu", role: "listbox", id: this.listboxId }, this.createOption && (index.h("li", { key: '25337a9a9f1ee67957aacad3c106edbc57284029', class: { 'hb-select__menu-item': true, 'hb-select__menu-item--create': true }, role: "option", "aria-selected": "false", onClick: this.handleCreate }, index.h("span", { key: '43b94deb1f4d4ab2fac5d248a8fb538dd476e7de', class: "hb-select__menu-item-label" }, "\u521B\u5EFA\u300C", this.createOption.label, "\u300D"))), displayOptions.length === 0 && !this.createOption ? (index.h("li", { class: "hb-select__menu-item hb-select__menu-item--empty", "aria-disabled": "true" }, "\u65E0\u6570\u636E")) : (displayOptions.map((option, index$1) => (index.h("li", { id: `${this.listboxId}-opt-${index$1}`, class: {
294
- 'hb-select__menu-item': true,
295
- 'hb-select__menu-item--selected': this.isSelected(option),
296
- 'hb-select__menu-item--disabled': option.disabled,
297
- 'hb-select__menu-item--divided': option.divided,
298
- 'hb-select__menu-item--active': index$1 === this.activeOptionIndex,
299
- }, role: "option", "aria-selected": this.isSelected(option) ? 'true' : 'false', "aria-disabled": option.disabled ? 'true' : undefined, onClick: () => this.handleSelect(option) }, this.multiple && (index.h("span", { class: "hb-select__menu-item-checkbox", "aria-hidden": "true" }, this.isSelected(option) ? (index.h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": 3, "stroke-linecap": "round", "stroke-linejoin": "round" }, index.h("path", { d: "M5 12l5 5L20 7" }))) : null)), index.h("span", { class: "hb-select__menu-item-label" }, option.label))))))))));
362
+ }), this.collapseTags && this.maxCollapseTags && this.modelValue.length > this.maxCollapseTags && (index.h("span", { class: "hb-select__tag" }, index.h("span", { class: "hb-select__tag-text" }, "+", this.modelValue.length - this.maxCollapseTags))))) : (index.h("input", { type: "text", class: "hb-select__input", placeholder: this.placeholder, value: this.filterable && this.isOpen ? this.searchValue : this.inputValue, disabled: this.disabled, readonly: !this.filterable || !this.isOpen, onInput: this.handleSearch, "aria-haspopup": "listbox", "aria-expanded": this.isOpen ? 'true' : 'false', role: "combobox" })), index.h("span", { key: 'a9dc4ecd508bb6828296cf2a9465ab5c24595573', class: "hb-select__suffix" }, showClear && (index.h("span", { key: 'cde7a95063f12c03844303681a3a95da1f5aaa66', class: "hb-select__clear", role: "button", "aria-label": "\u6E05\u7A7A", tabIndex: 0, onClick: this.handleClear, onKeyDown: a11y.activationClickHandler }, index.h("svg", { key: 'a9abef626a4a3da495afffe7e562fb557a26f8e7', viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": 2, "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true" }, index.h("path", { key: 'a95c9113f3c7ef741dc9a8111576827571668ecf', d: "M6 6l12 12M18 6L6 18" })))), index.h("span", { key: '184023640706c01ed18854cb5b1975f9def18dc4', class: "hb-select__arrow", "aria-hidden": "true" }, index.h("svg", { key: '14fec9c7d46ad1d22ddf07de502aedabe8b19b18', viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": 2, "stroke-linecap": "round", "stroke-linejoin": "round" }, index.h("path", { key: 'd280de7e1545be150238126b65f6c37e499bb83a', d: "M6 9l6 6 6-6" }))))), this.isOpen && (index.h("div", { key: 'e9a10e24e49095535c232ec39be458780789fde4', class: "hb-select__dropdown" }, index.h("ul", { key: '874854750698dcb2346cbd3a965ba0e500b5bafc', class: "hb-select__menu", role: "listbox", id: this.listboxId,
363
+ // V1:虚拟滚动时滚动容器限高 + onScroll 更新可见窗口
364
+ style: this.virtual ? { maxHeight: `${this.dropdownMaxHeight}px`, overflowY: 'auto' } : undefined, onScroll: this.virtual ? this.handleDropdownScroll : undefined }, this.createOption && (index.h("li", { key: '5ca5a633a0d69ddb1ba065bb08d947d663b774ac', class: { 'hb-select__menu-item': true, 'hb-select__menu-item--create': true }, role: "option", "aria-selected": "false", onClick: this.handleCreate }, index.h("span", { key: 'f986194f0456c910bba2447603505edce6cb6e60', class: "hb-select__menu-item-label" }, "\u521B\u5EFA\u300C", this.createOption.label, "\u300D"))), displayOptions.length === 0 && !this.createOption ? (index.h("li", { class: "hb-select__menu-item hb-select__menu-item--empty", "aria-disabled": "true" }, "\u65E0\u6570\u636E")) : this.virtual ? (
365
+ // V1:虚拟滚动——仅渲染可见窗口 + 顶部撑高 spacer
366
+ index.h("div", { style: { height: `${getTotalHeight(displayOptions.length, this.virtualItemHeight)}px`, position: 'relative' } }, index.h("div", { style: { transform: `translateY(${getOffsetTop(getVirtualRange({ total: displayOptions.length, itemHeight: this.virtualItemHeight, viewportHeight: this.dropdownMaxHeight, scrollTop: this.dropdownScrollTop }).startIndex, this.virtualItemHeight)}px)` } }, displayOptions
367
+ .slice(getVirtualRange({ total: displayOptions.length, itemHeight: this.virtualItemHeight, viewportHeight: this.dropdownMaxHeight, scrollTop: this.dropdownScrollTop }).startIndex, getVirtualRange({ total: displayOptions.length, itemHeight: this.virtualItemHeight, viewportHeight: this.dropdownMaxHeight, scrollTop: this.dropdownScrollTop }).endIndex)
368
+ .map((option, i) => {
369
+ const absIndex = getVirtualRange({ total: displayOptions.length, itemHeight: this.virtualItemHeight, viewportHeight: this.dropdownMaxHeight, scrollTop: this.dropdownScrollTop }).startIndex + i;
370
+ return this.renderOption(option, absIndex);
371
+ })))) : (displayOptions.map((option, index) => this.renderOption(option, index))))))));
300
372
  }
301
373
  static get watchers() { return {
302
374
  "modelValue": ["handleValueChange"],