@ditojs/ui 3.2.6 → 3.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.
@@ -1,106 +1,49 @@
1
1
  <!-- Derived from ATUI, and further extended: https://aliqin.github.io/atui/ -->
2
2
  <template lang="pug">
3
- DitoTrigger.dito-time-picker(
4
- ref="trigger"
5
- v-model:show="showPopup"
6
- trigger="focus"
7
- v-bind="{ transition, placement, disabled, target }"
3
+ DitoPickerInput.dito-time-picker(
4
+ ref="pickerInput"
5
+ v-bind="{ modelValue, show, transition, placement, target, disabled }"
6
+ :placeholder="placeholder"
7
+ :formatOptions="formatOptions"
8
+ icon="clock"
9
+ toggleLabel="Choose time"
10
+ inputClass="dito-time-picker-input"
11
+ :inputAttributes="$attrs"
12
+ :isValueDisabled="isValueDisabled"
13
+ :handlePanelKey="handlePanelKey"
14
+ @update:modelValue="value => $emit('update:modelValue', value)"
15
+ @update:show="show => $emit('update:show', show)"
16
+ @change="value => $emit('change', value)"
17
+ @focus="$emit('focus')"
18
+ @blur="$emit('blur')"
8
19
  )
9
- template(#trigger)
10
- slot(
11
- v-if="$slots.trigger"
12
- name="trigger"
20
+ template(
21
+ v-if="$slots.trigger"
22
+ #trigger
23
+ )
24
+ slot(name="trigger")
25
+ template(#prefix)
26
+ slot(name="prefix")
27
+ template(#suffix)
28
+ slot(name="suffix")
29
+ template(#popup="{ value, setValue, close }")
30
+ DitoTimePanel(
31
+ ref="timePanel"
32
+ :modelValue="value"
33
+ v-bind="{ disabledHour, disabledMinute, disabledSecond }"
34
+ @update:modelValue="setValue"
35
+ @select="close"
13
36
  )
14
- DitoInput.dito-time-picker-input(
15
- v-else
16
- ref="input"
17
- v-model="currentText"
18
- type="text"
19
- v-bind="{ placeholder, disabled, focused, ...$attrs }"
20
- @focus="onFocus(true)"
21
- @blur="onFocus(false)"
22
- @keydown="onKeyDown"
23
- @mousedown.stop="onMouseDown(false)"
24
- )
25
- template(#prefix)
26
- slot(name="prefix")
27
- template(#suffix)
28
- DitoIcon(
29
- name="clock"
30
- :disabled="disabled"
31
- @mousedown.prevent="onMouseDown(true)"
32
- )
33
- slot(name="suffix")
34
- template(#popup)
35
- .dito-time-picker-popup(
36
- role="dialog"
37
- aria-label="Choose time"
38
- )
39
- .dito-time-picker-panel
40
- ul.dito-time-picker-hour(
41
- ref="hour"
42
- role="listbox"
43
- aria-label="Hour"
44
- )
45
- template(
46
- v-for="index in 24"
47
- )
48
- li(
49
- v-if="!disabledHour(index - 1)"
50
- role="option"
51
- :aria-label="leftPad(index - 1)"
52
- :aria-selected="hour === index - 1"
53
- :class="{ selected: hour === index - 1 }"
54
- @click="hour = index - 1"
55
- ) {{ leftPad(index - 1) }}
56
- .dito-time-picker-panel
57
- ul.dito-time-picker-minute(
58
- ref="minute"
59
- role="listbox"
60
- aria-label="Minute"
61
- )
62
- template(
63
- v-for="index in 60"
64
- )
65
- li(
66
- v-if="!disabledMinute(index - 1)"
67
- role="option"
68
- :aria-label="leftPad(index - 1)"
69
- :aria-selected="minute === index - 1"
70
- :class="{ selected: minute === index - 1 }"
71
- @click="minute = index - 1"
72
- ) {{ leftPad(index - 1) }}
73
- .dito-time-picker-panel
74
- ul.dito-time-picker-second(
75
- ref="second"
76
- role="listbox"
77
- aria-label="Second"
78
- )
79
- template(
80
- v-for="index in 60"
81
- )
82
- li(
83
- v-if="!disabledSecond(index - 1)"
84
- role="option"
85
- :aria-label="leftPad(index - 1)"
86
- :aria-selected="second === index - 1"
87
- :class="{ selected: second === index - 1 }"
88
- @click="second = index - 1"
89
- ) {{ leftPad(index - 1) }}
90
37
  </template>
91
38
 
92
39
  <script>
93
- import { format, defaultFormats } from '@ditojs/utils'
94
- import DitoTrigger from './DitoTrigger.vue'
95
- import DitoInput from './DitoInput.vue'
96
- import DitoIcon from './DitoIcon.vue'
97
- import { alterDate, parseDate, getDatePartAtPosition } from '../utils/date.js'
98
- import { getSelection, setSelection } from '../utils/selection.js'
99
- import { getKeyNavigation } from '../utils/event.js'
100
- import { getTarget } from '../utils/trigger.js'
40
+ import { defaultFormats } from '@ditojs/utils'
41
+ import DitoPickerInput from './DitoPickerInput.vue'
42
+ import DitoTimePanel from './DitoTimePanel.vue'
43
+ import { isTimeDisabled } from '../utils/time.js'
101
44
 
102
45
  export default {
103
- components: { DitoTrigger, DitoInput, DitoIcon },
46
+ components: { DitoPickerInput, DitoTimePanel },
104
47
  emits: ['update:modelValue', 'update:show', 'change', 'focus', 'blur'],
105
48
  inheritAttrs: false,
106
49
 
@@ -125,394 +68,44 @@ export default {
125
68
  disabledSecond: { type: Function, default: () => false }
126
69
  },
127
70
 
128
- data() {
129
- return {
130
- currentValue: this.modelValue,
131
- showPopup: this.show,
132
- inputFocused: false,
133
- ignoreNextChange: false
134
- }
135
- },
136
-
137
71
  computed: {
138
- focused() {
139
- return this.inputFocused || this.showPopup
140
- },
141
-
142
- input() {
143
- return getTarget(this)?.querySelector('input')
144
- },
145
-
146
72
  formatOptions() {
147
73
  return {
148
74
  locale: this.locale,
149
75
  ...this.format
150
76
  }
151
- },
152
-
153
- currentText: {
154
- get() {
155
- return format(this.currentValue, this.formatOptions) || ''
156
- },
157
-
158
- set(value) {
159
- const date = parseDate(value, this.formatOptions)
160
- if (date) {
161
- this.currentValue = date
162
- }
163
- }
164
- },
165
-
166
- currentDate() {
167
- return (
168
- this.currentValue ||
169
- // Create a new Date() object with the time set to 0, to be used when
170
- // first setting any of the times, for meaningful dates in case the
171
- // object is shared with a DatePicker, e.g. through DateTimePicker.
172
- alterDate(new Date(), { hour: 0, minute: 0, second: 0, millisecond: 0 })
173
- )
174
- },
175
-
176
- hour: {
177
- get() {
178
- return this.currentDate.getHours()
179
- },
180
-
181
- set(hour) {
182
- this.setTime({ hour })
183
- }
184
- },
185
-
186
- minute: {
187
- get() {
188
- return this.currentDate.getMinutes()
189
- },
190
-
191
- set(minute) {
192
- this.setTime({ minute })
193
- }
194
- },
195
-
196
- second: {
197
- get() {
198
- return this.currentDate.getSeconds()
199
- },
200
-
201
- set(second) {
202
- this.setTime({ second })
203
- }
204
- }
205
- },
206
-
207
- watch: {
208
- modelValue(to, from) {
209
- if (+to !== +from) {
210
- this.currentValue = to
211
- this.scrollAll(true)
212
- }
213
- },
214
-
215
- currentValue(value) {
216
- if (+value !== +this.modelValue) {
217
- this.$emit('update:modelValue', value)
218
- this.$emit('change', value)
219
- this.scrollAll(true)
220
- }
221
- },
222
-
223
- currentText() {
224
- const { input } = this
225
- const selection = getSelection(input)
226
- if (
227
- this.focused &&
228
- !this.ignoreNextChange &&
229
- selection.start === selection.end
230
- ) {
231
- this.$nextTick(() => setSelection(input, selection))
232
- }
233
- this.ignoreNextChange = false
234
- },
235
-
236
- show(show) {
237
- this.showPopup = show
238
- },
239
-
240
- showPopup(to, from) {
241
- if (to) {
242
- this.scrollAll(false)
243
- }
244
- if (to ^ from) {
245
- this.$emit('update:show', to)
246
- }
247
- },
248
-
249
- focused(to, from) {
250
- if (to ^ from) {
251
- this.$emit(to ? 'focus' : 'blur')
252
- }
253
77
  }
254
78
  },
255
79
 
256
80
  methods: {
257
- leftPad(value) {
258
- return ('0' + value).slice(-2)
259
- },
260
-
261
- setTime(overrides) {
262
- this.currentValue = alterDate(this.currentDate, {
263
- ...overrides,
264
- millisecond: 0
265
- })
266
- },
267
-
268
- scrollAll(smooth) {
269
- const scroll = (ref, value) => {
270
- const target = this.$refs[ref]
271
- if (target) {
272
- const lineHeight = (
273
- target.scrollHeight /
274
- // First and last one add 3 times the margin.
275
- (target.childElementCount + 6)
276
- )
277
- const top = Math.round(value * lineHeight)
278
- const distance = Math.abs(target.scrollTop - top)
279
- target.scrollTo({
280
- top,
281
- behavior: smooth && distance < 100 ? 'smooth' : 'auto'
282
- })
283
- }
284
- }
285
- this.$nextTick(() => {
286
- scroll('hour', this.hour)
287
- scroll('minute', this.minute)
288
- scroll('second', this.second)
81
+ isValueDisabled(date) {
82
+ const { disabledHour, disabledMinute, disabledSecond } = this
83
+ return isTimeDisabled(date, {
84
+ disabledHour,
85
+ disabledMinute,
86
+ disabledSecond
289
87
  })
290
88
  },
291
89
 
292
- onFocus(focus) {
293
- this.inputFocused = focus
294
- this.showPopup = focus
295
- },
296
-
297
- onKeyDown(event) {
298
- if (event.key === 'Escape') {
299
- this.showPopup = false
300
- event.preventDefault()
301
- return
302
- }
303
- const { input } = this
304
- const selection = getSelection(input)
305
- const { ver: step, enter } = getKeyNavigation(event)
306
- if (step || enter) {
307
- event.preventDefault()
308
-
309
- const getDatePart = position =>
310
- getDatePartAtPosition(this.currentText, position, this.formatOptions)
311
-
312
- if (step) {
313
- if (!this.showPopup) {
314
- this.showPopup = true
315
- } else {
316
- const { name, start } = getDatePart(selection.start) || {}
317
- if (name) {
318
- const value = this[name] + step
319
- const count = this.$refs[name].childElementCount
320
- this[name] =
321
- value < 0
322
- ? value + count
323
- : value >= count
324
- ? value - count
325
- : value
326
- this.ignoreNextChange = true
327
- this.$nextTick(() => setSelection(input, getDatePart(start)))
328
- }
329
- }
330
- } else if (enter) {
331
- this.showPopup = false
332
- }
333
- } else if (selection.start === selection.end) {
334
- const { value } = input
335
- let pos = selection.start
336
-
337
- const isDigit = char => !isNaN(Number(char))
338
-
339
- if (event.key.length === 1) {
340
- if (isDigit(event.key) && pos < value.length) {
341
- if (
342
- value[pos] === ':' &&
343
- isDigit(value[pos - 1]) &&
344
- isDigit(value[pos - 2])
345
- ) {
346
- pos++
347
- }
348
- // Remove next digit so the event overrides it instead of inserting
349
- // new chars.
350
- if (
351
- isDigit(value[pos]) && (
352
- isDigit(value[pos + 1]) ||
353
- isDigit(value[pos - 1])
354
- )
355
- ) {
356
- input.value = (
357
- value.slice(0, pos) +
358
- value.slice(pos + 1)
359
- )
360
- setSelection(input, { start: pos, end: pos })
361
- }
362
- } else {
363
- const length = format(this.currentValue, this.formatOptions).length
364
- if (value.length === length) {
365
- event.preventDefault()
366
- } else if (value.length > length) {
367
- input.value = value.slice(0, length)
368
- setSelection(input, selection)
369
- event.preventDefault()
370
- }
371
- }
372
- } else if (event.key === 'Backspace') {
373
- if (
374
- pos === 1 ||
375
- [' ', ':'].includes(value[pos - 2])
376
- ) {
377
- if (
378
- pos === value.length ||
379
- value[pos] === ':'
380
- ) {
381
- pos--
382
- input.value = (
383
- value.slice(0, pos) +
384
- ((value[pos - 1] ?? '') + '00') +
385
- value.slice(pos + 1)
386
- )
387
- setSelection(input, { start: pos, end: pos })
388
- } else if (
389
- isDigit(value[pos]) &&
390
- !isDigit(+value[pos + 1])
391
- ) {
392
- // When removing the first of two digits, replace with 0
393
- input.value = value.slice(0, pos) + '0' + value.slice(pos)
394
- setSelection(input, { start: pos, end: pos })
395
- }
396
- }
397
- }
398
- }
399
- },
400
-
401
- onMouseDown(toggle) {
402
- this.showPopup = !this.disabled && (!toggle || !this.showPopup)
403
- if (this.showPopup) {
404
- this.focus()
405
- }
90
+ handlePanelKey(event, part) {
91
+ return this.$refs.timePanel?.handleKey(event, part) ?? false
406
92
  },
407
93
 
408
94
  focus() {
409
- this.input.focus()
95
+ this.$refs.pickerInput.focus()
410
96
  },
411
97
 
412
98
  blur() {
413
- this.input.blur()
99
+ this.$refs.pickerInput.blur()
414
100
  }
415
101
  }
416
102
  }
417
103
  </script>
418
104
 
419
105
  <style lang="scss">
420
- @import '../styles/_imports';
421
-
422
106
  .dito-time-picker {
423
107
  .dito-input {
424
- width: 100%;
425
108
  min-width: 6em;
426
109
  }
427
110
  }
428
-
429
- .dito-time-picker-popup {
430
- max-width: 160px;
431
- margin: $popup-margin;
432
- }
433
-
434
- $time-picker-line-height: 24px;
435
-
436
- .dito-time-picker {
437
- .dito-input {
438
- font-variant-numeric: tabular-nums;
439
- width: 100%;
440
- }
441
- }
442
-
443
- .dito-time-picker-popup {
444
- list-style: none;
445
- background: $color-white;
446
- border: $border-style;
447
- border-radius: $border-radius;
448
- box-shadow: $shadow-window;
449
- overflow: hidden;
450
- }
451
-
452
- .dito-time-picker-popup .dito-time-picker-panel {
453
- float: left;
454
- border: $border-style;
455
- border-width: 0 1px 0;
456
- margin-left: -1px;
457
- box-sizing: border-box;
458
- width: calc(100% / 3 + 1px);
459
- overflow: hidden;
460
-
461
- &:last-child {
462
- border-right: 0;
463
- }
464
-
465
- ul {
466
- overflow-x: hidden;
467
- overflow-y: auto;
468
- list-style: none;
469
- width: 100%;
470
- margin: 0;
471
- // Hide scrollbar:
472
- box-sizing: content-box;
473
- padding: 0 17px 0 0;
474
- height: 7 * $time-picker-line-height;
475
-
476
- & > li {
477
- box-sizing: content-box;
478
- background: $color-white;
479
- width: 100%;
480
- height: $time-picker-line-height;
481
- line-height: $time-picker-line-height;
482
- text-align: center;
483
- font-variant-numeric: tabular-nums;
484
- cursor: pointer;
485
- white-space: nowrap;
486
- overflow: hidden;
487
- @include user-select(none);
488
-
489
- &:first-child {
490
- margin-top: 3 * $time-picker-line-height;
491
- }
492
-
493
- &:last-child {
494
- margin-bottom: 3 * $time-picker-line-height;
495
- }
496
-
497
- &:hover {
498
- background: $color-highlight;
499
- }
500
-
501
- &.selected,
502
- &.selected:hover {
503
- color: $color-text-inverted;
504
- background: $color-active;
505
- }
506
-
507
- &.disabled {
508
- cursor: default;
509
- color: $color-disabled;
510
- }
511
-
512
- &.disabled:hover {
513
- background: transparent;
514
- }
515
- }
516
- }
517
- }
518
111
  </style>