stalefish 8.0.10 → 8.0.11
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/datePicker.mjs +239 -0
- package/components/dateTimePicker.mjs +18 -354
- package/components/timePicker.mjs +198 -0
- package/index.mjs +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { html, css, formField, fieldIsTouched, rerender } from 'halfcab'
|
|
2
|
+
|
|
3
|
+
const styles = css`
|
|
4
|
+
.textfield {
|
|
5
|
+
padding: 10px;
|
|
6
|
+
border: solid 5px #c9c9c9;
|
|
7
|
+
transition: border 0.3s;
|
|
8
|
+
outline: none;
|
|
9
|
+
width: 100%;
|
|
10
|
+
font-size: 18px;
|
|
11
|
+
border-radius: 0;
|
|
12
|
+
box-shadow: none !important;
|
|
13
|
+
font-weight: normal;
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
font-family: inherit;
|
|
16
|
+
line-height: 1.4em;
|
|
17
|
+
-webkit-appearance: none;
|
|
18
|
+
-moz-appearance: none;
|
|
19
|
+
appearance: none;
|
|
20
|
+
z-index: 20;
|
|
21
|
+
position: relative;
|
|
22
|
+
height: 55px;
|
|
23
|
+
background-color: #FFF;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.label {
|
|
27
|
+
transition: opacity 0.75s;
|
|
28
|
+
border-top-right-radius: 5px;
|
|
29
|
+
border-top-left-radius: 5px;
|
|
30
|
+
user-select: none;
|
|
31
|
+
position: absolute;
|
|
32
|
+
top: -55px;
|
|
33
|
+
z-index: 10;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.clear {
|
|
37
|
+
cursor: pointer;
|
|
38
|
+
position: absolute;
|
|
39
|
+
color: #AAA;
|
|
40
|
+
font-size: 0.8em;
|
|
41
|
+
line-height: 0.9em;
|
|
42
|
+
font-weight: normal;
|
|
43
|
+
box-sizing: border-box;
|
|
44
|
+
right: 40px;
|
|
45
|
+
background-color: #EEE;
|
|
46
|
+
padding: 5px 10px;
|
|
47
|
+
z-index: 30;
|
|
48
|
+
border-radius: 3px;
|
|
49
|
+
top: 17px;
|
|
50
|
+
}
|
|
51
|
+
`
|
|
52
|
+
|
|
53
|
+
const STATE_SYMBOL = Symbol.for('stalefish.date.state')
|
|
54
|
+
const ID_MAP_SYMBOL = Symbol.for('stalefish.date.idMap')
|
|
55
|
+
|
|
56
|
+
function pad2 (n) { return (n < 10 ? '0' : '') + n }
|
|
57
|
+
function toYMD (d) { return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}` }
|
|
58
|
+
function parseYMD (str) {
|
|
59
|
+
if (!str || typeof str !== 'string') return null
|
|
60
|
+
const m = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(str)
|
|
61
|
+
if (!m) return null
|
|
62
|
+
const y = parseInt(m[1], 10)
|
|
63
|
+
const mon = parseInt(m[2], 10) - 1
|
|
64
|
+
const day = parseInt(m[3], 10)
|
|
65
|
+
const d = new Date(y, mon, day)
|
|
66
|
+
if (d.getFullYear() !== y || d.getMonth() !== mon || d.getDate() !== day) return null
|
|
67
|
+
return d
|
|
68
|
+
}
|
|
69
|
+
function daysInMonth (y, m) { return new Date(y, m + 1, 0).getDate() }
|
|
70
|
+
|
|
71
|
+
function ensureState (holdingPen, property) {
|
|
72
|
+
if (!holdingPen) return {}
|
|
73
|
+
if (!holdingPen[STATE_SYMBOL]) {
|
|
74
|
+
Object.defineProperty(holdingPen, STATE_SYMBOL, { value: {}, enumerable: false })
|
|
75
|
+
}
|
|
76
|
+
const map = holdingPen[STATE_SYMBOL]
|
|
77
|
+
if (!map[property]) {
|
|
78
|
+
map[property] = { open: false, viewMonth: null, viewYear: null, tmpDate: null }
|
|
79
|
+
}
|
|
80
|
+
return map[property]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function ensureId (holdingPen, property, uniqueKey) {
|
|
84
|
+
if (!holdingPen) return `sf-date-${property || Math.random().toString(36).slice(2)}`
|
|
85
|
+
if (!holdingPen[ID_MAP_SYMBOL]) {
|
|
86
|
+
Object.defineProperty(holdingPen, ID_MAP_SYMBOL, { value: {}, enumerable: false })
|
|
87
|
+
}
|
|
88
|
+
const map = holdingPen[ID_MAP_SYMBOL]
|
|
89
|
+
if (!map[property]) {
|
|
90
|
+
map[property] = `sf-date-${property}-${Math.random().toString(36).slice(2)}`
|
|
91
|
+
}
|
|
92
|
+
return `sf-date-${uniqueKey || map[property]}`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildCalendarUI ({ state, today, onPickDay, onPrevMonth, onNextMonth }) {
|
|
96
|
+
const y = state.viewYear
|
|
97
|
+
const m = state.viewMonth
|
|
98
|
+
const first = new Date(y, m, 1)
|
|
99
|
+
const startWeekday = first.getDay() // 0 Sun - 6 Sat
|
|
100
|
+
const dim = daysInMonth(y, m)
|
|
101
|
+
const cells = []
|
|
102
|
+
for (let i = 0; i < startWeekday; i++) cells.push(null)
|
|
103
|
+
for (let d = 1; d <= dim; d++) cells.push(d)
|
|
104
|
+
while (cells.length % 7 !== 0) cells.push(null)
|
|
105
|
+
const rows = []
|
|
106
|
+
for (let i = 0; i < cells.length; i += 7) rows.push(cells.slice(i, i + 7))
|
|
107
|
+
const header = html`<div style="display:flex; justify-content:space-between; align-items:center; padding: 6px 8px;">
|
|
108
|
+
<button type="button" onclick=${onPrevMonth} aria-label="Previous month">‹</button>
|
|
109
|
+
<div>${new Date(y, m, 1).toLocaleString(undefined, { month: 'long', year: 'numeric' })}</div>
|
|
110
|
+
<button type="button" onclick=${onNextMonth} aria-label="Next month">›</button>
|
|
111
|
+
</div>`
|
|
112
|
+
const weekdayRow = html`<div style="display:grid; grid-template-columns: repeat(7, 1fr); gap:4px; padding: 0 8px; opacity:0.7; font-size:12px;">
|
|
113
|
+
${['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(d => html`<div style="text-align:center;">${d}</div>`)}
|
|
114
|
+
</div>`
|
|
115
|
+
const grid = html`<div style="display:grid; grid-template-columns: repeat(7, 1fr); gap:4px; padding: 8px;">
|
|
116
|
+
${rows.map(week => week.map(d => {
|
|
117
|
+
if (!d) return html`<div></div>`
|
|
118
|
+
const isToday = today.getFullYear() === y && today.getMonth() === m && today.getDate() === d
|
|
119
|
+
const isSelected = state.tmpDate && state.tmpDate.getFullYear() === y && state.tmpDate.getMonth() === m && state.tmpDate.getDate() === d
|
|
120
|
+
return html`<button type="button" onclick=${() => onPickDay(d)} style="padding:6px; ${isSelected ? 'background:#007aff; color:#fff;' : ''} ${isToday ? 'border:1px solid #007aff;' : 'border:1px solid #ddd;'}">${d}</button>`
|
|
121
|
+
}))}
|
|
122
|
+
</div>`
|
|
123
|
+
return html`<div>${header}${weekdayRow}${grid}</div>`
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function commitValue ({ holdingPen, property, onchange, valueStr }) {
|
|
127
|
+
const fauxE = { currentTarget: { validity: { valid: true }, value: valueStr } }
|
|
128
|
+
formField(holdingPen, property)(fauxE)
|
|
129
|
+
onchange && onchange(fauxE)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export default function datePicker ({
|
|
133
|
+
wrapperStyle = null,
|
|
134
|
+
holdingPen,
|
|
135
|
+
label,
|
|
136
|
+
placeholder,
|
|
137
|
+
property,
|
|
138
|
+
required,
|
|
139
|
+
pattern,
|
|
140
|
+
disabled,
|
|
141
|
+
disableClear = false,
|
|
142
|
+
onchange,
|
|
143
|
+
oninput,
|
|
144
|
+
uniqueKey
|
|
145
|
+
} = {}) {
|
|
146
|
+
const wrapperId = ensureId(holdingPen, property, uniqueKey)
|
|
147
|
+
const state = ensureState(holdingPen, property)
|
|
148
|
+
|
|
149
|
+
const currentStr = holdingPen && property ? holdingPen[property] : ''
|
|
150
|
+
const now = new Date()
|
|
151
|
+
if (state.viewMonth === null || state.viewYear === null) {
|
|
152
|
+
const baseDate = parseYMD(currentStr) || now
|
|
153
|
+
state.viewMonth = baseDate.getMonth()
|
|
154
|
+
state.viewYear = baseDate.getFullYear()
|
|
155
|
+
}
|
|
156
|
+
if (!state.tmpDate) {
|
|
157
|
+
state.tmpDate = parseYMD(currentStr) || new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const commit = () => {
|
|
161
|
+
const valueStr = state.tmpDate ? toYMD(state.tmpDate) : ''
|
|
162
|
+
commitValue({ holdingPen, property, onchange, valueStr })
|
|
163
|
+
state.open = false
|
|
164
|
+
rerender()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const clearValue = (e) => {
|
|
168
|
+
if (e) { e.stopPropagation(); e.preventDefault() }
|
|
169
|
+
commitValue({ holdingPen, property, onchange, valueStr: '' })
|
|
170
|
+
state.open = false
|
|
171
|
+
rerender()
|
|
172
|
+
return false
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const onInputChange = (e) => {
|
|
176
|
+
// let them type YYYY-MM-DD; commit only if parsing succeeds
|
|
177
|
+
const v = (e?.target?.value || '').trim()
|
|
178
|
+
const d = parseYMD(v)
|
|
179
|
+
if (d) {
|
|
180
|
+
state.tmpDate = d
|
|
181
|
+
commit()
|
|
182
|
+
} else {
|
|
183
|
+
oninput && oninput(e)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const open = (e) => {
|
|
188
|
+
if (disabled) return
|
|
189
|
+
e && e.stopPropagation()
|
|
190
|
+
// Only toggle open on click to avoid focus+click double toggle flicker
|
|
191
|
+
state.open = !state.open
|
|
192
|
+
rerender()
|
|
193
|
+
if (typeof window !== 'undefined' && state.open) {
|
|
194
|
+
const closeOnOutside = (ev) => {
|
|
195
|
+
const wrapperEl = document.getElementById(wrapperId)
|
|
196
|
+
if (!wrapperEl) return
|
|
197
|
+
if (!wrapperEl.contains(ev.target)) {
|
|
198
|
+
state.open = false
|
|
199
|
+
rerender()
|
|
200
|
+
document.removeEventListener('mousedown', closeOnOutside, true)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Defer attaching to next tick to avoid capturing the very click that opened
|
|
204
|
+
setTimeout(() => document.addEventListener('mousedown', closeOnOutside, true), 0)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const popup = state.open
|
|
209
|
+
? html`<div role="dialog" aria-modal="false" style="position:absolute; z-index: 9999; top: 55px; left: 0; background: #fff; border: 1px solid #ddd; box-shadow: 0 2px 10px rgba(0,0,0,.15); padding: 4px;">
|
|
210
|
+
${buildCalendarUI({
|
|
211
|
+
state,
|
|
212
|
+
today: new Date(),
|
|
213
|
+
onPickDay: (d) => { state.tmpDate = new Date(state.viewYear, state.viewMonth, d); rerender() },
|
|
214
|
+
onPrevMonth: () => { if (state.viewMonth === 0) { state.viewMonth = 11; state.viewYear -= 1 } else { state.viewMonth -= 1 }; rerender() },
|
|
215
|
+
onNextMonth: () => { if (state.viewMonth === 11) { state.viewMonth = 0; state.viewYear += 1 } else { state.viewMonth += 1 }; rerender() }
|
|
216
|
+
})}
|
|
217
|
+
<div style="display:flex; justify-content:flex-end; padding: 8px;"><button type="button" onclick=${commit}>Apply</button></div>
|
|
218
|
+
</div>`
|
|
219
|
+
: ''
|
|
220
|
+
|
|
221
|
+
const displayValue = currentStr || ''
|
|
222
|
+
|
|
223
|
+
return html`
|
|
224
|
+
<div id="${wrapperId}" class="${wrapperStyle}" style="min-height: 55px; display: inline-block; width: calc(100% - 10px); margin: 40px 5px 5px 5px;">
|
|
225
|
+
<div style="display: inline-block; width: 100%; text-align: left; position: relative; padding: 0;" onclick=${e => e.stopPropagation()}>
|
|
226
|
+
${label ? html`<span class="${styles.label}" style="opacity: ${(holdingPen && (holdingPen[property] === 0 || holdingPen[property])) ? 1 : 0}; font-size: 16px; font-weight: normal; color: #999; margin-left: 5px; padding: 9px; background-color: rgba(255,255,255,0.8); position: absolute; top: -36px;">${label}${required ? ' *' : ''}</span>` : ''}
|
|
227
|
+
${!disableClear ? html`<div data-clear class="${styles.clear}" onclick=${clearValue}>clear</div>` : ''}
|
|
228
|
+
<input data-gramm="false" ?disabled=${disabled} style="${disabled ? 'cursor: not-allowed; opacity: 0.3;' : ''}" class="${styles.textfield} ${fieldIsTouched(holdingPen, property) === true ? styles.touched : ''}" ${required ? { required: 'required' } : ''}
|
|
229
|
+
onclick=${open}
|
|
230
|
+
onchange=${onInputChange}
|
|
231
|
+
oninput=${onInputChange}
|
|
232
|
+
placeholder="${(placeholder || 'Date') + (required ? ' *' : '')}"
|
|
233
|
+
type="text" ${pattern ? { pattern } : ''}
|
|
234
|
+
.value=${displayValue} data-input />
|
|
235
|
+
${popup}
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
`
|
|
239
|
+
}
|
|
@@ -1,357 +1,21 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
font-weight: normal;
|
|
16
|
-
box-sizing: border-box;
|
|
17
|
-
font-family: inherit;
|
|
18
|
-
line-height: 1.4em;
|
|
19
|
-
-webkit-appearance: none;
|
|
20
|
-
-moz-appearance: none;
|
|
21
|
-
appearance: none;
|
|
22
|
-
z-index: 20;
|
|
23
|
-
position: relative;
|
|
24
|
-
height: 55px;
|
|
25
|
-
background-color: #FFF;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
.label {
|
|
29
|
-
transition: opacity 0.75s;
|
|
30
|
-
border-top-right-radius: 5px;
|
|
31
|
-
border-top-left-radius: 5px;
|
|
32
|
-
user-select: none;
|
|
33
|
-
position: absolute;
|
|
34
|
-
top: -55px;
|
|
35
|
-
z-index: 10;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
.textfield:focus {
|
|
39
|
-
border: solid 5px #969696;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
.textfield::placeholder {
|
|
43
|
-
color: #999;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.textfield.touched:invalid:not(:focus) {
|
|
47
|
-
border-color: red;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
.icon {
|
|
51
|
-
position: absolute;
|
|
52
|
-
pointer-events: none;
|
|
53
|
-
color: #c9c9c9;
|
|
54
|
-
right: 12px;
|
|
55
|
-
font-size: 2em;
|
|
56
|
-
z-index: 30;
|
|
57
|
-
position: absolute;
|
|
58
|
-
top: 9px;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.clear {
|
|
62
|
-
cursor: pointer;
|
|
63
|
-
position: absolute;
|
|
64
|
-
color: #AAA;
|
|
65
|
-
font-size: 0.8em;
|
|
66
|
-
line-height: 0.9em;
|
|
67
|
-
font-weight: normal;
|
|
68
|
-
box-sizing: border-box;
|
|
69
|
-
top: -1px;
|
|
70
|
-
right: 40px;
|
|
71
|
-
background-color: #EEE;
|
|
72
|
-
padding: 5px 10px;
|
|
73
|
-
z-index: 30;
|
|
74
|
-
border-radius: 3px;
|
|
75
|
-
position: absolute;
|
|
76
|
-
top: 17px;
|
|
77
|
-
}
|
|
78
|
-
`
|
|
79
|
-
|
|
80
|
-
function change ({ e, holdingPen, property }) {
|
|
81
|
-
const ff = formField(holdingPen, property)(e)
|
|
82
|
-
// e.target.focus()
|
|
83
|
-
return ff
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Touchscreen detection is no longer needed in the native picker
|
|
87
|
-
|
|
88
|
-
// --------------------
|
|
89
|
-
// Native date/time picker implementation (no external libs)
|
|
90
|
-
// --------------------
|
|
91
|
-
|
|
92
|
-
const STATE_SYMBOL = Symbol.for('stalefish.dtp.state')
|
|
93
|
-
const ID_MAP_SYMBOL = Symbol.for('stalefish.dtp.idMap')
|
|
94
|
-
|
|
95
|
-
function pad2 (n) { return (n < 10 ? '0' : '') + n }
|
|
96
|
-
function toYMD (d) { return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}` }
|
|
97
|
-
// function toHM (d) { return `${pad2(d.getHours())}:${pad2(d.getMinutes())}` }
|
|
98
|
-
function parseYMD (str) {
|
|
99
|
-
if (!str || typeof str !== 'string') return null
|
|
100
|
-
const m = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(str)
|
|
101
|
-
if (!m) return null
|
|
102
|
-
const y = parseInt(m[1], 10)
|
|
103
|
-
const mon = parseInt(m[2], 10) - 1
|
|
104
|
-
const day = parseInt(m[3], 10)
|
|
105
|
-
const d = new Date(y, mon, day)
|
|
106
|
-
if (d.getFullYear() !== y || d.getMonth() !== mon || d.getDate() !== day) return null
|
|
107
|
-
return d
|
|
108
|
-
}
|
|
109
|
-
function parseHM (str) {
|
|
110
|
-
if (!str || typeof str !== 'string') return null
|
|
111
|
-
const m = /^([0-9]{1,2}):([0-9]{2})$/.exec(str)
|
|
112
|
-
if (!m) return null
|
|
113
|
-
const h = parseInt(m[1], 10)
|
|
114
|
-
const min = parseInt(m[2], 10)
|
|
115
|
-
if (h < 0 || h > 23 || min < 0 || min > 59) return null
|
|
116
|
-
return { h, min }
|
|
117
|
-
}
|
|
118
|
-
function daysInMonth (y, m) { return new Date(y, m + 1, 0).getDate() }
|
|
119
|
-
|
|
120
|
-
function ensureState (holdingPen, property) {
|
|
121
|
-
if (!holdingPen) return {}
|
|
122
|
-
if (!holdingPen[STATE_SYMBOL]) {
|
|
123
|
-
Object.defineProperty(holdingPen, STATE_SYMBOL, { value: {}, enumerable: false })
|
|
124
|
-
}
|
|
125
|
-
const map = holdingPen[STATE_SYMBOL]
|
|
126
|
-
if (!map[property]) {
|
|
127
|
-
map[property] = { open: false, viewMonth: null, viewYear: null, tmpDate: null, tmpHour: 0, tmpMinute: 0 }
|
|
128
|
-
}
|
|
129
|
-
return map[property]
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function ensureId (holdingPen, property, uniqueKey) {
|
|
133
|
-
if (!holdingPen) return `sf-dtp-${property || Math.random().toString(36).slice(2)}`
|
|
134
|
-
if (!holdingPen[ID_MAP_SYMBOL]) {
|
|
135
|
-
Object.defineProperty(holdingPen, ID_MAP_SYMBOL, { value: {}, enumerable: false })
|
|
136
|
-
}
|
|
137
|
-
const map = holdingPen[ID_MAP_SYMBOL]
|
|
138
|
-
if (!map[property]) {
|
|
139
|
-
map[property] = `sf-dtp-${property}-${Math.random().toString(36).slice(2)}`
|
|
140
|
-
}
|
|
141
|
-
return `sf-dtp-${uniqueKey || map[property]}`
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function commitValue ({ mode, holdingPen, property, onchange, valueStr }) {
|
|
145
|
-
const fauxE = { currentTarget: { validity: { valid: true }, value: valueStr } }
|
|
146
|
-
formField(holdingPen, property)(fauxE)
|
|
147
|
-
onchange && onchange(fauxE)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function buildTimeUI ({ state, commit, minuteStep = 1 }) {
|
|
151
|
-
const inc = (key, delta, max) => () => {
|
|
152
|
-
state[key] = (state[key] + delta + (max + 1)) % (max + 1)
|
|
153
|
-
rerender()
|
|
154
|
-
}
|
|
155
|
-
const setFromInput = (key, max) => e => {
|
|
156
|
-
const v = parseInt(e.target.value, 10)
|
|
157
|
-
if (!isNaN(v) && v >= 0 && v <= max) {
|
|
158
|
-
state[key] = v
|
|
159
|
-
rerender()
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
const stepMinute = (delta) => () => {
|
|
163
|
-
state.tmpMinute = (state.tmpMinute + delta + 60) % 60
|
|
164
|
-
rerender()
|
|
165
|
-
}
|
|
166
|
-
return html`
|
|
167
|
-
<div style="display:flex; gap: 12px; align-items: center; padding: 8px;">
|
|
168
|
-
<div style="display:flex; flex-direction:column; align-items:center;">
|
|
169
|
-
<button type="button" onclick=${inc('tmpHour', 1, 23)} aria-label="Increase hour">▲</button>
|
|
170
|
-
<input style="width:3em; text-align:center;" value=${pad2(state.tmpHour)} oninput=${setFromInput('tmpHour', 23)} />
|
|
171
|
-
<button type="button" onclick=${inc('tmpHour', -1, 23)} aria-label="Decrease hour">▼</button>
|
|
172
|
-
</div>
|
|
173
|
-
<div>:</div>
|
|
174
|
-
<div style="display:flex; flex-direction:column; align-items:center;">
|
|
175
|
-
<button type="button" onclick=${stepMinute(minuteStep)} aria-label="Increase minute">▲</button>
|
|
176
|
-
<input style="width:3em; text-align:center;" value=${pad2(state.tmpMinute)} oninput=${setFromInput('tmpMinute', 59)} />
|
|
177
|
-
<button type="button" onclick=${stepMinute(-minuteStep)} aria-label="Decrease minute">▼</button>
|
|
178
|
-
</div>
|
|
179
|
-
<button type="button" style="margin-left:auto;" onclick=${commit}>Apply</button>
|
|
180
|
-
</div>`
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function buildCalendarUI ({ state, today, onPickDay, onPrevMonth, onNextMonth }) {
|
|
184
|
-
const y = state.viewYear
|
|
185
|
-
const m = state.viewMonth
|
|
186
|
-
const first = new Date(y, m, 1)
|
|
187
|
-
const startWeekday = first.getDay() // 0 Sun - 6 Sat
|
|
188
|
-
const dim = daysInMonth(y, m)
|
|
189
|
-
const cells = []
|
|
190
|
-
for (let i = 0; i < startWeekday; i++) cells.push(null)
|
|
191
|
-
for (let d = 1; d <= dim; d++) cells.push(d)
|
|
192
|
-
while (cells.length % 7 !== 0) cells.push(null)
|
|
193
|
-
const rows = []
|
|
194
|
-
for (let i = 0; i < cells.length; i += 7) rows.push(cells.slice(i, i + 7))
|
|
195
|
-
const header = html`<div style="display:flex; justify-content:space-between; align-items:center; padding: 6px 8px;">
|
|
196
|
-
<button type="button" onclick=${onPrevMonth} aria-label="Previous month">‹</button>
|
|
197
|
-
<div>${new Date(y, m, 1).toLocaleString(undefined, { month: 'long', year: 'numeric' })}</div>
|
|
198
|
-
<button type="button" onclick=${onNextMonth} aria-label="Next month">›</button>
|
|
199
|
-
</div>`
|
|
200
|
-
const weekdayRow = html`<div style="display:grid; grid-template-columns: repeat(7, 1fr); gap:4px; padding: 0 8px; opacity:0.7; font-size:12px;">
|
|
201
|
-
${['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(d => html`<div style="text-align:center;">${d}</div>`)}
|
|
202
|
-
</div>`
|
|
203
|
-
const grid = html`<div style="display:grid; grid-template-columns: repeat(7, 1fr); gap:4px; padding: 8px;">
|
|
204
|
-
${rows.map(week => week.map(d => {
|
|
205
|
-
if (!d) return html`<div></div>`
|
|
206
|
-
const isToday = today.getFullYear() === y && today.getMonth() === m && today.getDate() === d
|
|
207
|
-
const isSelected = state.tmpDate && state.tmpDate.getFullYear() === y && state.tmpDate.getMonth() === m && state.tmpDate.getDate() === d
|
|
208
|
-
return html`<button type="button" onclick=${() => onPickDay(d)} style="padding:6px; ${isSelected ? 'background:#007aff; color:#fff;' : ''} ${isToday ? 'border:1px solid #007aff;' : 'border:1px solid #ddd;'}">${d}</button>`
|
|
209
|
-
}))}
|
|
210
|
-
</div>`
|
|
211
|
-
return html`<div>${header}${weekdayRow}${grid}</div>`
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export default function dateTimePicker ({
|
|
215
|
-
wrapperStyle = null,
|
|
216
|
-
holdingPen,
|
|
217
|
-
label,
|
|
218
|
-
placeholder,
|
|
219
|
-
property,
|
|
220
|
-
required,
|
|
221
|
-
pattern,
|
|
222
|
-
permanentTopPlaceholder = true,
|
|
223
|
-
permanentTopLabel = false,
|
|
224
|
-
flatpickrConfig = {}, // ignored for compatibility
|
|
225
|
-
timeOnly = false,
|
|
226
|
-
dateTime = false, // new optional combined mode
|
|
227
|
-
disabled,
|
|
228
|
-
disableClear = false,
|
|
229
|
-
onchange,
|
|
230
|
-
oninput,
|
|
231
|
-
uniqueKey
|
|
232
|
-
} = {}) {
|
|
233
|
-
// Initialize per-field state and id
|
|
234
|
-
const state = ensureState(holdingPen, property)
|
|
235
|
-
const wrapperId = ensureId(holdingPen, property, uniqueKey)
|
|
236
|
-
|
|
237
|
-
// Derive current value and seed temp state if needed
|
|
238
|
-
const currentStr = holdingPen && property ? holdingPen[property] : ''
|
|
239
|
-
const now = new Date()
|
|
240
|
-
const openMode = timeOnly ? 'time' : (dateTime ? 'datetime' : 'date')
|
|
241
|
-
if (state.viewMonth === null || state.viewYear === null) {
|
|
242
|
-
const baseDate = parseYMD(currentStr) || now
|
|
243
|
-
state.viewMonth = baseDate.getMonth()
|
|
244
|
-
state.viewYear = baseDate.getFullYear()
|
|
245
|
-
}
|
|
246
|
-
if (!state.tmpDate) {
|
|
247
|
-
state.tmpDate = parseYMD(currentStr) || new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
|
248
|
-
}
|
|
249
|
-
const hm = parseHM(currentStr?.split(' ')?.[1] || (!timeOnly && !dateTime ? '' : currentStr)) || { h: now.getHours(), min: now.getMinutes() }
|
|
250
|
-
if (typeof state.tmpHour !== 'number') state.tmpHour = hm.h
|
|
251
|
-
if (typeof state.tmpMinute !== 'number') state.tmpMinute = hm.min
|
|
252
|
-
|
|
253
|
-
const icon = timeOnly ? timeIcon({ colour: '#ccc' }) : calendarIcon({ colour: '#ccc' })
|
|
254
|
-
|
|
255
|
-
const commit = () => {
|
|
256
|
-
let valueStr = ''
|
|
257
|
-
if (openMode === 'time') {
|
|
258
|
-
valueStr = `${pad2(state.tmpHour)}:${pad2(state.tmpMinute)}`
|
|
259
|
-
} else if (openMode === 'date') {
|
|
260
|
-
valueStr = toYMD(state.tmpDate)
|
|
261
|
-
} else {
|
|
262
|
-
valueStr = `${toYMD(state.tmpDate)} ${pad2(state.tmpHour)}:${pad2(state.tmpMinute)}`
|
|
1
|
+
import datePicker from './datePicker.mjs'
|
|
2
|
+
import timePicker from './timePicker.mjs'
|
|
3
|
+
|
|
4
|
+
// Delegator component for backward compatibility.
|
|
5
|
+
// New usage is to import and use datePicker or timePicker directly.
|
|
6
|
+
export default function dateTimePicker (props = {}) {
|
|
7
|
+
const { timeOnly, dateTime } = props
|
|
8
|
+
if (timeOnly) {
|
|
9
|
+
// Inline controls, no popup
|
|
10
|
+
return timePicker({ ...props })
|
|
11
|
+
}
|
|
12
|
+
if (dateTime) {
|
|
13
|
+
if (typeof console !== 'undefined' && console && console.warn) {
|
|
14
|
+
console.warn('stalefish/dateTimePicker: combined dateTime mode is deprecated. Use separate datePicker and timePicker fields.')
|
|
263
15
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
rerender()
|
|
16
|
+
// Fall back to showing just the date portion to avoid breaking UIs.
|
|
17
|
+
return datePicker({ ...props })
|
|
267
18
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (e) {
|
|
271
|
-
e.stopPropagation()
|
|
272
|
-
e.preventDefault()
|
|
273
|
-
}
|
|
274
|
-
commitValue({ mode: openMode, holdingPen, property, onchange, valueStr: '' })
|
|
275
|
-
state.open = false
|
|
276
|
-
rerender()
|
|
277
|
-
return false
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const onInputChange = (e) => {
|
|
281
|
-
change({ e, holdingPen, property, label: styles.label })
|
|
282
|
-
onchange && onchange(e)
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const toggleOpen = (e) => {
|
|
286
|
-
if (disabled) return
|
|
287
|
-
e && e.stopPropagation()
|
|
288
|
-
state.open = !state.open
|
|
289
|
-
rerender()
|
|
290
|
-
if (typeof window !== 'undefined' && state.open) {
|
|
291
|
-
// Click outside to close
|
|
292
|
-
const closeOnOutside = (ev) => {
|
|
293
|
-
const wrapperEl = document.getElementById(wrapperId)
|
|
294
|
-
if (!wrapperEl) return
|
|
295
|
-
if (!wrapperEl.contains(ev.target)) {
|
|
296
|
-
state.open = false
|
|
297
|
-
rerender()
|
|
298
|
-
document.removeEventListener('mousedown', closeOnOutside, true)
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
document.addEventListener('mousedown', closeOnOutside, true)
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Compute displayed value
|
|
306
|
-
const displayValue = (() => {
|
|
307
|
-
if (!currentStr) return ''
|
|
308
|
-
return currentStr
|
|
309
|
-
})()
|
|
310
|
-
|
|
311
|
-
const calendarPart = (() => {
|
|
312
|
-
if (openMode === 'time') return html``
|
|
313
|
-
return buildCalendarUI({
|
|
314
|
-
state,
|
|
315
|
-
today: new Date(),
|
|
316
|
-
onPickDay: (d) => { state.tmpDate = new Date(state.viewYear, state.viewMonth, d); rerender() },
|
|
317
|
-
onPrevMonth: () => {
|
|
318
|
-
if (state.viewMonth === 0) { state.viewMonth = 11; state.viewYear -= 1 } else { state.viewMonth -= 1 }
|
|
319
|
-
rerender()
|
|
320
|
-
},
|
|
321
|
-
onNextMonth: () => {
|
|
322
|
-
if (state.viewMonth === 11) { state.viewMonth = 0; state.viewYear += 1 } else { state.viewMonth += 1 }
|
|
323
|
-
rerender()
|
|
324
|
-
}
|
|
325
|
-
})
|
|
326
|
-
})()
|
|
327
|
-
|
|
328
|
-
const timePart = (() => {
|
|
329
|
-
if (openMode === 'date') {
|
|
330
|
-
return html`<div style="display:flex; justify-content:flex-end; padding: 8px;"><button type="button" onclick=${commit}>Apply</button></div>`
|
|
331
|
-
}
|
|
332
|
-
return buildTimeUI({ state, commit, minuteStep: 1 })
|
|
333
|
-
})()
|
|
334
|
-
|
|
335
|
-
const popup = state.open
|
|
336
|
-
? html`<div role="dialog" aria-modal="false" style="position:absolute; z-index: 9999; top: 55px; left: 0; background: #fff; border: 1px solid #ddd; box-shadow: 0 2px 10px rgba(0,0,0,.15); padding: 4px;">${calendarPart}${timePart}</div>`
|
|
337
|
-
: ''
|
|
338
|
-
|
|
339
|
-
return html`
|
|
340
|
-
<div id="${wrapperId}" class="${wrapperStyle}" style="min-height: 55px; display: inline-block; width: calc(100% - 10px); margin: 40px 5px 5px 5px;">
|
|
341
|
-
<div style="display: inline-block; width: 100%; text-align: left; position: relative; padding: 0;" onclick=${e => e.stopPropagation()}>
|
|
342
|
-
<div class="${styles.icon}">${icon}</div>
|
|
343
|
-
${label ? html`<span class="${styles.label}" style="opacity: ${holdingPen[property] === 0 || holdingPen[property] || (permanentTopPlaceholder || permanentTopLabel) ? 1 : 0}; font-size: 16px; font-weight: normal; color: #999; margin-left: 5px; padding: 9px; background-color: rgba(255,255,255,0.8); position: absolute; top: -36px;">${label}${required ? ' *' : ''}</span>` : ''}
|
|
344
|
-
${!disableClear ? html`<div data-clear class="${styles.clear}" onclick=${clearValue}>clear</div>` : ''}
|
|
345
|
-
<input data-gramm="false" ?disabled=${disabled} style="${disabled ? 'cursor: not-allowed; opacity: 0.3;' : ''}" class="${styles.textfield} ${fieldIsTouched(holdingPen, property) === true ? styles.touched : ''}" ${required ? { required: 'required' } : ''}
|
|
346
|
-
onclick=${toggleOpen}
|
|
347
|
-
onfocus=${toggleOpen}
|
|
348
|
-
onchange=${onInputChange}
|
|
349
|
-
oninput=${e => { e.target.defaultValue = ''; oninput && oninput(e) }}
|
|
350
|
-
placeholder="${placeholder || ''}${required ? ' *' : ''}"
|
|
351
|
-
type="text" ${pattern ? { pattern } : ''}
|
|
352
|
-
.value=${displayValue} data-input />
|
|
353
|
-
${popup}
|
|
354
|
-
</div>
|
|
355
|
-
</div>
|
|
356
|
-
`
|
|
19
|
+
// Default: date-only picker with popup
|
|
20
|
+
return datePicker({ ...props })
|
|
357
21
|
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { html, css, formField, fieldIsTouched, rerender } from 'halfcab'
|
|
2
|
+
|
|
3
|
+
const styles = css`
|
|
4
|
+
.textfield {
|
|
5
|
+
padding: 10px;
|
|
6
|
+
border: solid 5px #c9c9c9;
|
|
7
|
+
transition: border 0.3s;
|
|
8
|
+
outline: none;
|
|
9
|
+
width: 100%;
|
|
10
|
+
font-size: 18px;
|
|
11
|
+
border-radius: 0;
|
|
12
|
+
box-shadow: none !important;
|
|
13
|
+
font-weight: normal;
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
font-family: inherit;
|
|
16
|
+
line-height: 1.4em;
|
|
17
|
+
-webkit-appearance: none;
|
|
18
|
+
-moz-appearance: none;
|
|
19
|
+
appearance: none;
|
|
20
|
+
z-index: 20;
|
|
21
|
+
position: relative;
|
|
22
|
+
height: 55px;
|
|
23
|
+
background-color: #FFF;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.label {
|
|
27
|
+
transition: opacity 0.75s;
|
|
28
|
+
border-top-right-radius: 5px;
|
|
29
|
+
border-top-left-radius: 5px;
|
|
30
|
+
user-select: none;
|
|
31
|
+
position: absolute;
|
|
32
|
+
top: -55px;
|
|
33
|
+
z-index: 10;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.controls {
|
|
37
|
+
position: absolute;
|
|
38
|
+
right: 10px;
|
|
39
|
+
top: 12px;
|
|
40
|
+
display: flex;
|
|
41
|
+
gap: 6px;
|
|
42
|
+
z-index: 30;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.btn {
|
|
46
|
+
cursor: pointer;
|
|
47
|
+
background: #eee;
|
|
48
|
+
border: 1px solid #ddd;
|
|
49
|
+
border-radius: 3px;
|
|
50
|
+
padding: 6px 8px;
|
|
51
|
+
line-height: 1;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.clear {
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
position: absolute;
|
|
57
|
+
color: #AAA;
|
|
58
|
+
font-size: 0.8em;
|
|
59
|
+
line-height: 0.9em;
|
|
60
|
+
font-weight: normal;
|
|
61
|
+
box-sizing: border-box;
|
|
62
|
+
right: 72px;
|
|
63
|
+
background-color: #EEE;
|
|
64
|
+
padding: 5px 10px;
|
|
65
|
+
z-index: 30;
|
|
66
|
+
border-radius: 3px;
|
|
67
|
+
top: 17px;
|
|
68
|
+
}
|
|
69
|
+
`
|
|
70
|
+
|
|
71
|
+
const STATE_SYMBOL = Symbol.for('stalefish.time.state')
|
|
72
|
+
const ID_MAP_SYMBOL = Symbol.for('stalefish.time.idMap')
|
|
73
|
+
|
|
74
|
+
function pad2 (n) { return (n < 10 ? '0' : '') + n }
|
|
75
|
+
function parseHM (str) {
|
|
76
|
+
if (!str || typeof str !== 'string') return null
|
|
77
|
+
const m = /^([0-9]{1,2}):([0-9]{2})$/.exec(str)
|
|
78
|
+
if (!m) return null
|
|
79
|
+
const h = parseInt(m[1], 10)
|
|
80
|
+
const min = parseInt(m[2], 10)
|
|
81
|
+
if (h < 0 || h > 23 || min < 0 || min > 59) return null
|
|
82
|
+
return { h, min }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function ensureState (holdingPen, property) {
|
|
86
|
+
if (!holdingPen) return {}
|
|
87
|
+
if (!holdingPen[STATE_SYMBOL]) {
|
|
88
|
+
Object.defineProperty(holdingPen, STATE_SYMBOL, { value: {}, enumerable: false })
|
|
89
|
+
}
|
|
90
|
+
const map = holdingPen[STATE_SYMBOL]
|
|
91
|
+
if (!map[property]) {
|
|
92
|
+
map[property] = { tmpHour: 0, tmpMinute: 0 }
|
|
93
|
+
}
|
|
94
|
+
return map[property]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function ensureId (holdingPen, property, uniqueKey) {
|
|
98
|
+
if (!holdingPen) return `sf-time-${property || Math.random().toString(36).slice(2)}`
|
|
99
|
+
if (!holdingPen[ID_MAP_SYMBOL]) {
|
|
100
|
+
Object.defineProperty(holdingPen, ID_MAP_SYMBOL, { value: {}, enumerable: false })
|
|
101
|
+
}
|
|
102
|
+
const map = holdingPen[ID_MAP_SYMBOL]
|
|
103
|
+
if (!map[property]) {
|
|
104
|
+
map[property] = `sf-time-${property}-${Math.random().toString(36).slice(2)}`
|
|
105
|
+
}
|
|
106
|
+
return `sf-time-${uniqueKey || map[property]}`
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function commitValue ({ holdingPen, property, onchange, valueStr }) {
|
|
110
|
+
const fauxE = { currentTarget: { validity: { valid: true }, value: valueStr } }
|
|
111
|
+
formField(holdingPen, property)(fauxE)
|
|
112
|
+
onchange && onchange(fauxE)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export default function timePicker ({
|
|
116
|
+
wrapperStyle = null,
|
|
117
|
+
holdingPen,
|
|
118
|
+
label,
|
|
119
|
+
placeholder,
|
|
120
|
+
property,
|
|
121
|
+
required,
|
|
122
|
+
pattern,
|
|
123
|
+
disabled,
|
|
124
|
+
disableClear = false,
|
|
125
|
+
minuteStep = 15,
|
|
126
|
+
onchange,
|
|
127
|
+
oninput,
|
|
128
|
+
uniqueKey
|
|
129
|
+
} = {}) {
|
|
130
|
+
const wrapperId = ensureId(holdingPen, property, uniqueKey)
|
|
131
|
+
const state = ensureState(holdingPen, property)
|
|
132
|
+
|
|
133
|
+
const currentStr = holdingPen && property ? holdingPen[property] : ''
|
|
134
|
+
const now = new Date()
|
|
135
|
+
const hm = parseHM(currentStr) || { h: now.getHours(), min: Math.floor(now.getMinutes() / minuteStep) * minuteStep }
|
|
136
|
+
if (typeof state.tmpHour !== 'number') state.tmpHour = hm.h
|
|
137
|
+
if (typeof state.tmpMinute !== 'number') state.tmpMinute = hm.min
|
|
138
|
+
|
|
139
|
+
const stepMinute = (delta) => () => {
|
|
140
|
+
state.tmpMinute = (state.tmpMinute + delta + 60) % 60
|
|
141
|
+
const valueStr = `${pad2(state.tmpHour)}:${pad2(state.tmpMinute)}`
|
|
142
|
+
commitValue({ holdingPen, property, onchange, valueStr })
|
|
143
|
+
rerender()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const stepHour = (delta) => () => {
|
|
147
|
+
state.tmpHour = (state.tmpHour + delta + 24) % 24
|
|
148
|
+
const valueStr = `${pad2(state.tmpHour)}:${pad2(state.tmpMinute)}`
|
|
149
|
+
commitValue({ holdingPen, property, onchange, valueStr })
|
|
150
|
+
rerender()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const onTypedChange = (e) => {
|
|
154
|
+
// Allow free typing of HH:mm
|
|
155
|
+
const v = (e?.target?.value || '').trim()
|
|
156
|
+
const parsed = parseHM(v)
|
|
157
|
+
if (parsed) {
|
|
158
|
+
state.tmpHour = parsed.h
|
|
159
|
+
state.tmpMinute = parsed.min
|
|
160
|
+
commitValue({ holdingPen, property, onchange, valueStr: `${pad2(parsed.h)}:${pad2(parsed.min)}` })
|
|
161
|
+
rerender()
|
|
162
|
+
} else {
|
|
163
|
+
// Still forward input for external listeners if needed
|
|
164
|
+
oninput && oninput(e)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const clearValue = (e) => {
|
|
169
|
+
if (e) { e.stopPropagation(); e.preventDefault() }
|
|
170
|
+
commitValue({ holdingPen, property, onchange, valueStr: '' })
|
|
171
|
+
rerender()
|
|
172
|
+
return false
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const displayValue = holdingPen && property ? (holdingPen[property] || '') : ''
|
|
176
|
+
|
|
177
|
+
return html`
|
|
178
|
+
<div id="${wrapperId}" class="${wrapperStyle}" style="min-height: 55px; display: inline-block; width: calc(100% - 10px); margin: 40px 5px 5px 5px;">
|
|
179
|
+
<div style="display: inline-block; width: 100%; text-align: left; position: relative; padding: 0;">
|
|
180
|
+
${label ? html`<span class="${styles.label}" style="opacity: ${(holdingPen && (holdingPen[property] === 0 || holdingPen[property])) ? 1 : 0}; font-size: 16px; font-weight: normal; color: #999; margin-left: 5px; padding: 9px; background-color: rgba(255,255,255,0.8); position: absolute; top: -36px;">${label}${required ? ' *' : ''}</span>` : ''}
|
|
181
|
+
${!disableClear ? html`<div data-clear class="${styles.clear}" onclick=${clearValue}>clear</div>` : ''}
|
|
182
|
+
<input data-gramm="false" ?disabled=${disabled} style="${disabled ? 'cursor: not-allowed; opacity: 0.3;' : ''}" class="${styles.textfield} ${fieldIsTouched(holdingPen, property) === true ? styles.touched : ''}" ${required ? { required: 'required' } : ''}
|
|
183
|
+
onchange=${onTypedChange}
|
|
184
|
+
oninput=${onTypedChange}
|
|
185
|
+
placeholder="${(placeholder || 'Time') + (required ? ' *' : '')}"
|
|
186
|
+
type="text" ${pattern ? { pattern } : ''}
|
|
187
|
+
.value=${displayValue} data-input />
|
|
188
|
+
|
|
189
|
+
<div class="${styles.controls}">
|
|
190
|
+
<button type="button" class="${styles.btn}" onclick=${stepHour(1)} aria-label="Increase hour">+1h</button>
|
|
191
|
+
<button type="button" class="${styles.btn}" onclick=${stepHour(-1)} aria-label="Decrease hour">-1h</button>
|
|
192
|
+
<button type="button" class="${styles.btn}" onclick=${stepMinute(minuteStep)} aria-label="Increase minutes">+${minuteStep}m</button>
|
|
193
|
+
<button type="button" class="${styles.btn}" onclick=${stepMinute(-minuteStep)} aria-label="Decrease minutes">-${minuteStep}m</button>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
`
|
|
198
|
+
}
|
package/index.mjs
CHANGED
|
@@ -12,6 +12,8 @@ import toolbar from './components/toolbar.mjs'
|
|
|
12
12
|
import fab from './components/fab.mjs'
|
|
13
13
|
import table from './components/table.mjs'
|
|
14
14
|
import dateTimePicker from './components/dateTimePicker.mjs'
|
|
15
|
+
import datePicker from './components/datePicker.mjs'
|
|
16
|
+
import timePicker from './components/timePicker.mjs'
|
|
15
17
|
import moreVertical from './components/icons/moreVertical.mjs'
|
|
16
18
|
import close from './components/icons/close.mjs'
|
|
17
19
|
import up from './components/icons/up.mjs'
|
|
@@ -39,6 +41,8 @@ export {
|
|
|
39
41
|
fab,
|
|
40
42
|
table,
|
|
41
43
|
dateTimePicker,
|
|
44
|
+
datePicker,
|
|
45
|
+
timePicker,
|
|
42
46
|
calendarIcon,
|
|
43
47
|
timeIcon,
|
|
44
48
|
solidDown,
|