@ditojs/ui 3.2.6 → 3.3.0

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,58 +1,48 @@
1
1
  <!-- Derived from ATUI, and further extended: https://aliqin.github.io/atui/ -->
2
2
  <template lang="pug">
3
- DitoTrigger.dito-date-picker(
4
- ref="trigger"
5
- v-model:show="showPopup"
6
- trigger="focus"
7
- v-bind="{ transition, placement, disabled, target }"
3
+ DitoPickerInput.dito-date-picker(
4
+ ref="pickerInput"
5
+ v-bind="{ modelValue, show, transition, placement, target, disabled }"
6
+ :placeholder="placeholder"
7
+ :formatOptions="formatOptions"
8
+ icon="calendar"
9
+ toggleLabel="Choose date"
10
+ inputClass="dito-date-picker-input"
11
+ :inputAttributes="$attrs"
12
+ :isValueDisabled="disabledDate"
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"
13
- )
14
- DitoInput.dito-date-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="calendar"
30
- :disabled="disabled"
31
- @mousedown.prevent="onMouseDown(true)"
32
- )
33
- slot(name="suffix")
34
- template(#popup)
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 }")
35
30
  DitoCalendar.dito-date-picker-popup(
36
31
  ref="calendar"
37
- v-model="currentValue"
32
+ :modelValue="value"
38
33
  v-bind="{ locale, disabledDate }"
39
- @select="showPopup = false"
34
+ @update:modelValue="setValue"
35
+ @select="close"
40
36
  )
41
37
  </template>
42
38
 
43
39
  <script>
44
- import { format, defaultFormats } from '@ditojs/utils'
45
- import DitoTrigger from './DitoTrigger.vue'
40
+ import { defaultFormats } from '@ditojs/utils'
41
+ import DitoPickerInput from './DitoPickerInput.vue'
46
42
  import DitoCalendar from './DitoCalendar.vue'
47
- import DitoInput from './DitoInput.vue'
48
- import DitoIcon from './DitoIcon.vue'
49
- import { parseDate, getDatePartAtPosition } from '../utils/date.js'
50
- import { getSelection, setSelection } from '../utils/selection.js'
51
- import { getKeyNavigation } from '../utils/event.js'
52
- import { getTarget } from '../utils/trigger.js'
53
43
 
54
44
  export default {
55
- components: { DitoTrigger, DitoCalendar, DitoInput, DitoIcon },
45
+ components: { DitoPickerInput, DitoCalendar },
56
46
  emits: ['update:modelValue', 'update:show', 'change', 'focus', 'blur'],
57
47
  inheritAttrs: false,
58
48
 
@@ -75,125 +65,26 @@ export default {
75
65
  disabledDate: { type: Function, default: () => false }
76
66
  },
77
67
 
78
- data() {
79
- return {
80
- currentValue: this.modelValue,
81
- showPopup: this.show,
82
- inputFocused: false
83
- }
84
- },
85
-
86
68
  computed: {
87
- focused() {
88
- return this.inputFocused || this.showPopup
89
- },
90
-
91
- input() {
92
- return getTarget(this)?.querySelector('input')
93
- },
94
-
95
69
  formatOptions() {
96
70
  return {
97
71
  locale: this.locale,
98
72
  ...this.format
99
73
  }
100
- },
101
-
102
- currentText: {
103
- get() {
104
- return format(this.currentValue, this.formatOptions) || ''
105
- },
106
-
107
- set(value) {
108
- const date = parseDate(value, this.formatOptions)
109
- if (date) {
110
- this.currentValue = date
111
- }
112
- }
113
- }
114
- },
115
-
116
- watch: {
117
- modelValue(to, from) {
118
- if (+to !== +from) {
119
- this.currentValue = to
120
- }
121
- },
122
-
123
- currentValue(value) {
124
- if (+value !== +this.modelValue) {
125
- this.$emit('update:modelValue', value)
126
- this.$emit('change', value)
127
- }
128
- },
129
-
130
- show(show) {
131
- this.showPopup = show
132
- },
133
-
134
- showPopup(to, from) {
135
- if (to ^ from) {
136
- this.$emit('update:show', to)
137
- }
138
- },
139
-
140
- focused(to, from) {
141
- if (to ^ from) {
142
- this.$emit(to ? 'focus' : 'blur')
143
- }
144
74
  }
145
75
  },
146
76
 
147
77
  methods: {
148
- onFocus(focus) {
149
- this.inputFocused = focus
150
- this.showPopup = focus
151
- },
152
-
153
- onKeyDown(event) {
154
- if (event.key === 'Escape') {
155
- this.showPopup = false
156
- event.preventDefault()
157
- return
158
- }
159
- const { ver: step, enter } = getKeyNavigation(event)
160
- if (step || enter) {
161
- event.preventDefault()
162
-
163
- const position = getSelection(this.input)?.start
164
- const getDatePart = position =>
165
- getDatePartAtPosition(this.currentText, position, this.formatOptions)
166
-
167
- if (step && !this.showPopup) {
168
- this.showPopup = true
169
- } else {
170
- const { calendar } = this.$refs
171
- if (calendar) {
172
- const { name: mode, start } = getDatePart(position) || {}
173
- if (
174
- mode &&
175
- calendar.navigate({ step, enter, mode, update: true })
176
- ) {
177
- this.$nextTick(() => setSelection(this.input, getDatePart(start)))
178
- }
179
- }
180
- }
181
- }
182
- },
183
-
184
- onMouseDown(toggle) {
185
- this.showPopup = !this.disabled && (!toggle || !this.showPopup)
186
- if (this.showPopup) {
187
- this.focus()
188
- }
78
+ handlePanelKey(event, part) {
79
+ return this.$refs.calendar?.handleKey(event, part) ?? false
189
80
  },
190
81
 
191
82
  focus() {
192
- this.input.focus()
83
+ this.$refs.pickerInput.focus()
193
84
  },
194
85
 
195
86
  blur() {
196
- this.input.blur()
87
+ this.$refs.pickerInput.blur()
197
88
  }
198
89
  }
199
90
  }
@@ -204,8 +95,6 @@ export default {
204
95
 
205
96
  .dito-date-picker {
206
97
  .dito-input {
207
- font-variant-numeric: tabular-nums;
208
- width: 100%;
209
98
  min-width: 8em;
210
99
  }
211
100
  }
@@ -1,61 +1,52 @@
1
1
  <template lang="pug">
2
- .dito-date-time-picker(ref="picker")
3
- .dito-date-time-picker__inner(
4
- :class="{ 'dito-date-time-picker__inner--focus': focused }"
5
- )
6
- DitoInput.dito-date-time-picker__input(
7
- ref="input"
8
- v-model="currentText"
9
- type="text"
10
- v-bind="{ placeholder, disabled, focused, ...$attrs }"
11
- @focus="onFocus(true)"
12
- @blur="onFocus(false)"
13
- @keydown="onKeyDown"
14
- @mousedown.stop="onMouseDown(false)"
2
+ DitoPickerInput.dito-date-time-picker(
3
+ ref="pickerInput"
4
+ v-bind="{ modelValue, transition, disabled, placeholder, formatOptions }"
5
+ :placement="activePanel === 'time' ? 'bottom-right' : 'bottom-left'"
6
+ icon="calendar"
7
+ toggleLabel="Choose date and time"
8
+ inputClass="dito-date-time-picker__input"
9
+ :inputAttributes="$attrs"
10
+ :handlePanelKey="handlePanelKey"
11
+ @update:modelValue="value => $emit('update:modelValue', value)"
12
+ @change="value => $emit('change', value)"
13
+ @focus="$emit('focus')"
14
+ @blur="$emit('blur')"
15
+ @caret-part-change="onCaretPartChange"
16
+ )
17
+ template(#prefix)
18
+ slot(name="prefix")
19
+ template(#suffix)
20
+ slot(name="suffix")
21
+ template(#popup="{ value, setValue, close }")
22
+ DitoCalendar.dito-date-picker-popup(
23
+ v-if="activePanel === 'date'"
24
+ ref="calendar"
25
+ :modelValue="value"
26
+ :locale="locale"
27
+ @update:modelValue="setValue"
28
+ @select="close"
15
29
  )
16
- template(#prefix)
17
- slot(name="prefix")
18
- template(#suffix)
19
- DitoIcon(
20
- name="calendar"
21
- :disabled="disabled"
22
- @mousedown.prevent="onMouseDown(true)"
23
- )
24
- slot(name="suffix")
25
- DitoDatePicker(
26
- ref="date"
27
- v-model="currentValue"
28
- v-model:show="showDate"
29
- placement="bottom-left"
30
- :target="$refs.picker"
31
- v-bind="attributes"
30
+ DitoTimePanel(
31
+ v-else
32
+ ref="timePanel"
33
+ :modelValue="value"
34
+ @update:modelValue="setValue"
35
+ @select="close"
32
36
  )
33
- template(#trigger)
34
- //- Intentionally empty
35
- DitoTimePicker(
36
- ref="time"
37
- v-model="currentValue"
38
- v-model:show="showTime"
39
- placement="bottom-right"
40
- :target="$refs.picker"
41
- v-bind="attributes"
42
- )
43
- template(#trigger)
44
- //- Intentionally empty
45
37
  </template>
46
38
 
47
39
  <script>
48
- import { format, defaultFormats, assignDeeply } from '@ditojs/utils'
49
- import DitoInput from './DitoInput.vue'
50
- import DitoDatePicker from './DitoDatePicker.vue'
51
- import DitoTimePicker from './DitoTimePicker.vue'
52
- import DitoIcon from './DitoIcon.vue'
53
- import { parseDate } from '../utils/date.js'
54
- import { getSelection, setSelection } from '../utils/selection.js'
55
- import { getKeyNavigation } from '../utils/event.js'
56
-
40
+ import { defaultFormats, assignDeeply, isPlainObject } from '@ditojs/utils'
41
+ import DitoPickerInput from './DitoPickerInput.vue'
42
+ import DitoCalendar from './DitoCalendar.vue'
43
+ import DitoTimePanel from './DitoTimePanel.vue'
44
+ import { isTimePartName } from '../utils/time.js'
45
+
46
+ // Shows a calendar below the date and a time panel below the time, depending
47
+ // on where the caret is in the input.
57
48
  export default {
58
- components: { DitoInput, DitoDatePicker, DitoTimePicker, DitoIcon },
49
+ components: { DitoPickerInput, DitoCalendar, DitoTimePanel },
59
50
  emits: ['update:modelValue', 'change', 'focus', 'blur'],
60
51
  inheritAttrs: false,
61
52
 
@@ -70,173 +61,76 @@ export default {
70
61
 
71
62
  data() {
72
63
  return {
73
- currentValue: this.modelValue,
74
- showDate: false,
75
- showTime: false,
76
- inputFocused: false,
77
- dateFocused: false,
78
- timeFocused: false,
79
- closedMode: null
64
+ // The panel that the popup shows, 'date' or 'time'.
65
+ activePanel: 'date'
80
66
  }
81
67
  },
82
68
 
83
69
  computed: {
84
- attributes() {
85
- const { transition, disabled, locale, formatOptions: format } = this
86
- return { transition, disabled, locale, format }
87
- },
88
-
89
- focused() {
90
- return this.inputFocused || this.showDate || this.showTime
91
- },
92
-
93
- input() {
94
- return this.$refs.input.input
95
- },
96
-
97
70
  formatOptions() {
98
- return assignDeeply(
71
+ const options = assignDeeply(
99
72
  {
100
73
  locale: this.locale,
101
74
  time: defaultFormats.time,
102
75
  date: defaultFormats.date
103
76
  },
104
- {
105
- date: {
106
- month: 'short',
107
- format: (value, type, options) =>
108
- type === 'literal' && /\bat\b/.test(value)
109
- ? ', '
110
- : this.format?.date?.format?.(value, type, options) ?? value
111
- }
112
- },
77
+ { date: { month: 'short' } },
113
78
  this.format
114
79
  )
115
- },
116
-
117
- timeIndex() {
118
- const text = this.currentText
119
- if (text) {
120
- const time = text.match(/([\S]+\s*)$/)?.[1]
121
- if (time) {
122
- return text.length - time.length
123
- }
124
- }
125
- return null
126
- },
127
-
128
- currentText: {
129
- get() {
130
- return format(this.currentValue, this.formatOptions) || ''
131
- },
132
-
133
- set(value) {
134
- const date = parseDate(value, this.formatOptions)
135
- if (date) {
136
- const selection = getSelection(this.input)
137
- this.currentValue = date
138
- this.$nextTick(() => setSelection(this.input, selection))
139
- }
140
- }
141
- }
142
- },
143
-
144
- watch: {
145
- modelValue(to, from) {
146
- if (+to !== +from) {
147
- this.currentValue = to
148
- }
149
- },
150
-
151
- currentValue(value) {
152
- if (+value !== +this.modelValue) {
153
- this.$emit('update:modelValue', value)
154
- this.$emit('change', value)
155
- }
156
- },
157
-
158
- focused(to, from) {
159
- if (to ^ from) {
160
- this.$emit(to ? 'focus' : 'blur')
80
+ // Wrap the merged date `format()`, which may be a custom one, to
81
+ // separate the date and time with a comma rather than "at". `date` can
82
+ // also be `true` for the default format, or `false` to omit the date.
83
+ if (isPlainObject(options.date)) {
84
+ const { format } = options.date
85
+ options.date.format = (value, type, formatOptions) =>
86
+ type === 'literal' && /\bat\b/.test(value)
87
+ ? ', '
88
+ : format?.(value, type, formatOptions) ?? value
161
89
  }
90
+ return options
162
91
  }
163
92
  },
164
93
 
165
94
  methods: {
166
- onFocus(focus) {
167
- this.inputFocused = focus
168
- if (focus) {
169
- this.updatePopups()
170
- } else {
171
- this.showDate = false
172
- this.showTime = false
173
- }
174
- },
175
-
176
- onMouseDown(toggle) {
177
- if (toggle && (this.showDate || this.showTime)) {
178
- this.showDate = false
179
- this.showTime = false
180
- } else if (!this.disabled) {
181
- this.focus()
182
- requestAnimationFrame(() => this.updatePopups())
183
- }
95
+ getPanelForPart(name) {
96
+ return isTimePartName(name) ? 'time' : 'date'
184
97
  },
185
98
 
186
- onKeyDown(event) {
187
- const mode = this.getMode(event)
188
- this.$refs[mode].onKeyDown(event)
189
- if (event.defaultPrevented) {
190
- this.closedMode = mode
191
- } else if (mode !== this.closedMode) {
192
- this.updatePopups(mode)
99
+ onCaretPartChange(name) {
100
+ const panel = this.getPanelForPart(name)
101
+ if (panel !== this.activePanel) {
102
+ this.activePanel = panel
103
+ // Show the other panel, also if the popup was closed for this one.
104
+ this.$refs.pickerInput.open()
193
105
  }
194
106
  },
195
107
 
196
- getMode(event = null) {
197
- const { start } = getSelection(this.input)
198
- const { hor: step } = getKeyNavigation(event)
199
- return this.timeIndex === null || start + step < this.timeIndex
200
- ? 'date'
201
- : 'time'
202
- },
203
-
204
- updatePopups(mode = this.getMode()) {
205
- if (mode) {
206
- this.closedMode = null
108
+ handlePanelKey(event, part) {
109
+ // Only the shown panel handles keys, see `onCaretPartChange()` for how
110
+ // it follows the caret.
111
+ if (this.getPanelForPart(part?.name) !== this.activePanel) {
112
+ return false
207
113
  }
208
- this.showDate = mode === 'date'
209
- this.showTime = mode === 'time'
114
+ const { calendar, timePanel } = this.$refs
115
+ const panel = this.activePanel === 'time' ? timePanel : calendar
116
+ return panel?.handleKey(event, part) ?? false
210
117
  },
211
118
 
212
119
  focus() {
213
- this.input.focus()
120
+ this.$refs.pickerInput.focus()
214
121
  },
215
122
 
216
123
  blur() {
217
- this.input.blur()
124
+ this.$refs.pickerInput.blur()
218
125
  }
219
126
  }
220
127
  }
221
128
  </script>
222
129
 
223
130
  <style lang="scss">
224
- @import '../styles/_imports';
225
-
226
131
  .dito-date-time-picker {
227
- &__inner {
228
- display: flex;
229
- }
230
-
231
- &__input {
232
- flex: 1;
233
- font-variant-numeric: tabular-nums;
132
+ .dito-input {
234
133
  min-width: 12em;
235
134
  }
236
-
237
- .dito-date-picker,
238
- .dito-time-picker {
239
- position: absolute;
240
- }
241
135
  }
242
136
  </style>
@@ -17,6 +17,7 @@
17
17
  :placeholder="placeholder"
18
18
  :autocomplete="autocomplete"
19
19
  :aria-label="title"
20
+ :size="size"
20
21
  v-bind="attributes"
21
22
  @mousedown="onMouseDown"
22
23
  )
@@ -59,7 +60,8 @@ export default {
59
60
  },
60
61
 
61
62
  attributes() {
62
- const { class: _, ...attributes } = this.$attrs
63
+ // `size` is bound separately, see the `size` computed property.
64
+ const { class: _class, size: _size, ...attributes } = this.$attrs
63
65
  return attributes
64
66
  },
65
67
 
@@ -68,10 +70,10 @@ export default {
68
70
  },
69
71
 
70
72
  size() {
71
- // Determine size based on min & max settings, if they're provided.
73
+ // Unless a size is provided, fit the longest of the min & max settings.
72
74
  const { size, min, max } = this.$attrs
73
75
  const getLength = value => (value != null ? `${value}`.length : 0)
74
- return size || getLength(min) || getLength(max) || undefined
76
+ return size || Math.max(getLength(min), getLength(max)) || undefined
75
77
  }
76
78
  },
77
79