@v-c/picker 1.2.2 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/LICENSE +21 -21
  2. package/dist/PickerInput/Popup/index.d.ts +13 -1
  3. package/dist/PickerInput/Popup/index.js +10 -1
  4. package/dist/PickerInput/RangePicker.js +55 -72
  5. package/dist/PickerInput/Selector/ClearIcon.d.ts +11 -0
  6. package/dist/PickerInput/Selector/ClearIcon.js +53 -0
  7. package/dist/PickerInput/Selector/Icon.d.ts +0 -5
  8. package/dist/PickerInput/Selector/Icon.js +16 -57
  9. package/dist/PickerInput/Selector/Input.js +2 -5
  10. package/dist/PickerInput/Selector/RangeSelector.js +9 -5
  11. package/dist/PickerInput/Selector/SingleSelector/index.d.ts +1 -1
  12. package/dist/PickerInput/Selector/SingleSelector/index.js +5 -8
  13. package/dist/PickerInput/SinglePicker.js +56 -40
  14. package/dist/PickerInput/hooks/useFocusEvents.d.ts +28 -0
  15. package/dist/PickerInput/hooks/useFocusEvents.js +36 -0
  16. package/dist/PickerInput/hooks/useFocusLock.d.ts +14 -0
  17. package/dist/PickerInput/hooks/useFocusLock.js +25 -0
  18. package/dist/PickerInput/hooks/useRangeDisabledDate.d.ts +1 -1
  19. package/dist/PickerInput/hooks/useRangeDisabledDate.js +3 -3
  20. package/dist/PickerInput/hooks/useRangePickerValue.d.ts +7 -1
  21. package/dist/PickerInput/hooks/useRangePickerValue.js +10 -16
  22. package/dist/PickerInput/hooks/useRangeValue.d.ts +3 -3
  23. package/dist/PickerInput/hooks/useRangeValue.js +15 -13
  24. package/dist/PickerInput/hooks/useRangeValueChange.d.ts +109 -0
  25. package/dist/PickerInput/hooks/useRangeValueChange.js +226 -0
  26. package/dist/PickerPanel/PanelBody.js +2 -1
  27. package/dist/PickerPanel/PanelHeader.js +2 -1
  28. package/dist/locale/am_ET.js +4 -4
  29. package/dist/locale/ar_EG.js +4 -4
  30. package/dist/locale/az_AZ.js +4 -4
  31. package/dist/locale/bg_BG.js +4 -4
  32. package/dist/locale/bn_BD.js +4 -4
  33. package/dist/locale/by_BY.js +4 -4
  34. package/dist/locale/ca_ES.js +4 -4
  35. package/dist/locale/cs_CZ.js +4 -4
  36. package/dist/locale/da_DK.js +4 -4
  37. package/dist/locale/de_DE.js +4 -4
  38. package/dist/locale/el_GR.js +4 -4
  39. package/dist/locale/en_GB.js +4 -4
  40. package/dist/locale/en_US.js +4 -4
  41. package/dist/locale/es_ES.js +4 -4
  42. package/dist/locale/es_MX.js +4 -4
  43. package/dist/locale/et_EE.js +4 -4
  44. package/dist/locale/eu_ES.js +4 -4
  45. package/dist/locale/fa_IR.js +4 -4
  46. package/dist/locale/fi_FI.js +4 -4
  47. package/dist/locale/fr_BE.js +4 -4
  48. package/dist/locale/fr_CA.js +4 -4
  49. package/dist/locale/fr_FR.js +4 -4
  50. package/dist/locale/ga_IE.js +4 -4
  51. package/dist/locale/gl_ES.js +4 -4
  52. package/dist/locale/he_IL.js +4 -4
  53. package/dist/locale/hi_IN.js +4 -4
  54. package/dist/locale/hr_HR.js +4 -4
  55. package/dist/locale/hu_HU.js +4 -4
  56. package/dist/locale/id_ID.js +4 -4
  57. package/dist/locale/is_IS.js +4 -4
  58. package/dist/locale/it_IT.js +4 -4
  59. package/dist/locale/ja_JP.js +4 -4
  60. package/dist/locale/ka_GE.js +4 -4
  61. package/dist/locale/kk_KZ.js +4 -4
  62. package/dist/locale/km_KH.js +4 -4
  63. package/dist/locale/kmr_IQ.js +4 -4
  64. package/dist/locale/kn_IN.js +4 -4
  65. package/dist/locale/ko_KR.js +4 -4
  66. package/dist/locale/lt_LT.js +4 -4
  67. package/dist/locale/lv_LV.js +4 -4
  68. package/dist/locale/mk_MK.js +4 -4
  69. package/dist/locale/ml_IN.js +4 -4
  70. package/dist/locale/mn_MN.js +4 -4
  71. package/dist/locale/mr_IN.js +4 -4
  72. package/dist/locale/ms_MY.js +2 -2
  73. package/dist/locale/my_MM.js +4 -4
  74. package/dist/locale/nb_NO.js +4 -4
  75. package/dist/locale/ne_NP.js +4 -4
  76. package/dist/locale/nl_BE.js +4 -4
  77. package/dist/locale/nl_NL.js +4 -4
  78. package/dist/locale/pl_PL.js +4 -4
  79. package/dist/locale/pt_BR.js +4 -4
  80. package/dist/locale/pt_PT.js +4 -4
  81. package/dist/locale/ro_RO.js +4 -4
  82. package/dist/locale/ru_RU.js +4 -4
  83. package/dist/locale/si_LK.js +4 -4
  84. package/dist/locale/sk_SK.js +4 -4
  85. package/dist/locale/sl_SI.js +4 -4
  86. package/dist/locale/sr_Cyrl_RS.js +4 -4
  87. package/dist/locale/sr_RS.js +4 -4
  88. package/dist/locale/sv_SE.js +4 -4
  89. package/dist/locale/ta_IN.js +4 -4
  90. package/dist/locale/th_TH.js +4 -4
  91. package/dist/locale/tk_TK.js +4 -4
  92. package/dist/locale/tr_TR.js +4 -4
  93. package/dist/locale/ug_CN.js +4 -4
  94. package/dist/locale/uk_UA.js +4 -4
  95. package/dist/locale/ur_PK.js +4 -4
  96. package/dist/locale/uz_UZ.js +4 -4
  97. package/dist/locale/vi_VN.js +4 -4
  98. package/dist/locale/zh_CN.js +4 -4
  99. package/dist/locale/zh_TW.js +4 -4
  100. package/dist/utils/miscUtil.d.ts +1 -1
  101. package/dist/utils/miscUtil.js +3 -4
  102. package/package.json +4 -4
  103. package/dist/PickerInput/hooks/useRangeActive.d.ts +0 -9
  104. package/dist/PickerInput/hooks/useRangeActive.js +0 -59
@@ -7,11 +7,12 @@ import { formatValue, formatValues } from "../utils/valueUtil.js";
7
7
  import useCellRender from "./hooks/useCellRender.js";
8
8
  import useFieldsInvalidate from "./hooks/useFieldsInvalidate.js";
9
9
  import useFilledProps from "./hooks/useFilledProps.js";
10
+ import useFocusEvents, { isTargetInContainers } from "./hooks/useFocusEvents.js";
10
11
  import useOpen from "./hooks/useOpen.js";
11
12
  import usePresets from "./hooks/usePresets.js";
12
- import useRangeActive from "./hooks/useRangeActive.js";
13
13
  import useRangePickerValue from "./hooks/useRangePickerValue.js";
14
14
  import useRangeValue, { useInnerValue } from "./hooks/useRangeValue.js";
15
+ import useRangeValueChange from "./hooks/useRangeValueChange.js";
15
16
  import useShowNow from "./hooks/useShowNow.js";
16
17
  import useToggleDates from "../hooks/useToggleDates.js";
17
18
  import Popup from "./Popup/index.js";
@@ -117,15 +118,15 @@ var SinglePicker = /* @__PURE__ */ defineComponent((props, { expose }) => {
117
118
  };
118
119
  const [mergedValue, setInnerValue, getCalendarValue, triggerCalendarChange, triggerOk] = useInnerValue(generateConfig, locale, formatList, ref(false), order, defaultValue, value, onInternalCalendarChange, onInternalOk);
119
120
  const calendarValue = computed(() => getCalendarValue.value);
120
- const [focused, triggerFocus, lastOperation, activeIndex] = useRangeActive(computed(() => [disabled.value]));
121
- const onSharedFocus = (event) => {
122
- triggerFocus(true);
121
+ const popupRef = ref();
122
+ const isInternalPickerElement = (element) => isTargetInContainers(element, [selectorRef.value?.nativeElement, popupRef.value]);
123
+ const [focused, onFieldFocus, onFieldBlur] = useFocusEvents(isInternalPickerElement, (_index, event) => {
123
124
  onFocus.value?.(event, {});
124
- };
125
- const onSharedBlur = (event) => {
126
- triggerFocus(false);
125
+ }, (_index, event) => {
127
126
  onBlur.value?.(event, {});
128
- };
127
+ }, () => {
128
+ triggerOpen(false);
129
+ });
129
130
  const mergedMode = ref(picker.value ?? mode.value);
130
131
  watch(picker, () => {
131
132
  mergedMode.value = picker.value;
@@ -139,12 +140,32 @@ var SinglePicker = /* @__PURE__ */ defineComponent((props, { expose }) => {
139
140
  const onInternalChange = (dates, dateStrings) => {
140
141
  if (props?.onChange) props?.onChange?.(toValueByFormat(dates), pickerParam(dateStrings));
141
142
  };
142
- const [, triggerSubmitChange] = useRangeValue(computed(() => {
143
+ const [, triggerSubmitChange, resetValue] = useRangeValue(computed(() => {
143
144
  return {
144
145
  ...fp.value,
145
146
  onChange: onInternalChange
146
147
  };
147
- }), mergedValue, setInnerValue, () => getCalendarValue.value, triggerCalendarChange, computed(() => []), formatList, focused, mergedOpen, isInvalidateDate);
148
+ }), mergedValue, setInnerValue, () => getCalendarValue.value, triggerCalendarChange, computed(() => []), formatList, isInvalidateDate);
149
+ const getFieldCalendarValue = () => {
150
+ const values = getCalendarValue.value;
151
+ return [values.length ? values : null];
152
+ };
153
+ const triggerFieldCalendarChange = (_index, nextValues) => {
154
+ triggerCalendarChange(nextValues);
155
+ };
156
+ const flushFieldSubmit = (_index, needTriggerChange) => {
157
+ if (needTriggerChange) {
158
+ triggerSubmitChange(getCalendarValue.value);
159
+ triggerOpen(false, { force: true });
160
+ }
161
+ };
162
+ const resetFieldValue = () => {
163
+ resetValue();
164
+ };
165
+ const [, activeIndex, , , triggerSingleValueChange, resetSingleValueChange] = useRangeValueChange(ref(1), needConfirm, ref([false]), getFieldCalendarValue, triggerFieldCalendarChange, flushFieldSubmit, resetFieldValue);
166
+ watch(mergedOpen, (open) => {
167
+ if (!open) triggerSingleValueChange(0, "popupClose");
168
+ });
148
169
  const [submitInvalidates, onSelectorInvalid] = useFieldsInvalidate(calendarValue, isInvalidateDate);
149
170
  const submitInvalidate = computed(() => submitInvalidates.value.some((invalidated) => invalidated));
150
171
  const onInternalPickerValueChange = (dates, info) => {
@@ -157,7 +178,7 @@ var SinglePicker = /* @__PURE__ */ defineComponent((props, { expose }) => {
157
178
  onPickerValueChange.value(dates[0], cleanInfo);
158
179
  }
159
180
  };
160
- const [currentPickerValue, setCurrentPickerValue] = useRangePickerValue(generateConfig, locale, calendarValue, computed(() => [mergedMode.value]), mergedOpen, activeIndex, internalPicker, ref(false), defaultPickerValue, pickerValue, computed(() => toArray(showTime.value?.defaultOpenValue)), onInternalPickerValueChange, minDate, maxDate);
181
+ const [currentPickerValue, setCurrentPickerValue] = useRangePickerValue(generateConfig, locale, calendarValue, computed(() => [mergedMode.value]), mergedOpen, ref(false), activeIndex, internalPicker, ref(false), defaultPickerValue, pickerValue, computed(() => toArray(showTime.value?.defaultOpenValue)), onInternalPickerValueChange, minDate, maxDate);
161
182
  const triggerModeChange = (nextPickerValue, nextMode, triggerEvent) => {
162
183
  setMode(nextMode);
163
184
  if (onPanelChange.value && triggerEvent !== false) {
@@ -166,12 +187,12 @@ var SinglePicker = /* @__PURE__ */ defineComponent((props, { expose }) => {
166
187
  }
167
188
  };
168
189
  /**
169
- * Different with RangePicker, confirm should check `multiple` logic.
170
- * This will never provide `date` instead.
190
+ * Submit the complete value list stored in SinglePicker field `0`.
191
+ * 提交 SinglePicker field `0` 内保存的整组值。
171
192
  */
172
- const triggerConfirm = () => {
173
- triggerSubmitChange(getCalendarValue.value);
174
- triggerOpen(false, { force: true });
193
+ const triggerConfirm = (source = "confirm") => {
194
+ triggerSingleValueChange(0, source);
195
+ resetSingleValueChange();
175
196
  };
176
197
  const onSelectorClick = (event) => {
177
198
  if (!disabled.value && !selectorRef.value?.nativeElement?.contains(document.activeElement)) selectorRef.value?.focus();
@@ -217,14 +238,13 @@ var SinglePicker = /* @__PURE__ */ defineComponent((props, { expose }) => {
217
238
  };
218
239
  const onPanelFocus = (event) => {
219
240
  triggerOpen(true);
220
- onSharedFocus(event);
241
+ onFieldFocus(0, "panel", event);
221
242
  };
222
243
  const onPanelSelect = (date) => {
223
- lastOperation("panel");
224
244
  if (multiple.value && internalMode.value !== picker.value) return;
225
245
  const nextValues = multiple.value ? toggleDates(getCalendarValue.value, date) : [date];
226
- triggerCalendarChange(nextValues);
227
- if (!needConfirm.value && !complexPicker.value && internalPicker.value === internalMode.value) triggerConfirm();
246
+ const panelFinished = !complexPicker.value && internalPicker.value === internalMode.value;
247
+ triggerSingleValueChange(0, panelFinished ? "panel-final" : "panel-intermediate", nextValues);
228
248
  };
229
249
  const onPopupClose = () => {
230
250
  triggerOpen(false);
@@ -247,23 +267,26 @@ var SinglePicker = /* @__PURE__ */ defineComponent((props, { expose }) => {
247
267
  multiple: fp.value.multiple
248
268
  };
249
269
  });
250
- const onSelectorChange = (date) => {
251
- triggerCalendarChange(date);
270
+ const onSelectorChange = (date, source = "input") => {
271
+ triggerSingleValueChange(0, source, date);
252
272
  };
253
273
  const onSelectorInputChange = () => {
254
- lastOperation("input");
274
+ triggerSingleValueChange(0, "input");
255
275
  };
256
276
  const onSelectorFocus = (event) => {
257
- lastOperation("input");
277
+ triggerSingleValueChange(0, "field-switch");
258
278
  triggerOpen(true, { inherit: true });
259
- onSharedFocus(event);
279
+ onFieldFocus(0, "input", event);
260
280
  };
261
281
  const onSelectorBlur = (event) => {
262
- triggerOpen(false);
263
- onSharedBlur(event);
282
+ onFieldBlur(0, "input", event);
264
283
  };
265
284
  const onSelectorKeyDown = (event, preventDefault) => {
266
- if (event.key === "Tab") triggerConfirm();
285
+ if (event.key === "Tab") triggerConfirm("keyboard-submit-weak");
286
+ else if (event.key === "Escape") {
287
+ triggerSingleValueChange(0, "esc");
288
+ triggerOpen(false);
289
+ }
267
290
  onKeyDown.value?.(event, preventDefault);
268
291
  };
269
292
  providePickerContext(computed(() => {
@@ -285,14 +308,6 @@ var SinglePicker = /* @__PURE__ */ defineComponent((props, { expose }) => {
285
308
  ], () => {
286
309
  if (mergedOpen.value && activeIndex.value !== void 0) triggerModeChange(null, picker.value, false);
287
310
  }, { flush: "post" });
288
- watch(mergedOpen, () => {
289
- const lastOp = lastOperation();
290
- if (!mergedOpen.value && lastOp === "input") {
291
- triggerOpen(false);
292
- triggerConfirm();
293
- }
294
- if (!mergedOpen.value && complexPicker.value && !needConfirm.value && lastOp === "panel") triggerConfirm();
295
- }, { flush: "post" });
296
311
  const popupProps = computed(() => {
297
312
  const [mergedClassNames, mergedStyles] = semanticCtx.value;
298
313
  return {
@@ -301,7 +316,7 @@ var SinglePicker = /* @__PURE__ */ defineComponent((props, { expose }) => {
301
316
  showTime: showTime.value,
302
317
  disabledDate: disabledDate.value,
303
318
  onFocus: onPanelFocus,
304
- onBlur: onSharedBlur,
319
+ onBlur: (event) => onFieldBlur(0, "panel", event),
305
320
  picker: picker.value,
306
321
  mode: mergedMode.value,
307
322
  internalMode: internalMode.value,
@@ -317,7 +332,7 @@ var SinglePicker = /* @__PURE__ */ defineComponent((props, { expose }) => {
317
332
  hoverValue: hoverValues.value,
318
333
  onHover: onPanelHover,
319
334
  needConfirm: needConfirm.value,
320
- onSubmit: triggerConfirm,
335
+ onSubmit: () => triggerConfirm("confirm"),
321
336
  onOk: triggerOk,
322
337
  presets: presetList.value,
323
338
  onPresetHover,
@@ -325,7 +340,8 @@ var SinglePicker = /* @__PURE__ */ defineComponent((props, { expose }) => {
325
340
  onNow,
326
341
  cellRender: onInternalCellRender,
327
342
  classNames: mergedClassNames,
328
- styles: mergedStyles
343
+ styles: mergedStyles,
344
+ popupContainerRef: popupRef.value
329
345
  };
330
346
  });
331
347
  return () => {
@@ -354,7 +370,7 @@ var SinglePicker = /* @__PURE__ */ defineComponent((props, { expose }) => {
354
370
  onFocus: onSelectorFocus,
355
371
  onBlur: onSelectorBlur,
356
372
  onKeyDown: onSelectorKeyDown,
357
- onSubmit: triggerConfirm,
373
+ onSubmit: () => triggerConfirm("keyboard-submit"),
358
374
  value: selectorValues.value,
359
375
  maskFormat: maskFormat.value,
360
376
  onChange: onSelectorChange,
@@ -0,0 +1,28 @@
1
+ import { Ref } from 'vue';
2
+ /** Focus event source. / 焦点事件来源。 */
3
+ export type FocusSource = 'input' | 'panel';
4
+ /** Focus event handler. / 聚焦事件处理函数。 */
5
+ export type FieldFocusHandler = (index: number, source: FocusSource, event: FocusEvent) => void;
6
+ /** Blur event handler. / 失焦事件处理函数。 */
7
+ export type FieldBlurHandler = (index: number, source: FocusSource, event: FocusEvent) => void;
8
+ /** Check whether an element belongs to the current focus scope. / 检查元素是否属于当前焦点范围。 */
9
+ export type IsInternalElement = (element: EventTarget | null) => boolean;
10
+ /** Notify a Picker focus or blur event. / 通知 Picker 的聚焦或失焦事件。 */
11
+ export type FocusEventHandler = (index: number, event: FocusEvent) => void;
12
+ export type UseFocusEventsReturn = [
13
+ focused: Ref<boolean>,
14
+ onFieldFocus: FieldFocusHandler,
15
+ onFieldBlur: FieldBlurHandler
16
+ ];
17
+ /** Check whether the target belongs to any container. / 判断目标是否属于任意一个容器。 */
18
+ export declare function isTargetInContainers(target: EventTarget | null, containers: readonly (Element | null | undefined)[]): boolean;
19
+ /**
20
+ * Handle field focus and blur events.
21
+ * 处理 field 的聚焦与失焦事件。
22
+ *
23
+ * Always forward the actual element focus events. Only the internal Picker
24
+ * blur is skipped when `relatedTarget` still belongs to the Picker.
25
+ * 始终转发元素实际发生的焦点事件。仅当 `relatedTarget` 仍属于 Picker 时,
26
+ * 跳过 Picker 内部的整体失焦逻辑。
27
+ */
28
+ export default function useFocusEvents(isInternalElement: IsInternalElement, onFocus?: FocusEventHandler, onBlur?: FocusEventHandler, onConfirmedBlur?: FocusEventHandler): UseFocusEventsReturn;
@@ -0,0 +1,36 @@
1
+ import { computed, shallowRef } from "vue";
2
+ //#region src/PickerInput/hooks/useFocusEvents.ts
3
+ /** Check whether the target belongs to any container. / 判断目标是否属于任意一个容器。 */
4
+ function isTargetInContainers(target, containers) {
5
+ return containers.some((container) => !!container && (container === target || container.contains(target)));
6
+ }
7
+ /**
8
+ * Handle field focus and blur events.
9
+ * 处理 field 的聚焦与失焦事件。
10
+ *
11
+ * Always forward the actual element focus events. Only the internal Picker
12
+ * blur is skipped when `relatedTarget` still belongs to the Picker.
13
+ * 始终转发元素实际发生的焦点事件。仅当 `relatedTarget` 仍属于 Picker 时,
14
+ * 跳过 Picker 内部的整体失焦逻辑。
15
+ */
16
+ function useFocusEvents(isInternalElement, onFocus, onBlur, onConfirmedBlur) {
17
+ const focusedIndex = shallowRef(null);
18
+ const onFieldFocus = (index, _source, event) => {
19
+ focusedIndex.value = index;
20
+ onFocus?.(index, event);
21
+ };
22
+ const onFieldBlur = (index, _source, event) => {
23
+ if (!isInternalElement(event.relatedTarget)) {
24
+ focusedIndex.value = null;
25
+ onConfirmedBlur?.(index, event);
26
+ }
27
+ onBlur?.(index, event);
28
+ };
29
+ return [
30
+ computed(() => focusedIndex.value !== null),
31
+ onFieldFocus,
32
+ onFieldBlur
33
+ ];
34
+ }
35
+ //#endregion
36
+ export { useFocusEvents as default, isTargetInContainers };
@@ -0,0 +1,14 @@
1
+ import { Ref } from 'vue';
2
+ interface FocusLockSelectorRef {
3
+ startInput?: HTMLElement | null;
4
+ endInput?: HTMLElement | null;
5
+ focus: (index?: number | {
6
+ index?: number;
7
+ }) => void;
8
+ }
9
+ /**
10
+ * Keep focus on the specified input field while focus moves inside the Picker.
11
+ * 当焦点在 Picker 内移动时,将其锁定在指定的输入框上。
12
+ */
13
+ export default function useFocusLock(index: Ref<number | null>, forceFocus: Ref<boolean>, selectorRef: Ref<FocusLockSelectorRef | undefined>, popupRef: Ref<HTMLElement | undefined | null>, triggerOpen: (open: boolean) => void): void;
14
+ export {};
@@ -0,0 +1,25 @@
1
+ import { isTargetInContainers } from "./useFocusEvents.js";
2
+ import { onUpdated, watch } from "vue";
3
+ //#region src/PickerInput/hooks/useFocusLock.ts
4
+ /**
5
+ * Keep focus on the specified input field while focus moves inside the Picker.
6
+ * 当焦点在 Picker 内移动时,将其锁定在指定的输入框上。
7
+ */
8
+ function useFocusLock(index, forceFocus, selectorRef, popupRef, triggerOpen) {
9
+ watch([index, forceFocus], () => {
10
+ if (index.value !== null && forceFocus.value) {
11
+ triggerOpen(true);
12
+ selectorRef.value?.focus({ index: index.value });
13
+ }
14
+ }, { flush: "post" });
15
+ const lockFocus = () => {
16
+ if (index.value === null) return;
17
+ const inputFields = [selectorRef.value?.startInput, selectorRef.value?.endInput];
18
+ const activeElement = (inputFields[index.value]?.getRootNode())?.activeElement ?? document.activeElement;
19
+ if (isTargetInContainers(activeElement, [popupRef.value])) return;
20
+ if (inputFields.some((field, fieldIndex) => fieldIndex !== index.value && isTargetInContainers(activeElement, [field]))) inputFields[index.value]?.focus();
21
+ };
22
+ onUpdated(lockFocus);
23
+ }
24
+ //#endregion
25
+ export { useFocusLock as default };
@@ -6,4 +6,4 @@ import { RangeValueType } from '../RangePicker';
6
6
  * RangePicker need additional logic to handle the `disabled` case. e.g.
7
7
  * [disabled, enabled] should end date not before start date
8
8
  */
9
- export default function useRangeDisabledDate<DateType extends object = any>(values: Ref<RangeValueType<DateType>>, disabled: Ref<[boolean, boolean]>, activeIndexList: Ref<number[]>, generateConfig: Ref<GenerateConfig<DateType>>, locale: Ref<Locale>, disabledDate: Ref<DisabledDate<DateType> | undefined>): DisabledDate<DateType>;
9
+ export default function useRangeDisabledDate<DateType extends object = any>(values: Ref<RangeValueType<DateType>>, disabled: Ref<[boolean, boolean]>, activeIndexRef: Ref<number>, triggeredFields: Ref<number[]>, generateConfig: Ref<GenerateConfig<DateType>>, locale: Ref<Locale>, disabledDate: Ref<DisabledDate<DateType> | undefined>): DisabledDate<DateType>;
@@ -5,13 +5,13 @@ import { isSame } from "../../utils/dateUtil.js";
5
5
  * RangePicker need additional logic to handle the `disabled` case. e.g.
6
6
  * [disabled, enabled] should end date not before start date
7
7
  */
8
- function useRangeDisabledDate(values, disabled, activeIndexList, generateConfig, locale, disabledDate) {
8
+ function useRangeDisabledDate(values, disabled, activeIndexRef, triggeredFields, generateConfig, locale, disabledDate) {
9
9
  const rangeDisabledDate = (date, info) => {
10
- const activeIndex = activeIndexList.value[activeIndexList.value.length - 1];
10
+ const activeIndex = activeIndexRef.value;
11
11
  const [start, end] = values.value;
12
12
  const mergedInfo = {
13
13
  ...info,
14
- from: getFromDate(values.value, activeIndexList.value)
14
+ from: getFromDate(values.value, triggeredFields.value, activeIndex)
15
15
  };
16
16
  if (activeIndex === 1 && disabled.value[0] && start && !isSame(generateConfig.value, locale.value, start, date, mergedInfo.type) && generateConfig.value.isAfter(start, date)) return true;
17
17
  if (activeIndex === 0 && disabled.value[1] && end && !isSame(generateConfig.value, locale.value, end, date, mergedInfo.type) && generateConfig.value.isAfter(date, end)) return true;
@@ -2,4 +2,10 @@ import { Ref } from 'vue';
2
2
  import { GenerateConfig } from '../../generate';
3
3
  import { InternalMode, Locale, PanelMode } from '../../interface';
4
4
  export declare function offsetPanelDate<DateType = any>(generateConfig: GenerateConfig<DateType>, picker: InternalMode, date: DateType, offset: number): DateType;
5
- export default function useRangePickerValue<DateType extends object, ValueType extends DateType[]>(generateConfig: Ref<GenerateConfig<DateType>>, locale: Ref<Locale>, calendarValue: Ref<ValueType>, modes: Ref<PanelMode[]>, open: Ref<boolean>, activeIndex: Ref<number>, pickerMode: Ref<InternalMode>, multiplePanel: Ref<boolean>, defaultPickerValue?: Ref<any>, pickerValue?: Ref<any>, timeDefaultValue?: Ref<any>, onPickerValueChange?: ((dates: ValueType, info: any) => void) | Ref<((dates: ValueType, info: any) => void) | undefined>, minDate?: Ref<DateType | undefined>, maxDate?: Ref<DateType | undefined>): [currentIndexPickerValue: Ref<DateType>, setCurrentIndexPickerValue: (value: DateType, source?: 'reset' | 'panel') => void];
5
+ export default function useRangePickerValue<DateType extends object, ValueType extends DateType[]>(generateConfig: Ref<GenerateConfig<DateType>>, locale: Ref<Locale>, calendarValue: Ref<ValueType>, modes: Ref<PanelMode[]>, open: Ref<boolean>,
6
+ /**
7
+ * Keep the current panels when focus moves between fields inside an open
8
+ * Picker, instead of jumping to the newly active field's value.
9
+ * Picker 打开状态下在 field 间移动焦点时保持当前面板,而不是跳到新 field 的值。
10
+ */
11
+ preserveOnFieldChange: Ref<boolean>, activeIndex: Ref<number>, pickerMode: Ref<InternalMode>, multiplePanel: Ref<boolean>, defaultPickerValue?: Ref<any>, pickerValue?: Ref<any>, timeDefaultValue?: Ref<any>, onPickerValueChange?: ((dates: ValueType, info: any) => void) | Ref<((dates: ValueType, info: any) => void) | undefined>, minDate?: Ref<DateType | undefined>, maxDate?: Ref<DateType | undefined>): [currentIndexPickerValue: Ref<DateType>, setCurrentIndexPickerValue: (value: DateType, source?: 'reset' | 'panel') => void];
@@ -15,7 +15,7 @@ function offsetPanelDate(generateConfig, picker, date, offset) {
15
15
  }
16
16
  }
17
17
  var EMPTY_LIST = [];
18
- function useRangePickerValue(generateConfig, locale, calendarValue, modes, open, activeIndex, pickerMode, multiplePanel, defaultPickerValue = ref(EMPTY_LIST), pickerValue = ref(EMPTY_LIST), timeDefaultValue = ref(EMPTY_LIST), onPickerValueChange, minDate, maxDate) {
18
+ function useRangePickerValue(generateConfig, locale, calendarValue, modes, open, preserveOnFieldChange, activeIndex, pickerMode, multiplePanel, defaultPickerValue = ref(EMPTY_LIST), pickerValue = ref(EMPTY_LIST), timeDefaultValue = ref(EMPTY_LIST), onPickerValueChange, minDate, maxDate) {
19
19
  const isTimePicker = computed(() => pickerMode.value === "time");
20
20
  const mergedActiveIndex = computed(() => activeIndex.value || 0);
21
21
  const getDefaultPickerValue = (index) => {
@@ -45,21 +45,15 @@ function useRangePickerValue(generateConfig, locale, calendarValue, modes, open,
45
45
  mode: modes.value
46
46
  });
47
47
  };
48
+ const isSamePanel = (date1, date2) => {
49
+ if (pickerMode.value === "year") return Math.floor(generateConfig.value.getYear(date1) / 10) === Math.floor(generateConfig.value.getYear(date2) / 10);
50
+ const panelMode = pickerMode.value === "month" || pickerMode.value === "quarter" ? "year" : "month";
51
+ return isSame(generateConfig.value, locale.value, date1, date2, panelMode);
52
+ };
48
53
  const getEndDatePickerValue = (startDate, endDate) => {
49
- if (multiplePanel.value) {
50
- const mode = {
51
- date: "month",
52
- datetime: "month",
53
- week: "month",
54
- month: "year",
55
- quarter: "year"
56
- }[pickerMode.value];
57
- if (mode && !isSame(generateConfig.value, locale.value, startDate, endDate, mode)) return offsetPanelDate(generateConfig.value, pickerMode.value, endDate, -1);
58
- if (pickerMode.value === "year" && startDate && endDate) {
59
- if (Math.floor(generateConfig.value.getYear(startDate) / 10) !== Math.floor(generateConfig.value.getYear(endDate) / 10)) return offsetPanelDate(generateConfig.value, pickerMode.value, endDate, -1);
60
- }
61
- }
62
- return endDate;
54
+ if (!multiplePanel.value || !startDate) return endDate;
55
+ const nextPanelDate = offsetPanelDate(generateConfig.value, pickerMode.value, startDate, 1);
56
+ return isSamePanel(startDate, endDate) || isSamePanel(nextPanelDate, endDate) ? startDate : offsetPanelDate(generateConfig.value, pickerMode.value, endDate, -1);
63
57
  };
64
58
  const prevActiveIndexRef = ref(null);
65
59
  watch(() => [
@@ -70,7 +64,7 @@ function useRangePickerValue(generateConfig, locale, calendarValue, modes, open,
70
64
  if (!open.value) return;
71
65
  if (defaultPickerValue.value?.[mergedActiveIndex.value]) return;
72
66
  let nextPickerValue = isTimePicker.value ? null : generateConfig.value.getNow();
73
- if (prevActiveIndexRef.value !== null && prevActiveIndexRef.value !== mergedActiveIndex.value) nextPickerValue = [getStartPickerValue(true), getEndPickerValue(true)][mergedActiveIndex.value ^ 1];
67
+ if (preserveOnFieldChange.value && prevActiveIndexRef.value !== null && prevActiveIndexRef.value !== mergedActiveIndex.value) nextPickerValue = [getStartPickerValue(true), getEndPickerValue(true)][mergedActiveIndex.value ^ 1];
74
68
  else if (calendarValue.value?.[mergedActiveIndex.value]) nextPickerValue = mergedActiveIndex.value === 0 ? calendarValue.value[0] : getEndDatePickerValue(calendarValue.value[0], calendarValue.value[1]);
75
69
  else if (calendarValue.value?.[mergedActiveIndex.value ^ 1]) nextPickerValue = calendarValue.value[mergedActiveIndex.value ^ 1];
76
70
  if (!nextPickerValue) return;
@@ -11,7 +11,7 @@ type UseInnerValueReturn<ValueType extends object[]> = readonly [
11
11
  ];
12
12
  /**
13
13
  * Control the internal `value` align with prop `value` and provide a temp `calendarValue` for ui.
14
- * `calendarValue` will be reset when blur & focus & open.
14
+ * The caller controls the temporary `calendarValue` lifecycle through event handlers.
15
15
  */
16
16
  export declare function useInnerValue<ValueType extends DateType[], DateType extends object = any>(generateConfig: Ref<GenerateConfig<DateType>>, locale: Ref<Locale>, formatList: Ref<FormatType[]>, rangeValue: Ref<boolean | undefined>, order: Ref<boolean | undefined>, defaultValue: Ref<ValueType | undefined>, value: Ref<ValueType | undefined>, onCalendarChange?: (dates: ValueType, dateStrings: [string, string], info: BaseInfo) => void, onOk?: (dates: ValueType) => void): UseInnerValueReturn<ValueType>;
17
17
  export default function useRangeValue<ValueType extends DateType[], DateType extends object = any>(info: ComputedRef<{
@@ -21,8 +21,8 @@ export default function useRangeValue<ValueType extends DateType[], DateType ext
21
21
  allowEmpty: boolean[];
22
22
  order: boolean;
23
23
  onChange?: (dates: ValueType | null, dateStrings: [string, string] | null) => void;
24
- }>, mergedValue: Ref<ValueType> | ComputedRef<ValueType>, setInnerValue: (nextValue: ValueType) => void, getCalendarValue: () => ValueType, triggerCalendarChange: TriggerCalendarChange<ValueType>, disabled: Ref<boolean[]>, formatList: Ref<FormatType[]>, focused: Ref<boolean>, open: Ref<boolean>, isInvalidateDate: (date: DateType, info?: {
24
+ }>, mergedValue: Ref<ValueType> | ComputedRef<ValueType>, setInnerValue: (nextValue: ValueType) => void, getCalendarValue: () => ValueType, triggerCalendarChange: TriggerCalendarChange<ValueType>, disabled: Ref<boolean[]>, formatList: Ref<FormatType[]>, isInvalidateDate: (date: DateType, info?: {
25
25
  from?: DateType;
26
26
  activeIndex: number;
27
- }) => boolean): readonly [(index: number, needTriggerChange: boolean) => void, (nextValue?: ValueType) => boolean];
27
+ }) => boolean): readonly [(index: number, needTriggerChange: boolean) => void, (nextValue?: ValueType) => boolean, (index?: number) => void];
28
28
  export {};
@@ -1,6 +1,5 @@
1
1
  import { fillIndex } from "../../utils/miscUtil.js";
2
2
  import { formatValue, isSame, isSameTimestamp } from "../../utils/dateUtil.js";
3
- import useLockEffect from "./useLockEffect.js";
4
3
  import { computed, ref, shallowRef, watch } from "vue";
5
4
  //#region src/PickerInput/hooks/useRangeValue.ts
6
5
  var EMPTY_VALUE = [];
@@ -32,7 +31,7 @@ function orderDates(dates, generateConfig) {
32
31
  }
33
32
  /**
34
33
  * Control the internal `value` align with prop `value` and provide a temp `calendarValue` for ui.
35
- * `calendarValue` will be reset when blur & focus & open.
34
+ * The caller controls the temporary `calendarValue` lifecycle through event handlers.
36
35
  */
37
36
  function useInnerValue(generateConfig, locale, formatList, rangeValue, order, defaultValue, value, onCalendarChange, onOk) {
38
37
  const mergedValue = shallowRef((value.value === void 0 ? defaultValue.value : value.value) || EMPTY_VALUE);
@@ -74,7 +73,7 @@ function useInnerValue(generateConfig, locale, formatList, rangeValue, order, de
74
73
  triggerOk
75
74
  ];
76
75
  }
77
- function useRangeValue(info, mergedValue, setInnerValue, getCalendarValue, triggerCalendarChange, disabled, formatList, focused, open, isInvalidateDate) {
76
+ function useRangeValue(info, mergedValue, setInnerValue, getCalendarValue, triggerCalendarChange, disabled, formatList, isInvalidateDate) {
78
77
  const orderOnChange = computed(() => disabled.value.some((d) => d) ? false : info.value.order);
79
78
  const [getDateTexts, isSameDates] = useUtil(computed(() => info.value.generateConfig), computed(() => info.value.locale), formatList);
80
79
  const submitValue = ref(mergedValue.value);
@@ -121,17 +120,20 @@ function useRangeValue(info, mergedValue, setInnerValue, getCalendarValue, trigg
121
120
  setSubmitValue(nextSubmitValue);
122
121
  if (needTriggerChange) triggerSubmit();
123
122
  };
124
- const interactiveFinished = computed(() => !focused.value && !open.value);
125
- useLockEffect(computed(() => !interactiveFinished.value), (next) => {
126
- if (next === false) {
127
- if (!next) {
128
- triggerSubmit();
129
- triggerCalendarChange(mergedValue.value);
130
- submitValue.value = mergedValue.value;
131
- }
123
+ const resetValue = (index) => {
124
+ if (index === void 0) {
125
+ triggerCalendarChange(mergedValue.value);
126
+ submitValue.value = mergedValue.value;
127
+ return;
132
128
  }
133
- });
134
- return [flushSubmit, triggerSubmit];
129
+ triggerCalendarChange(fillIndex(getCalendarValue(), index, mergedValue.value[index]));
130
+ setSubmitValue(fillIndex(submitValue.value, index, mergedValue.value[index]));
131
+ };
132
+ return [
133
+ flushSubmit,
134
+ triggerSubmit,
135
+ resetValue
136
+ ];
135
137
  }
136
138
  //#endregion
137
139
  export { useRangeValue as default, useInnerValue };
@@ -0,0 +1,109 @@
1
+ import { ComputedRef, Ref } from 'vue';
2
+ /** Change source of a field. / Field 的变更来源。 */
3
+ export type RangeValueChangeSource = 'input' | 'remove' | 'keyboard-submit' | 'keyboard-submit-weak' | 'esc' | 'panel-intermediate' | 'panel-final' | 'popupClose' | 'field-switch' | 'confirm';
4
+ /** Resolved operation for one field interaction. / 一次 field 交互最终执行的操作。 */
5
+ export type RangeValueChangeAction = 'modify' | 'submitCurrent' | 'switchNext' | 'finish' | 'abort' | 'resetCurrent' | 'resetCurrentAndSwitchNext' | 'resetAll';
6
+ /** Receive a field interaction and its optional value. / 接收 field 交互及可选变更值。 */
7
+ export type TriggerChange<FieldValue> = (index: number, source: RangeValueChangeSource, value?: FieldValue) => void;
8
+ /** Read the latest temporary CalendarValue. / 读取最新的临时 CalendarValue。 */
9
+ export type GetCalendarValue<FieldValue> = () => readonly (FieldValue | null | undefined)[];
10
+ /** Update one field in CalendarValue. / 更新 CalendarValue 中的一个 field。 */
11
+ export type TriggerCalendarChange<FieldValue> = (index: number, value: FieldValue) => void;
12
+ /**
13
+ * Flush one field and optionally emit the final change.
14
+ * 提交一个 field,并按需触发最终 change。
15
+ */
16
+ export type FlushSubmit = (index: number, needTriggerChange: boolean) => void;
17
+ /**
18
+ * Reset one field, or all fields when index is omitted.
19
+ * 重置指定 field;未传 index 时重置全部 field。
20
+ */
21
+ export type ResetValue = (index?: number) => void;
22
+ export type UseRangeValueChangeReturn<FieldValue> = [
23
+ currentIndex: Ref<number | null>,
24
+ activeIndex: ComputedRef<number>,
25
+ forceFocus: Ref<boolean>,
26
+ triggeredFields: ComputedRef<number[]>,
27
+ triggerChange: TriggerChange<FieldValue>,
28
+ reset: VoidFunction
29
+ ];
30
+ /**
31
+ * Coordinate CalendarValue updates, part submits and final submits for any
32
+ * number of fields.
33
+ * 统一管理任意数量 field 的 CalendarValue 更新、局部提交与最终提交。
34
+ *
35
+ * Flow / 流程:
36
+ * Every event is first resolved from `source`, `needConfirm`, `allowEmpty` and
37
+ * the field indexes to one action. State changes only happen while executing
38
+ * that action, so event sources never submit or reset values on their own.
39
+ * 每个事件先根据 `source`、`needConfirm`、`allowEmpty` 与 field index 得到唯一
40
+ * action。状态只在执行 action 时改变,事件来源本身不直接提交或重置值。
41
+ *
42
+ * Index transitions also expose whether focus must follow the next field.
43
+ * Confirm-like operations and a popup close following a panel operation
44
+ * switch focus strongly. A popup close following input, Tab and an actual
45
+ * field focus switch only update the expected index.
46
+ * index 切换还会同步给出是否必须跟随聚焦下一个 field。确认类操作,以及面板
47
+ * 操作后的 popup 关闭使用强切换;input 后的 popup 关闭、Tab 与真实 field
48
+ * 聚焦切换只更新预期 index。
49
+ *
50
+ * Source resolution / 事件解析:
51
+ *
52
+ * - `esc` always resolves to `resetAll`.
53
+ * `esc` 始终解析为 `resetAll`。
54
+ * - With no current field, `popupClose` resolves to `resetAll`; any other
55
+ * non-cancel event starts a new interaction from its field.
56
+ * 没有当前 field 时,`popupClose` 解析为 `resetAll`;其余非撤销事件从
57
+ * 对应 field 开始新一轮交互。
58
+ * - `field-switch` advances exactly one field in circular order. `needConfirm`
59
+ * locks an unconfirmed non-empty field unless it allows empty; an allow-empty
60
+ * field is reset before advancing.
61
+ * `field-switch` 只允许按循环顺序推进一个 field。`needConfirm` 会锁定未确认
62
+ * 且非空的 field;允许空值时先重置再推进。
63
+ * - Other sources must target the current field. `input` and
64
+ * `panel-intermediate` modify it; `remove` explicitly submits the removed
65
+ * value even when the field does not allow empty. `panel-final` advances only
66
+ * without confirmation; `keyboard-submit-weak` part-submits without
67
+ * advancing; `keyboard-submit` and `confirm` advance only when the field has
68
+ * a value or allows empty.
69
+ * 其余来源必须指向当前 field。`input` 与 `panel-intermediate` 只修改;
70
+ * `remove` 会明确提交删除后的值,即使 field 不允许为空;`panel-final`
71
+ * 仅在无需确认时推进;`keyboard-submit-weak` 只做局部提交而不推进;
72
+ * `keyboard-submit` 与 `confirm` 仅在有值或允许空值时推进。
73
+ * - `popupClose` finishes an untouched interaction. Without confirmation it
74
+ * submits a valid field; with confirmation it submits only after every field
75
+ * has participated, otherwise it resets all temporary values. A modified
76
+ * allow-empty field is reset before the final submit.
77
+ * `popupClose` 会直接结束未修改的交互。无需确认时提交有效 field;需要确认时
78
+ * 仅在所有 field 都参与过后提交,否则重置全部临时值。当前 field 已修改且
79
+ * 允许为空时,会先重置当前值再完成提交。
80
+ *
81
+ * Action execution / Action 执行:
82
+ *
83
+ * - `modify`: update or record the current CalendarValue.
84
+ * 更新或记录当前 CalendarValue。
85
+ * - `submitCurrent`: part-submit the current field without advancing.
86
+ * 局部提交当前 field,但不推进。
87
+ * - `switchNext`: submit the current field and advance to the next field.
88
+ * 提交当前 field 并推进到下一个 field。
89
+ * - `finish`: end an interaction in which no field was modified without
90
+ * resetting values. / 结束所有 field 均未修改的交互,不重置任何值。
91
+ * - `abort`: stop without changing any state.
92
+ * 直接短路,不改变任何状态。
93
+ * - `resetCurrent`: discard only the current field.
94
+ * 仅撤销当前 field。
95
+ * - `resetCurrentAndSwitchNext`: discard the current temporary value and
96
+ * advance without submitting. Revisiting a field starts a new round.
97
+ * 撤销当前临时值并直接推进,不触发提交;再次进入已访问 field 时开启新一轮。
98
+ * - `resetAll`: discard all temporary values and end the interaction.
99
+ * 撤销全部临时值并结束本轮交互。
100
+ *
101
+ * Vue note: upstream keeps `currentIndex` and `forceFocus` in `useSyncState`
102
+ * because React state is not readable synchronously inside the same handler.
103
+ * Vue refs already are, so plain refs carry both the render value and the
104
+ * synchronous read.
105
+ * Vue 说明:上游用 `useSyncState` 保存 `currentIndex` 与 `forceFocus`,是因为
106
+ * React state 无法在同一个 handler 内同步读回;Vue 的 ref 本身即可同步读写,
107
+ * 因此直接用 ref 同时承担渲染值与同步读取。
108
+ */
109
+ export default function useRangeValueChange<FieldValue = unknown>(fieldCount: Ref<number> | ComputedRef<number>, needConfirm: Ref<boolean> | ComputedRef<boolean>, allowEmpty: Ref<readonly boolean[]> | ComputedRef<readonly boolean[]>, getCalendarValue: GetCalendarValue<FieldValue>, triggerCalendarChange: TriggerCalendarChange<FieldValue>, flushSubmit: FlushSubmit, resetValue: ResetValue): UseRangeValueChangeReturn<FieldValue>;