@api-client/ui 0.5.5 → 0.5.6
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/build/src/elements/highlight/MarkdownStyles.d.ts.map +1 -1
- package/build/src/elements/highlight/MarkdownStyles.js +0 -13
- package/build/src/elements/highlight/MarkdownStyles.js.map +1 -1
- package/build/src/elements/http/BodyEditor.d.ts +0 -13
- package/build/src/elements/http/BodyEditor.d.ts.map +1 -1
- package/build/src/elements/http/BodyEditor.js +0 -13
- package/build/src/elements/http/BodyEditor.js.map +1 -1
- package/build/src/elements/http/BodyTextEditor.d.ts +0 -13
- package/build/src/elements/http/BodyTextEditor.d.ts.map +1 -1
- package/build/src/elements/http/BodyTextEditor.js +0 -13
- package/build/src/elements/http/BodyTextEditor.js.map +1 -1
- package/build/src/elements/http/BodyUrlEncodedEditor.d.ts +0 -13
- package/build/src/elements/http/BodyUrlEncodedEditor.d.ts.map +1 -1
- package/build/src/elements/http/BodyUrlEncodedEditor.js +0 -13
- package/build/src/elements/http/BodyUrlEncodedEditor.js.map +1 -1
- package/build/src/elements/http/UrlInput.d.ts +0 -13
- package/build/src/elements/http/UrlInput.d.ts.map +1 -1
- package/build/src/elements/http/UrlInput.js +0 -13
- package/build/src/elements/http/UrlInput.js.map +1 -1
- package/build/src/index.d.ts +2 -0
- package/build/src/index.d.ts.map +1 -1
- package/build/src/index.js +2 -0
- package/build/src/index.js.map +1 -1
- package/build/src/md/button/internals/button.styles.js +1 -1
- package/build/src/md/button/internals/button.styles.js.map +1 -1
- package/build/src/md/date/internals/DateTime.d.ts +0 -13
- package/build/src/md/date/internals/DateTime.d.ts.map +1 -1
- package/build/src/md/date/internals/DateTime.js +0 -13
- package/build/src/md/date/internals/DateTime.js.map +1 -1
- package/build/src/md/date-picker/index.d.ts +13 -0
- package/build/src/md/date-picker/index.d.ts.map +1 -0
- package/build/src/md/date-picker/index.js +13 -0
- package/build/src/md/date-picker/index.js.map +1 -0
- package/build/src/md/date-picker/internals/DatePicker.styles.d.ts +4 -0
- package/build/src/md/date-picker/internals/DatePicker.styles.d.ts.map +1 -0
- package/build/src/md/date-picker/internals/DatePicker.styles.js +336 -0
- package/build/src/md/date-picker/internals/DatePicker.styles.js.map +1 -0
- package/build/src/md/date-picker/internals/DatePickerCalendar.d.ts +159 -0
- package/build/src/md/date-picker/internals/DatePickerCalendar.d.ts.map +1 -0
- package/build/src/md/date-picker/internals/DatePickerCalendar.js +770 -0
- package/build/src/md/date-picker/internals/DatePickerCalendar.js.map +1 -0
- package/build/src/md/date-picker/internals/DatePickerUtils.d.ts +93 -0
- package/build/src/md/date-picker/internals/DatePickerUtils.d.ts.map +1 -0
- package/build/src/md/date-picker/internals/DatePickerUtils.js +221 -0
- package/build/src/md/date-picker/internals/DatePickerUtils.js.map +1 -0
- package/build/src/md/date-picker/ui-date-picker-input.d.ts +108 -0
- package/build/src/md/date-picker/ui-date-picker-input.d.ts.map +1 -0
- package/build/src/md/date-picker/ui-date-picker-input.js +397 -0
- package/build/src/md/date-picker/ui-date-picker-input.js.map +1 -0
- package/build/src/md/date-picker/ui-date-picker-modal-input.d.ts +119 -0
- package/build/src/md/date-picker/ui-date-picker-modal-input.d.ts.map +1 -0
- package/build/src/md/date-picker/ui-date-picker-modal-input.js +473 -0
- package/build/src/md/date-picker/ui-date-picker-modal-input.js.map +1 -0
- package/build/src/md/date-picker/ui-date-picker-modal.d.ts +108 -0
- package/build/src/md/date-picker/ui-date-picker-modal.d.ts.map +1 -0
- package/build/src/md/date-picker/ui-date-picker-modal.js +344 -0
- package/build/src/md/date-picker/ui-date-picker-modal.js.map +1 -0
- package/build/src/md/dialog/internals/Dialog.styles.d.ts.map +1 -1
- package/build/src/md/dialog/internals/Dialog.styles.js +1 -0
- package/build/src/md/dialog/internals/Dialog.styles.js.map +1 -1
- package/demo/elements/har/har2.json +1 -1
- package/demo/md/date-picker/date-picker.ts +301 -0
- package/demo/md/date-picker/index.html +171 -0
- package/demo/md/index.html +2 -0
- package/package.json +1 -1
- package/src/elements/highlight/MarkdownStyles.ts +0 -13
- package/src/elements/http/BodyEditor.ts +0 -13
- package/src/elements/http/BodyTextEditor.ts +0 -13
- package/src/elements/http/BodyUrlEncodedEditor.ts +0 -13
- package/src/elements/http/UrlInput.ts +0 -13
- package/src/index.ts +17 -0
- package/src/md/button/internals/button.styles.ts +1 -1
- package/src/md/date/internals/DateTime.ts +0 -14
- package/src/md/date-picker/README.md +184 -0
- package/src/md/date-picker/index.ts +17 -0
- package/src/md/date-picker/internals/DatePicker.styles.ts +338 -0
- package/src/md/date-picker/internals/DatePickerCalendar.ts +697 -0
- package/src/md/date-picker/internals/DatePickerUtils.ts +288 -0
- package/src/md/date-picker/ui-date-picker-input.ts +272 -0
- package/src/md/date-picker/ui-date-picker-modal-input.ts +371 -0
- package/src/md/date-picker/ui-date-picker-modal.ts +263 -0
- package/src/md/dialog/internals/Dialog.styles.ts +1 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { LitElement, html, TemplateResult } from 'lit'
|
|
2
|
+
import { customElement, property, state } from 'lit/decorators.js'
|
|
3
|
+
import { modalStyles } from './internals/DatePicker.styles.js'
|
|
4
|
+
import { DateRange, formatDate, parseDate } from './internals/DatePickerUtils.js'
|
|
5
|
+
import '../../md/dialog/ui-dialog.js'
|
|
6
|
+
import '../../md/button/ui-button.js'
|
|
7
|
+
import '../../md/icon-button/ui-icon-button.js'
|
|
8
|
+
import '../../md/text-field/ui-outlined-text-field.js'
|
|
9
|
+
import '../../md/icons/ui-icon.js'
|
|
10
|
+
|
|
11
|
+
export interface ModalInputDatePickerChangeEvent {
|
|
12
|
+
range: DateRange
|
|
13
|
+
formattedRange: {
|
|
14
|
+
start: string | null
|
|
15
|
+
end: string | null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A modal date input picker for manual date entry using keyboard.
|
|
21
|
+
* Ideal for compact layouts and precise date entry.
|
|
22
|
+
*
|
|
23
|
+
* ## Usage
|
|
24
|
+
*
|
|
25
|
+
* ```html
|
|
26
|
+
* <ui-date-picker-modal-input
|
|
27
|
+
* .open=${true}
|
|
28
|
+
* .selectedRange=${{ start: new Date(), end: null }}
|
|
29
|
+
* @date-input-change=${this.handleRangeChange}
|
|
30
|
+
* @close=${this.handleClose}
|
|
31
|
+
* ></ui-date-picker-modal-input>
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* ### Single date mode
|
|
35
|
+
* ```html
|
|
36
|
+
* <ui-date-picker-modal-input
|
|
37
|
+
* .rangeMode=${false}
|
|
38
|
+
* .selectedDate=${new Date()}
|
|
39
|
+
* ></ui-date-picker-modal-input>
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
@customElement('ui-date-picker-modal-input')
|
|
43
|
+
export class UiDatePickerModalInput extends LitElement {
|
|
44
|
+
static override styles = modalStyles
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether the modal is open
|
|
48
|
+
*/
|
|
49
|
+
@property({ type: Boolean }) accessor open = false
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The modal title
|
|
53
|
+
*/
|
|
54
|
+
@property({ type: String }) override accessor title = 'Enter dates'
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Whether to use range mode (two date inputs) or single date mode
|
|
58
|
+
*/
|
|
59
|
+
@property({ type: Boolean }) accessor rangeMode = true
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Label for the start date (or single date)
|
|
63
|
+
*/
|
|
64
|
+
@property({ type: String }) accessor startLabel = 'Start date'
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Label for the end date (only used in range mode)
|
|
68
|
+
*/
|
|
69
|
+
@property({ type: String }) accessor endLabel = 'End date'
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Placeholder for date inputs
|
|
73
|
+
*/
|
|
74
|
+
@property({ type: String }) accessor placeholder = 'MM/DD/YYYY'
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The selected date (single mode)
|
|
78
|
+
*/
|
|
79
|
+
@property({ type: Object }) accessor selectedDate: Date | null = null
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The selected date range (range mode)
|
|
83
|
+
*/
|
|
84
|
+
@property({ type: Object }) accessor selectedRange: DateRange | null = null
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Minimum selectable date
|
|
88
|
+
*/
|
|
89
|
+
@property({ type: Object }) accessor minDate: Date | undefined = undefined
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Maximum selectable date
|
|
93
|
+
*/
|
|
94
|
+
@property({ type: Object }) accessor maxDate: Date | undefined = undefined
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Locale for date formatting
|
|
98
|
+
*/
|
|
99
|
+
@property({ type: String }) accessor locale: string | undefined = undefined
|
|
100
|
+
|
|
101
|
+
@state() private accessor _startInput = ''
|
|
102
|
+
|
|
103
|
+
@state() private accessor _endInput = ''
|
|
104
|
+
|
|
105
|
+
@state() private accessor _startError = ''
|
|
106
|
+
|
|
107
|
+
@state() private accessor _endError = ''
|
|
108
|
+
|
|
109
|
+
override connectedCallback(): void {
|
|
110
|
+
super.connectedCallback()
|
|
111
|
+
this._updateInputValues()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
override willUpdate(changedProperties: Map<string | number | symbol, unknown>): void {
|
|
115
|
+
if (changedProperties.has('selectedDate') || changedProperties.has('selectedRange')) {
|
|
116
|
+
this._updateInputValues()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private _updateInputValues(): void {
|
|
121
|
+
if (this.rangeMode && this.selectedRange) {
|
|
122
|
+
this._startInput = this.selectedRange.start ? formatDate(this.selectedRange.start, this.locale) : ''
|
|
123
|
+
this._endInput = this.selectedRange.end ? formatDate(this.selectedRange.end, this.locale) : ''
|
|
124
|
+
} else if (!this.rangeMode && this.selectedDate) {
|
|
125
|
+
this._startInput = formatDate(this.selectedDate, this.locale)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private _validateDate(dateString: string, isEndDate = false): Date | null {
|
|
130
|
+
if (!dateString.trim()) return null
|
|
131
|
+
|
|
132
|
+
const parsedDate = parseDate(dateString)
|
|
133
|
+
if (!parsedDate) return null
|
|
134
|
+
|
|
135
|
+
// Check min/max constraints
|
|
136
|
+
if (this.minDate && parsedDate < this.minDate) return null
|
|
137
|
+
if (this.maxDate && parsedDate > this.maxDate) return null
|
|
138
|
+
|
|
139
|
+
// For range mode, validate end date is after start date
|
|
140
|
+
if (this.rangeMode && isEndDate) {
|
|
141
|
+
const startDate = parseDate(this._startInput)
|
|
142
|
+
if (startDate && parsedDate < startDate) return null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return parsedDate
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private _handleStartInput(event: Event): void {
|
|
149
|
+
const target = event.target as HTMLInputElement
|
|
150
|
+
this._startInput = target.value
|
|
151
|
+
this._startError = ''
|
|
152
|
+
|
|
153
|
+
if (this._startInput.trim()) {
|
|
154
|
+
const date = this._validateDate(this._startInput)
|
|
155
|
+
if (!date) {
|
|
156
|
+
this._startError = 'Invalid date format or out of range'
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (this.rangeMode) {
|
|
161
|
+
const currentRange = this.selectedRange || { start: null, end: null }
|
|
162
|
+
this.selectedRange = { ...currentRange, start: date }
|
|
163
|
+
} else {
|
|
164
|
+
this.selectedDate = date
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this._dispatchChangeEvent()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private _handleEndInput(event: Event): void {
|
|
172
|
+
const target = event.target as HTMLInputElement
|
|
173
|
+
this._endInput = target.value
|
|
174
|
+
this._endError = ''
|
|
175
|
+
|
|
176
|
+
if (this._endInput.trim()) {
|
|
177
|
+
const date = this._validateDate(this._endInput, true)
|
|
178
|
+
if (!date) {
|
|
179
|
+
this._endError = 'Invalid date format, out of range, or before start date'
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const currentRange = this.selectedRange || { start: null, end: null }
|
|
184
|
+
this.selectedRange = { ...currentRange, end: date }
|
|
185
|
+
this._dispatchChangeEvent()
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private _handleCancel(): void {
|
|
190
|
+
this._dispatchCloseEvent(false)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private _handleConfirm(): void {
|
|
194
|
+
// Validate all inputs before confirming
|
|
195
|
+
let hasErrors = false
|
|
196
|
+
|
|
197
|
+
if (this.rangeMode) {
|
|
198
|
+
if (!this._startInput.trim()) {
|
|
199
|
+
this._startError = 'Start date is required'
|
|
200
|
+
hasErrors = true
|
|
201
|
+
} else if (!this._validateDate(this._startInput)) {
|
|
202
|
+
this._startError = 'Invalid start date'
|
|
203
|
+
hasErrors = true
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!this._endInput.trim()) {
|
|
207
|
+
this._endError = 'End date is required'
|
|
208
|
+
hasErrors = true
|
|
209
|
+
} else if (!this._validateDate(this._endInput, true)) {
|
|
210
|
+
this._endError = 'Invalid end date'
|
|
211
|
+
hasErrors = true
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
if (!this._startInput.trim()) {
|
|
215
|
+
this._startError = 'Date is required'
|
|
216
|
+
hasErrors = true
|
|
217
|
+
} else if (!this._validateDate(this._startInput)) {
|
|
218
|
+
this._startError = 'Invalid date'
|
|
219
|
+
hasErrors = true
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!hasErrors) {
|
|
224
|
+
this._dispatchCloseEvent(true)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private _dispatchChangeEvent(): void {
|
|
229
|
+
if (this.rangeMode && this.selectedRange) {
|
|
230
|
+
const event: ModalInputDatePickerChangeEvent = {
|
|
231
|
+
range: this.selectedRange,
|
|
232
|
+
formattedRange: {
|
|
233
|
+
start: this.selectedRange.start ? formatDate(this.selectedRange.start, this.locale) : null,
|
|
234
|
+
end: this.selectedRange.end ? formatDate(this.selectedRange.end, this.locale) : null,
|
|
235
|
+
},
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
this.dispatchEvent(
|
|
239
|
+
new CustomEvent('date-input-change', {
|
|
240
|
+
detail: event,
|
|
241
|
+
bubbles: true,
|
|
242
|
+
composed: true,
|
|
243
|
+
})
|
|
244
|
+
)
|
|
245
|
+
} else if (!this.rangeMode && this.selectedDate) {
|
|
246
|
+
this.dispatchEvent(
|
|
247
|
+
new CustomEvent('date-input-change', {
|
|
248
|
+
detail: {
|
|
249
|
+
date: this.selectedDate,
|
|
250
|
+
formattedDate: formatDate(this.selectedDate, this.locale),
|
|
251
|
+
},
|
|
252
|
+
bubbles: true,
|
|
253
|
+
composed: true,
|
|
254
|
+
})
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private _dispatchCloseEvent(confirmed: boolean): void {
|
|
260
|
+
this.dispatchEvent(
|
|
261
|
+
new CustomEvent('modal-input-close', {
|
|
262
|
+
detail: {
|
|
263
|
+
confirmed,
|
|
264
|
+
date: confirmed && !this.rangeMode ? this.selectedDate : null,
|
|
265
|
+
range: confirmed && this.rangeMode ? this.selectedRange : null,
|
|
266
|
+
},
|
|
267
|
+
bubbles: true,
|
|
268
|
+
composed: true,
|
|
269
|
+
})
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private _renderHeader(): TemplateResult {
|
|
274
|
+
return html`
|
|
275
|
+
<div class="modal-header">
|
|
276
|
+
<h2 class="modal-title">${this.title}</h2>
|
|
277
|
+
<ui-icon-button @click=${this._handleCancel} aria-label="Close" title="Close">
|
|
278
|
+
<ui-icon icon="close"></ui-icon>
|
|
279
|
+
</ui-icon-button>
|
|
280
|
+
</div>
|
|
281
|
+
`
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private _renderInputs(): TemplateResult {
|
|
285
|
+
return html`
|
|
286
|
+
<div class="modal-content">
|
|
287
|
+
<div style="display: flex; flex-direction: column; gap: 16px; width: 100%; max-width: 400px;">
|
|
288
|
+
<ui-outlined-text-field
|
|
289
|
+
.label=${this.rangeMode ? this.startLabel : 'Date'}
|
|
290
|
+
.placeholder=${this.placeholder}
|
|
291
|
+
.value=${this._startInput}
|
|
292
|
+
.errorMessage=${this._startError}
|
|
293
|
+
.error=${!!this._startError}
|
|
294
|
+
@input=${this._handleStartInput}
|
|
295
|
+
>
|
|
296
|
+
<ui-icon slot="trailing-icon" icon="calendarToday"></ui-icon>
|
|
297
|
+
</ui-outlined-text-field>
|
|
298
|
+
|
|
299
|
+
${this.rangeMode
|
|
300
|
+
? html`
|
|
301
|
+
<ui-outlined-text-field
|
|
302
|
+
.label=${this.endLabel}
|
|
303
|
+
.placeholder=${this.placeholder}
|
|
304
|
+
.value=${this._endInput}
|
|
305
|
+
.errorMessage=${this._endError}
|
|
306
|
+
.error=${!!this._endError}
|
|
307
|
+
@input=${this._handleEndInput}
|
|
308
|
+
>
|
|
309
|
+
<ui-icon slot="trailing-icon" icon="calendarToday"></ui-icon>
|
|
310
|
+
</ui-outlined-text-field>
|
|
311
|
+
`
|
|
312
|
+
: ''}
|
|
313
|
+
|
|
314
|
+
<div
|
|
315
|
+
style="margin-top: 16px; padding: 16px; background: var(--ui-color-surface-variant, #e7e0ec); border-radius: 12px;"
|
|
316
|
+
>
|
|
317
|
+
<p style="margin: 0; font-size: 14px; color: var(--ui-color-on-surface-variant, #49454f);">
|
|
318
|
+
<strong>Supported formats:</strong><br />
|
|
319
|
+
MM/DD/YYYY, MM-DD-YYYY, YYYY-MM-DD
|
|
320
|
+
</p>
|
|
321
|
+
<p style="margin: 8px 0 0; font-size: 14px; color: var(--ui-color-on-surface-variant, #49454f);">
|
|
322
|
+
<strong>Examples:</strong> 12/25/2024, 12-25-2024, 2024-12-25
|
|
323
|
+
</p>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
`
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private _renderActions(): TemplateResult {
|
|
331
|
+
const hasValidInput = this.rangeMode
|
|
332
|
+
? this._startInput.trim() && this._endInput.trim() && !this._startError && !this._endError
|
|
333
|
+
: this._startInput.trim() && !this._startError
|
|
334
|
+
|
|
335
|
+
return html`
|
|
336
|
+
<div class="modal-actions">
|
|
337
|
+
<ui-button color="text" @click=${this._handleCancel}>Cancel</ui-button>
|
|
338
|
+
<ui-button color="filled" @click=${this._handleConfirm} .disabled=${!hasValidInput}> Confirm </ui-button>
|
|
339
|
+
</div>
|
|
340
|
+
`
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
override render(): TemplateResult {
|
|
344
|
+
return html`
|
|
345
|
+
<ui-dialog .open=${this.open} @close=${this._handleCancel}>
|
|
346
|
+
${this._renderHeader()} ${this._renderInputs()} ${this._renderActions()}
|
|
347
|
+
</ui-dialog>
|
|
348
|
+
`
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
declare global {
|
|
353
|
+
interface HTMLElementTagNameMap {
|
|
354
|
+
'ui-date-picker-modal-input': UiDatePickerModalInput
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
interface HTMLElementEventMap {
|
|
358
|
+
'date-input-change': CustomEvent<
|
|
359
|
+
| ModalInputDatePickerChangeEvent
|
|
360
|
+
| {
|
|
361
|
+
date: Date
|
|
362
|
+
formattedDate: string
|
|
363
|
+
}
|
|
364
|
+
>
|
|
365
|
+
'modal-input-close': CustomEvent<{
|
|
366
|
+
confirmed: boolean
|
|
367
|
+
date?: Date | null
|
|
368
|
+
range?: DateRange | null
|
|
369
|
+
}>
|
|
370
|
+
}
|
|
371
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { LitElement, html, TemplateResult } from 'lit'
|
|
2
|
+
import { customElement, property, state } from 'lit/decorators.js'
|
|
3
|
+
import { modalStyles } from './internals/DatePicker.styles.js'
|
|
4
|
+
import { DateRange, formatDate } from './internals/DatePickerUtils.js'
|
|
5
|
+
import type { DateRangeSelectEvent } from './internals/DatePickerCalendar.js'
|
|
6
|
+
import './internals/DatePickerCalendar.js'
|
|
7
|
+
import '../../md/dialog/ui-dialog.js'
|
|
8
|
+
import '../../md/button/ui-button.js'
|
|
9
|
+
import '../../md/icon-button/ui-icon-button.js'
|
|
10
|
+
import '../../md/icons/ui-icon.js'
|
|
11
|
+
|
|
12
|
+
export interface ModalDatePickerChangeEvent {
|
|
13
|
+
range: DateRange
|
|
14
|
+
formattedRange: {
|
|
15
|
+
start: string | null
|
|
16
|
+
end: string | null
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A modal date picker for selecting date ranges.
|
|
22
|
+
* Extends full-screen and is ideal for date range selection like flight bookings.
|
|
23
|
+
*
|
|
24
|
+
* ## Usage
|
|
25
|
+
*
|
|
26
|
+
* ```html
|
|
27
|
+
* <ui-date-picker-modal
|
|
28
|
+
* .open=${true}
|
|
29
|
+
* .selectedRange=${{ start: new Date(), end: null }}
|
|
30
|
+
* @change=${this.handleRangeChange}
|
|
31
|
+
* @close=${this.handleClose}
|
|
32
|
+
* ></ui-date-picker-modal>
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* ### Custom labels
|
|
36
|
+
* ```html
|
|
37
|
+
* <ui-date-picker-modal
|
|
38
|
+
* title="Select travel dates"
|
|
39
|
+
* startLabel="Check-in"
|
|
40
|
+
* endLabel="Check-out"
|
|
41
|
+
* ></ui-date-picker-modal>
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
@customElement('ui-date-picker-modal')
|
|
45
|
+
export class UiDatePickerModal extends LitElement {
|
|
46
|
+
static override styles = modalStyles
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether the modal is open
|
|
50
|
+
*/
|
|
51
|
+
@property({ type: Boolean }) accessor open = false
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The modal title
|
|
55
|
+
*/
|
|
56
|
+
@property({ type: String }) override accessor title = 'Select dates'
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Label for the start date
|
|
60
|
+
*/
|
|
61
|
+
@property({ type: String }) accessor startLabel = 'Start date'
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Label for the end date
|
|
65
|
+
*/
|
|
66
|
+
@property({ type: String }) accessor endLabel = 'End date'
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The selected date range
|
|
70
|
+
*/
|
|
71
|
+
@property({ type: Object }) accessor selectedRange: DateRange | null = null
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Minimum selectable date
|
|
75
|
+
*/
|
|
76
|
+
@property({ type: Object }) accessor minDate: Date | undefined = undefined
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Maximum selectable date
|
|
80
|
+
*/
|
|
81
|
+
@property({ type: Object }) accessor maxDate: Date | undefined = undefined
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Array of disabled dates
|
|
85
|
+
*/
|
|
86
|
+
@property({ type: Array }) accessor disabledDates: Date[] | undefined = undefined
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Locale for date formatting
|
|
90
|
+
*/
|
|
91
|
+
@property({ type: String }) accessor locale: string | undefined = undefined
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Whether to show edit/calendar toggle button
|
|
95
|
+
*/
|
|
96
|
+
@property({ type: Boolean }) accessor showModeToggle = true
|
|
97
|
+
|
|
98
|
+
@state() private accessor _isInputMode = false
|
|
99
|
+
|
|
100
|
+
private _handleRangeSelect(event: CustomEvent<DateRangeSelectEvent>): void {
|
|
101
|
+
this.selectedRange = event.detail.range
|
|
102
|
+
this._dispatchChangeEvent()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private _handleCancel(): void {
|
|
106
|
+
this._dispatchCloseEvent(false)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private _handleConfirm(): void {
|
|
110
|
+
this._dispatchCloseEvent(true)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private _handleModeToggle(): void {
|
|
114
|
+
this._isInputMode = !this._isInputMode
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private _dispatchChangeEvent(): void {
|
|
118
|
+
if (!this.selectedRange) return
|
|
119
|
+
|
|
120
|
+
const event: ModalDatePickerChangeEvent = {
|
|
121
|
+
range: this.selectedRange,
|
|
122
|
+
formattedRange: {
|
|
123
|
+
start: this.selectedRange.start ? formatDate(this.selectedRange.start, this.locale) : null,
|
|
124
|
+
end: this.selectedRange.end ? formatDate(this.selectedRange.end, this.locale) : null,
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.dispatchEvent(
|
|
129
|
+
new CustomEvent('date-range-change', {
|
|
130
|
+
detail: event,
|
|
131
|
+
bubbles: true,
|
|
132
|
+
composed: true,
|
|
133
|
+
})
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private _dispatchCloseEvent(confirmed: boolean): void {
|
|
138
|
+
this.dispatchEvent(
|
|
139
|
+
new CustomEvent('close', {
|
|
140
|
+
detail: {
|
|
141
|
+
confirmed,
|
|
142
|
+
range: confirmed ? this.selectedRange : null,
|
|
143
|
+
},
|
|
144
|
+
bubbles: true,
|
|
145
|
+
composed: true,
|
|
146
|
+
})
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private _renderHeader(): TemplateResult {
|
|
151
|
+
return html`
|
|
152
|
+
<div class="modal-header">
|
|
153
|
+
<h2 class="modal-title">${this.title}</h2>
|
|
154
|
+
<div style="display: flex; gap: 8px;">
|
|
155
|
+
${this.showModeToggle
|
|
156
|
+
? html`
|
|
157
|
+
<ui-icon-button
|
|
158
|
+
@click=${this._handleModeToggle}
|
|
159
|
+
aria-label=${this._isInputMode ? 'Show calendar' : 'Show date input'}
|
|
160
|
+
title=${this._isInputMode ? 'Show calendar' : 'Show date input'}
|
|
161
|
+
>
|
|
162
|
+
<ui-icon icon=${this._isInputMode ? 'calendarToday' : 'edit'}></ui-icon>
|
|
163
|
+
</ui-icon-button>
|
|
164
|
+
`
|
|
165
|
+
: ''}
|
|
166
|
+
<ui-icon-button @click=${this._handleCancel} aria-label="Close" title="Close">
|
|
167
|
+
<ui-icon icon="close"></ui-icon>
|
|
168
|
+
</ui-icon-button>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
`
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private _renderDateDisplay(): TemplateResult {
|
|
175
|
+
const startDate = this.selectedRange?.start
|
|
176
|
+
const endDate = this.selectedRange?.end
|
|
177
|
+
|
|
178
|
+
return html`
|
|
179
|
+
<div class="date-range-display">
|
|
180
|
+
<div class="date-display">
|
|
181
|
+
<div class="date-label">${this.startLabel}</div>
|
|
182
|
+
<div class="date-value">${startDate ? formatDate(startDate, this.locale) : 'Select date'}</div>
|
|
183
|
+
</div>
|
|
184
|
+
<div class="date-separator">—</div>
|
|
185
|
+
<div class="date-display">
|
|
186
|
+
<div class="date-label">${this.endLabel}</div>
|
|
187
|
+
<div class="date-value">${endDate ? formatDate(endDate, this.locale) : 'Select date'}</div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
`
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private _renderCalendar(): TemplateResult {
|
|
194
|
+
const currentDate = this.selectedRange?.start || new Date()
|
|
195
|
+
|
|
196
|
+
return html`
|
|
197
|
+
<ui-date-picker-calendar
|
|
198
|
+
.year=${currentDate.getFullYear()}
|
|
199
|
+
.month=${currentDate.getMonth()}
|
|
200
|
+
.selectedRange=${this.selectedRange}
|
|
201
|
+
.rangeSelection=${true}
|
|
202
|
+
.minDate=${this.minDate}
|
|
203
|
+
.maxDate=${this.maxDate}
|
|
204
|
+
.disabledDates=${this.disabledDates}
|
|
205
|
+
.locale=${this.locale}
|
|
206
|
+
@date-range-select=${this._handleRangeSelect}
|
|
207
|
+
></ui-date-picker-calendar>
|
|
208
|
+
`
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private _renderInputMode(): TemplateResult {
|
|
212
|
+
// For now, we'll show a placeholder for input mode
|
|
213
|
+
// This could be enhanced with actual date input fields
|
|
214
|
+
return html`
|
|
215
|
+
<div style="padding: 24px; text-align: center; color: var(--ui-color-on-surface-variant);">
|
|
216
|
+
<ui-icon icon="edit" style="font-size: 48px; margin-bottom: 16px;"></ui-icon>
|
|
217
|
+
<p>Manual date input mode</p>
|
|
218
|
+
<p>This feature can be enhanced with date input fields</p>
|
|
219
|
+
</div>
|
|
220
|
+
`
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private _renderContent(): TemplateResult {
|
|
224
|
+
return html`
|
|
225
|
+
<div class="modal-content">
|
|
226
|
+
${this._renderDateDisplay()} ${this._isInputMode ? this._renderInputMode() : this._renderCalendar()}
|
|
227
|
+
</div>
|
|
228
|
+
`
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private _renderActions(): TemplateResult {
|
|
232
|
+
const hasValidRange = this.selectedRange?.start && this.selectedRange?.end
|
|
233
|
+
|
|
234
|
+
return html`
|
|
235
|
+
<div class="modal-actions">
|
|
236
|
+
<ui-button color="text" @click=${this._handleCancel}>Cancel</ui-button>
|
|
237
|
+
<ui-button color="filled" @click=${this._handleConfirm} .disabled=${!hasValidRange}> Confirm </ui-button>
|
|
238
|
+
</div>
|
|
239
|
+
`
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
override render(): TemplateResult {
|
|
243
|
+
return html`
|
|
244
|
+
<ui-dialog .open=${this.open} @close=${this._handleCancel}>
|
|
245
|
+
${this._renderHeader()} ${this._renderContent()} ${this._renderActions()}
|
|
246
|
+
</ui-dialog>
|
|
247
|
+
`
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
declare global {
|
|
252
|
+
interface HTMLElementTagNameMap {
|
|
253
|
+
'ui-date-picker-modal': UiDatePickerModal
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
interface HTMLElementEventMap {
|
|
257
|
+
'date-range-change': CustomEvent<ModalDatePickerChangeEvent>
|
|
258
|
+
'close': CustomEvent<{
|
|
259
|
+
confirmed: boolean
|
|
260
|
+
range: DateRange | null
|
|
261
|
+
}>
|
|
262
|
+
}
|
|
263
|
+
}
|