@7365admin1/layer-common 4.2.2-staging.267 → 4.2.2-staging.268
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.
- package/components/Input/DateTimePicker.vue +69 -157
- package/package.json +1 -1
|
@@ -1,42 +1,54 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
<template #append-inner>
|
|
15
|
-
<v-icon icon="mdi-calendar" @click.stop="openDatePicker" />
|
|
16
|
-
</template>
|
|
17
|
-
</v-text-field>
|
|
18
|
-
<div class="w-100 d-flex align-end ga-3 hidden-input">
|
|
19
|
-
<input
|
|
20
|
-
ref="dateInput"
|
|
21
|
-
:type="inputType"
|
|
22
|
-
v-model="dateTime"
|
|
23
|
-
:min="minValue"
|
|
24
|
-
/>
|
|
25
|
-
</div>
|
|
26
|
-
</div>
|
|
2
|
+
<v-text-field
|
|
3
|
+
v-bind="$attrs"
|
|
4
|
+
ref="dateTimePickerRef"
|
|
5
|
+
class="filter-field"
|
|
6
|
+
v-model="fieldValue"
|
|
7
|
+
:type="inputType"
|
|
8
|
+
:label="placeholder || undefined"
|
|
9
|
+
:min="minValue"
|
|
10
|
+
:rules="rules"
|
|
11
|
+
persistent-placeholder
|
|
12
|
+
autocomplete="off"
|
|
13
|
+
/>
|
|
27
14
|
</template>
|
|
28
15
|
|
|
29
16
|
<script setup lang="ts">
|
|
30
|
-
import { ref, computed
|
|
17
|
+
import { ref, computed } from "vue";
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* The field the user edits IS the native date input now.
|
|
21
|
+
*
|
|
22
|
+
* It used to be two elements: a read-only Vuetify text field for show, and a
|
|
23
|
+
* zero-height `opacity: 0` native input behind it that `showPicker()` was
|
|
24
|
+
* called on. That arrangement broke every way a user can enter a date:
|
|
25
|
+
*
|
|
26
|
+
* - TYPING went into Vuetify's own internal copy of the value (the text field
|
|
27
|
+
* was bound with `:model-value` and nothing listened for the update), so the
|
|
28
|
+
* typed date DISPLAYED and PASSED the required-rule while both models here
|
|
29
|
+
* stayed empty - `startDate: ""` went to the API, and nothing could ever be
|
|
30
|
+
* Upcoming.
|
|
31
|
+
* - PICKING a date drove the invisible input. A partly-filled
|
|
32
|
+
* `datetime-local` has an empty value until every segment is set, and the
|
|
33
|
+
* segments were invisible, so the picker closed and set nothing.
|
|
34
|
+
* - An End Date that was typed stayed invalid for the same reason as the
|
|
35
|
+
* first point, so an announcement with a real end date could not be created
|
|
36
|
+
* at all.
|
|
37
|
+
*
|
|
38
|
+
* One visible native input has the browser's own picker, its own typing, and a
|
|
39
|
+
* real `v-model`, which is all three fixes.
|
|
40
|
+
*/
|
|
31
41
|
|
|
32
42
|
const prop = defineProps({
|
|
33
43
|
rules: {
|
|
34
44
|
type: Array as PropType<Array<any>>,
|
|
35
45
|
default: () => [],
|
|
36
46
|
},
|
|
47
|
+
// Rendered as the field's label: a native date input always draws its own
|
|
48
|
+
// dd/mm/yyyy segments, so a placeholder would never be seen.
|
|
37
49
|
placeholder: {
|
|
38
50
|
type: String,
|
|
39
|
-
default: "
|
|
51
|
+
default: "",
|
|
40
52
|
},
|
|
41
53
|
dateOnly: {
|
|
42
54
|
type: Boolean,
|
|
@@ -48,11 +60,10 @@ const prop = defineProps({
|
|
|
48
60
|
},
|
|
49
61
|
});
|
|
50
62
|
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const dateTimeUTC = defineModel<string | null>("utc", { default: null }); // UTC format
|
|
63
|
+
const dateTime = defineModel<string | null>({ default: null }); // 2025-10-10T13:09, local
|
|
64
|
+
const dateTimeUTC = defineModel<string | null>("utc", { default: null }); // ISO 8601, UTC
|
|
54
65
|
|
|
55
|
-
const
|
|
66
|
+
const dateTimePickerRef = ref<HTMLInputElement | null>(null);
|
|
56
67
|
const inputType = computed(() => (prop.dateOnly ? "date" : "datetime-local"));
|
|
57
68
|
|
|
58
69
|
const minValue = computed(() => {
|
|
@@ -62,148 +73,49 @@ const minValue = computed(() => {
|
|
|
62
73
|
}
|
|
63
74
|
return prop.min;
|
|
64
75
|
});
|
|
65
|
-
const inputPlaceholder = computed(
|
|
66
|
-
() =>
|
|
67
|
-
prop.placeholder ||
|
|
68
|
-
(prop.dateOnly ? "MM/DD/YYYY" : "DD/MM/YYYY, HH:MM AM/PM")
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
const dateInput = ref<HTMLInputElement | null>(null);
|
|
72
|
-
const dateTimePickerRef = ref<HTMLInputElement | null>(null);
|
|
73
|
-
let nativeInput: HTMLInputElement | null = null;
|
|
74
|
-
|
|
75
|
-
const isInitialLoad = ref(true);
|
|
76
|
-
|
|
77
|
-
function handleNativeInputClick(e: MouseEvent) {
|
|
78
|
-
e.stopPropagation();
|
|
79
|
-
openDatePicker();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function openDatePicker() {
|
|
83
|
-
setTimeout(() => {
|
|
84
|
-
dateInput.value?.showPicker?.();
|
|
85
|
-
}, 0);
|
|
86
|
-
}
|
|
87
76
|
|
|
88
77
|
function validate() {
|
|
89
78
|
(dateTimePickerRef.value as any)?.validate();
|
|
90
79
|
}
|
|
91
80
|
|
|
92
|
-
|
|
93
|
-
if (!dateStr) return "";
|
|
94
|
-
|
|
95
|
-
const date = new Date(dateStr);
|
|
96
|
-
if (Number.isNaN(date.getTime())) return "";
|
|
97
|
-
|
|
98
|
-
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
99
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
100
|
-
const year = date.getFullYear();
|
|
81
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
101
82
|
|
|
102
|
-
|
|
103
|
-
|
|
83
|
+
// What the native input accepts: `YYYY-MM-DD` or `YYYY-MM-DDTHH:mm`, local time.
|
|
84
|
+
function toInputValue(value: string | null): string {
|
|
85
|
+
if (!value) return "";
|
|
86
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
87
|
+
return prop.dateOnly ? value : `${value}T00:00`;
|
|
104
88
|
}
|
|
105
|
-
|
|
106
|
-
let hours = date.getHours();
|
|
107
|
-
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
108
|
-
|
|
109
|
-
const ampm = hours >= 12 ? "PM" : "AM";
|
|
110
|
-
hours = hours % 12;
|
|
111
|
-
hours = hours ? hours : 12;
|
|
112
|
-
|
|
113
|
-
const formattedTime = `${String(hours).padStart(2, "0")}:${minutes} ${ampm}`;
|
|
114
|
-
|
|
115
|
-
return `${day}/${month}/${year}, ${formattedTime}`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function toDateOnlyInputValue(dateStr: string): string {
|
|
119
|
-
if (!dateStr) return "";
|
|
120
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return dateStr;
|
|
121
|
-
const date = new Date(dateStr);
|
|
89
|
+
const date = new Date(value);
|
|
122
90
|
if (Number.isNaN(date.getTime())) return "";
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
91
|
+
const day = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
92
|
+
return prop.dateOnly
|
|
93
|
+
? day
|
|
94
|
+
: `${day}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
127
95
|
}
|
|
128
96
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const localDate = new Date(dateDefault);
|
|
138
|
-
dateTimeUTC.value = localDate.toISOString();
|
|
139
|
-
}
|
|
140
|
-
} else if (dateUTC) {
|
|
141
|
-
dateTimeFormattedReadOnly.value = convertToReadableFormat(dateUTC);
|
|
142
|
-
if (prop.dateOnly) {
|
|
143
|
-
dateTime.value = toDateOnlyInputValue(dateUTC);
|
|
144
|
-
} else {
|
|
145
|
-
const localDate = new Date(dateUTC);
|
|
146
|
-
dateTime.value = formatDateISO8601(localDate);
|
|
147
|
-
}
|
|
148
|
-
} else {
|
|
149
|
-
dateTimeFormattedReadOnly.value = null;
|
|
150
|
-
}
|
|
97
|
+
// What the API stores: a date-only field stays `YYYY-MM-DD`, everything else
|
|
98
|
+
// becomes UTC.
|
|
99
|
+
function toUTCValue(value: string | null): string | null {
|
|
100
|
+
if (!value) return null;
|
|
101
|
+
if (prop.dateOnly) return toInputValue(value).slice(0, 10) || null;
|
|
102
|
+
const date = new Date(value);
|
|
103
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
104
|
+
return date.toISOString();
|
|
151
105
|
}
|
|
152
106
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
dateTimeFormattedReadOnly.value = convertToReadableFormat(dateVal);
|
|
164
|
-
if (prop.dateOnly) {
|
|
165
|
-
dateTimeUTC.value = toDateOnlyInputValue(dateVal);
|
|
166
|
-
} else {
|
|
167
|
-
const localDate = new Date(dateVal);
|
|
168
|
-
dateTimeUTC.value = localDate.toISOString();
|
|
169
|
-
}
|
|
170
|
-
},
|
|
171
|
-
{ immediate: false }
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
watch(
|
|
175
|
-
dateTimeUTC,
|
|
176
|
-
() => {
|
|
177
|
-
handleInitialDate();
|
|
107
|
+
// The one value the field is bound to. A getter and a setter instead of a pair
|
|
108
|
+
// of watchers: no initial-load flag, no chance of the two models fighting each
|
|
109
|
+
// other, and it behaves the same on the server as in the browser.
|
|
110
|
+
const fieldValue = computed({
|
|
111
|
+
get: () => toInputValue(dateTime.value || dateTimeUTC.value),
|
|
112
|
+
set: (value: string | null) => {
|
|
113
|
+
dateTime.value = value || null;
|
|
114
|
+
dateTimeUTC.value = toUTCValue(value);
|
|
178
115
|
},
|
|
179
|
-
{ immediate: true }
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
onMounted(async () => {
|
|
183
|
-
await nextTick();
|
|
184
|
-
isInitialLoad.value = false;
|
|
185
|
-
// Wait until Vuetify renders its internal input
|
|
186
|
-
nativeInput = (dateTimePickerRef.value as any)?.$el?.querySelector(
|
|
187
|
-
"input"
|
|
188
|
-
);
|
|
189
|
-
if (nativeInput) {
|
|
190
|
-
nativeInput.addEventListener("click", handleNativeInputClick);
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
onBeforeUnmount(() => {
|
|
195
|
-
nativeInput?.removeEventListener("click", handleNativeInputClick);
|
|
196
|
-
nativeInput = null;
|
|
197
116
|
});
|
|
198
117
|
|
|
199
118
|
defineExpose({
|
|
200
119
|
validate,
|
|
201
120
|
});
|
|
202
121
|
</script>
|
|
203
|
-
|
|
204
|
-
<style scoped>
|
|
205
|
-
.hidden-input {
|
|
206
|
-
opacity: 0;
|
|
207
|
-
height: 0;
|
|
208
|
-
}
|
|
209
|
-
</style>
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@7365admin1/layer-common",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "4.2.2-staging.
|
|
5
|
+
"version": "4.2.2-staging.268",
|
|
6
6
|
"author": "7365admin1",
|
|
7
7
|
"main": "./nuxt.config.ts",
|
|
8
8
|
"//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",
|