@ditojs/ui 3.2.0 → 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.
- package/dist/dito-ui.css +1 -1
- package/dist/dito-ui.es.js +1350 -3294
- package/dist/dito-ui.umd.js +1 -7
- package/package.json +15 -9
- package/src/components/DitoButton.vue +91 -16
- package/src/components/DitoCalendar.vue +318 -248
- package/src/components/DitoDatePicker.vue +36 -147
- package/src/components/DitoDateTimePicker.vue +75 -181
- package/src/components/DitoInput.vue +5 -3
- package/src/components/DitoMenuButton.vue +226 -0
- package/src/components/DitoPagination.vue +9 -3
- package/src/components/DitoPickerInput.vue +285 -0
- package/src/components/DitoSwitch.vue +4 -6
- package/src/components/DitoTimePanel.vue +264 -0
- package/src/components/DitoTimePicker.vue +47 -454
- package/src/components/DitoTrigger.vue +293 -97
- package/src/components/index.js +1 -0
- package/src/styles/components/_button.scss +7 -9
- package/src/styles/components/_resize.scss +8 -0
- package/src/styles/mixins/_input.scss +6 -0
- package/src/utils/button.js +24 -0
- package/src/utils/date.js +18 -0
- package/src/utils/event.js +18 -9
- package/src/utils/index.js +1 -0
- package/src/utils/time.js +107 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<!-- Derived from ATUI, and further extended: https://aliqin.github.io/atui/ -->
|
|
2
2
|
<template lang="pug">
|
|
3
|
-
.dito-trigger-container
|
|
3
|
+
.dito-trigger-container(
|
|
4
|
+
@keydown="onKeyDown"
|
|
5
|
+
)
|
|
4
6
|
.dito-trigger(
|
|
5
7
|
v-if="alwaysShow"
|
|
6
8
|
ref="trigger"
|
|
@@ -28,7 +30,10 @@
|
|
|
28
30
|
:class="triggerClass"
|
|
29
31
|
)
|
|
30
32
|
slot(name="trigger")
|
|
31
|
-
Transition(
|
|
33
|
+
Transition(
|
|
34
|
+
:name="transition ? `dito-${transition}` : undefined"
|
|
35
|
+
:css="!!transition"
|
|
36
|
+
)
|
|
32
37
|
.dito-popup(
|
|
33
38
|
v-if="trigger === 'hover'"
|
|
34
39
|
v-show="showPopup"
|
|
@@ -60,11 +65,20 @@ import { hyphenate } from '@ditojs/utils'
|
|
|
60
65
|
import { addEvents, combineEvents } from '../utils/event.js'
|
|
61
66
|
import { getTarget } from '../utils/trigger'
|
|
62
67
|
|
|
68
|
+
function isFocusableControl(element) {
|
|
69
|
+
return !!element.matches?.('input, textarea, select, button')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isEditableControl(element) {
|
|
73
|
+
return !!element.matches?.('input, textarea, select')
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
export default {
|
|
64
77
|
emits: ['update:show'],
|
|
65
78
|
|
|
66
79
|
props: {
|
|
67
80
|
trigger: { type: String, default: 'click' },
|
|
81
|
+
// The name of the transition of the popup, or `null` to show it at once.
|
|
68
82
|
transition: { type: String, default: 'slide' },
|
|
69
83
|
placement: { type: String, default: 'bottom' },
|
|
70
84
|
show: { type: Boolean, default: false },
|
|
@@ -74,6 +88,8 @@ export default {
|
|
|
74
88
|
zIndex: { type: Number, default: 0 },
|
|
75
89
|
keepInView: { type: Boolean, default: true },
|
|
76
90
|
hideWhenClickOutside: { type: Boolean, default: true },
|
|
91
|
+
// Whether the popup's first child takes on the width of the target.
|
|
92
|
+
matchTargetWidth: { type: Boolean, default: true },
|
|
77
93
|
alwaysShow: { type: Boolean, default: false },
|
|
78
94
|
cover: { type: Boolean, default: false },
|
|
79
95
|
hideDelay: { type: Number, default: 0 }
|
|
@@ -82,12 +98,11 @@ export default {
|
|
|
82
98
|
data() {
|
|
83
99
|
return {
|
|
84
100
|
showPopup: this.show,
|
|
85
|
-
popupPlacement: this.placement,
|
|
86
101
|
focusEvents: null,
|
|
87
102
|
closeEvents: null,
|
|
88
103
|
popupEvents: null,
|
|
89
|
-
|
|
90
|
-
|
|
104
|
+
mouseLeaveTimer: null,
|
|
105
|
+
targetResizeObserver: null
|
|
91
106
|
}
|
|
92
107
|
},
|
|
93
108
|
|
|
@@ -110,10 +125,6 @@ export default {
|
|
|
110
125
|
|
|
111
126
|
popupStyle() {
|
|
112
127
|
return this.zIndex ? `z-index: ${this.zIndex}` : ''
|
|
113
|
-
},
|
|
114
|
-
|
|
115
|
-
triggerTarget() {
|
|
116
|
-
return getTarget(this)
|
|
117
128
|
}
|
|
118
129
|
},
|
|
119
130
|
|
|
@@ -124,72 +135,195 @@ export default {
|
|
|
124
135
|
|
|
125
136
|
showPopup(to, from) {
|
|
126
137
|
if (to ^ from) {
|
|
138
|
+
if (!to && this.trigger === 'focus' && this.isPopupFocused()) {
|
|
139
|
+
// Don't lose the focus with the popup, e.g. after selecting a date in
|
|
140
|
+
// a calendar with the keyboard.
|
|
141
|
+
this.focusTriggerInput()
|
|
142
|
+
}
|
|
127
143
|
this.$emit('update:show', to)
|
|
128
144
|
this.$nextTick(() => this.onShowPopup(to))
|
|
129
145
|
}
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
target() {
|
|
149
|
+
if (this.$refs.trigger) {
|
|
150
|
+
this.addTargetEvents()
|
|
151
|
+
if (this.targetResizeObserver) {
|
|
152
|
+
this.unobserveTargetSize()
|
|
153
|
+
this.observeTargetSize()
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
placement() {
|
|
159
|
+
this.$nextTick(() => this.updatePosition())
|
|
130
160
|
}
|
|
131
161
|
},
|
|
132
162
|
|
|
133
163
|
mounted() {
|
|
134
|
-
|
|
135
|
-
if (this.trigger === 'focus') {
|
|
136
|
-
this.focusEvents = this.addFocusEvents(this.triggerTarget ?? trigger)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (this.hideWhenClickOutside && !this.alwaysShow) {
|
|
140
|
-
// Use 'mouseup' instead of 'click', since click appears to happen after
|
|
141
|
-
// the DOM inside of popups could change in a way so that the check
|
|
142
|
-
// `popup.contains(event.target)` would fail:
|
|
143
|
-
this.closeEvents = addEvents(window, {
|
|
144
|
-
mouseup: event => {
|
|
145
|
-
if (
|
|
146
|
-
this.showPopup &&
|
|
147
|
-
!popup.contains(event.target) &&
|
|
148
|
-
!trigger.contains(event.target) &&
|
|
149
|
-
!this.triggerTarget?.contains(event.target)
|
|
150
|
-
) {
|
|
151
|
-
this.showPopup = false
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
|
|
155
|
-
blur: () => {
|
|
156
|
-
this.showPopup = false
|
|
157
|
-
}
|
|
158
|
-
})
|
|
159
|
-
}
|
|
164
|
+
this.addTargetEvents()
|
|
160
165
|
|
|
161
|
-
if (this.
|
|
166
|
+
if (this.showPopup) {
|
|
167
|
+
// The `showPopup` watcher doesn't see popups that are shown initially.
|
|
168
|
+
this.onShowPopup(true)
|
|
169
|
+
} else if (this.alwaysShow) {
|
|
162
170
|
this.showPopup = true
|
|
163
171
|
}
|
|
164
172
|
},
|
|
165
173
|
|
|
166
174
|
created() {
|
|
167
175
|
this.showPopup = this.show
|
|
176
|
+
// Whether `focusTriggerInput()` is moving the focus, see there.
|
|
177
|
+
this.isFocusingTriggerInput = false
|
|
168
178
|
},
|
|
169
179
|
|
|
170
180
|
unmounted() {
|
|
171
181
|
this.focusEvents?.remove()
|
|
172
|
-
this.
|
|
182
|
+
this.removeCloseEvents()
|
|
173
183
|
this.popupEvents?.remove()
|
|
184
|
+
this.unobserveTargetSize()
|
|
185
|
+
clearTimeout(this.mouseLeaveTimer)
|
|
174
186
|
},
|
|
175
187
|
|
|
176
188
|
methods: {
|
|
189
|
+
open() {
|
|
190
|
+
if (!this.disabled) {
|
|
191
|
+
this.showPopup = true
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
close() {
|
|
196
|
+
if (!this.alwaysShow) {
|
|
197
|
+
this.showPopup = false
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
toggle() {
|
|
202
|
+
if (this.showPopup) {
|
|
203
|
+
this.close()
|
|
204
|
+
} else {
|
|
205
|
+
this.open()
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
// Returns the element that the popup is positioned at, resolving a `target`
|
|
210
|
+
// given by ref name, which is only possible once mounted. This is why it
|
|
211
|
+
// can't be a computed property: `$refs` aren't reactive.
|
|
212
|
+
getTargetElement() {
|
|
213
|
+
return getTarget(this) ?? this.$refs.trigger
|
|
214
|
+
},
|
|
215
|
+
|
|
177
216
|
isPopupFocused() {
|
|
178
|
-
return !!this.$refs.popup?.
|
|
217
|
+
return !!this.$refs.popup?.contains(document.activeElement)
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
// Returns whether `element` is part of the trigger, its target or the
|
|
221
|
+
// popup.
|
|
222
|
+
containsElement(element) {
|
|
223
|
+
return (
|
|
224
|
+
!!element && (
|
|
225
|
+
this.$el.contains(element) ||
|
|
226
|
+
this.getTargetElement().contains(element)
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
getTriggerInput() {
|
|
232
|
+
return this.getTargetElement()?.querySelector('input, textarea') ?? null
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
focusTriggerInput() {
|
|
236
|
+
// The input's focus event would open the popup again, see
|
|
237
|
+
// `addFocusEvents()`. Focus events are dispatched synchronously.
|
|
238
|
+
this.isFocusingTriggerInput = true
|
|
239
|
+
try {
|
|
240
|
+
this.getTriggerInput()?.focus()
|
|
241
|
+
} finally {
|
|
242
|
+
this.isFocusingTriggerInput = false
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
addTargetEvents() {
|
|
247
|
+
this.focusEvents?.remove()
|
|
248
|
+
this.focusEvents =
|
|
249
|
+
this.trigger === 'focus'
|
|
250
|
+
? this.addFocusEvents(this.getTargetElement())
|
|
251
|
+
: null
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
// Adds the events that close the popup while it is shown, see
|
|
255
|
+
// `onShowPopup()`.
|
|
256
|
+
addCloseEvents() {
|
|
257
|
+
if (this.hideWhenClickOutside && !this.alwaysShow && !this.closeEvents) {
|
|
258
|
+
const { trigger, popup } = this.$refs
|
|
259
|
+
// Use 'mouseup' instead of 'click', since click appears to happen after
|
|
260
|
+
// the DOM inside of popups could change in a way so that the check
|
|
261
|
+
// `popup.contains(event.target)` would fail:
|
|
262
|
+
this.closeEvents = addEvents(window, {
|
|
263
|
+
mouseup: event => {
|
|
264
|
+
if (
|
|
265
|
+
this.showPopup &&
|
|
266
|
+
!popup.contains(event.target) &&
|
|
267
|
+
!trigger.contains(event.target) &&
|
|
268
|
+
!this.getTargetElement().contains(event.target)
|
|
269
|
+
) {
|
|
270
|
+
this.showPopup = false
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
blur: () => {
|
|
275
|
+
this.showPopup = false
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
removeCloseEvents() {
|
|
282
|
+
this.closeEvents?.remove()
|
|
283
|
+
this.closeEvents = null
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
onKeyDown(event) {
|
|
287
|
+
if (event.key === 'Escape' && this.showPopup && !this.alwaysShow) {
|
|
288
|
+
// Don't let Escape also close what contains the trigger, e.g. dialogs.
|
|
289
|
+
event.stopPropagation()
|
|
290
|
+
this.close()
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
// Positions the popup whenever the size of the target changes, which
|
|
295
|
+
// includes the moment it is laid out, also if the popup is shown inside a
|
|
296
|
+
// hidden part of the page that only becomes visible later. The target is
|
|
297
|
+
// observed rather than the popup, since `updatePosition()` resizes the
|
|
298
|
+
// popup, which would trigger the observer again.
|
|
299
|
+
observeTargetSize() {
|
|
300
|
+
if (!this.targetResizeObserver) {
|
|
301
|
+
this.targetResizeObserver = new ResizeObserver(
|
|
302
|
+
() => this.updatePosition()
|
|
303
|
+
)
|
|
304
|
+
this.targetResizeObserver.observe(this.getTargetElement())
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
unobserveTargetSize() {
|
|
309
|
+
this.targetResizeObserver?.disconnect()
|
|
310
|
+
this.targetResizeObserver = null
|
|
179
311
|
},
|
|
180
312
|
|
|
181
313
|
updatePosition() {
|
|
182
314
|
const { trigger, popup } = this.$refs
|
|
183
|
-
if (this.
|
|
184
|
-
|
|
315
|
+
if (!popup || !this.showPopup || popup.offsetWidth === 0) {
|
|
316
|
+
// Unmounted, hidden, or not laid out yet, see `observeTargetSize()`.
|
|
185
317
|
return
|
|
186
318
|
}
|
|
187
319
|
|
|
188
|
-
const target = this.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
320
|
+
const target = this.getTargetElement()
|
|
321
|
+
if (this.matchTargetWidth) {
|
|
322
|
+
// Actually resize the popup's first child, so it can set size limits.
|
|
323
|
+
const el = this.target === 'popup' ? trigger : popup.firstElementChild
|
|
324
|
+
if (el) {
|
|
325
|
+
el.style.width = getComputedStyle(target).width
|
|
326
|
+
}
|
|
193
327
|
}
|
|
194
328
|
|
|
195
329
|
const bounds = target.getBoundingClientRect()
|
|
@@ -200,42 +334,77 @@ export default {
|
|
|
200
334
|
const popupWidth = popup.offsetWidth
|
|
201
335
|
const popupHeight = popup.offsetHeight
|
|
202
336
|
|
|
203
|
-
let [part1, part2] = this.
|
|
337
|
+
let [part1, part2] = this.placement.split('-') || []
|
|
204
338
|
if (this.keepInView) {
|
|
205
339
|
const winWidth = window.innerWidth
|
|
206
340
|
const winHeight = window.innerHeight
|
|
341
|
+
const roomAbove = triggerTop
|
|
342
|
+
const roomBelow = winHeight - triggerTop - triggerHeight
|
|
343
|
+
const roomLeft = triggerLeft
|
|
344
|
+
const roomRight = winWidth - triggerLeft - triggerWidth
|
|
345
|
+
// Flips the popup to the other side if it doesn't fit, but only if
|
|
346
|
+
// there's more room there, so it doesn't move further out of view.
|
|
347
|
+
const shouldFlip = (room, otherRoom, size) => (
|
|
348
|
+
room < size && otherRoom > room
|
|
349
|
+
)
|
|
350
|
+
|
|
207
351
|
if (part1 === 'top') {
|
|
208
|
-
if (
|
|
352
|
+
if (shouldFlip(roomAbove, roomBelow, popupHeight)) {
|
|
209
353
|
part1 = 'bottom'
|
|
210
354
|
}
|
|
211
355
|
} else if (part1 === 'bottom') {
|
|
212
|
-
if (
|
|
356
|
+
if (shouldFlip(roomBelow, roomAbove, popupHeight)) {
|
|
213
357
|
part1 = 'top'
|
|
214
358
|
}
|
|
215
359
|
} else if (part1 === 'left') {
|
|
216
|
-
if (
|
|
360
|
+
if (shouldFlip(roomLeft, roomRight, popupWidth)) {
|
|
217
361
|
part1 = 'right'
|
|
218
362
|
}
|
|
219
363
|
} else if (part1 === 'right') {
|
|
220
|
-
if (
|
|
364
|
+
if (shouldFlip(roomRight, roomLeft, popupWidth)) {
|
|
221
365
|
part1 = 'left'
|
|
222
366
|
}
|
|
223
367
|
}
|
|
224
368
|
|
|
369
|
+
// The alignments of the popup's edges with the trigger's edges.
|
|
225
370
|
if (part2 === 'top') {
|
|
226
|
-
if (
|
|
371
|
+
if (
|
|
372
|
+
shouldFlip(
|
|
373
|
+
winHeight - triggerTop,
|
|
374
|
+
triggerTop + triggerHeight,
|
|
375
|
+
popupHeight
|
|
376
|
+
)
|
|
377
|
+
) {
|
|
227
378
|
part2 = 'bottom'
|
|
228
379
|
}
|
|
229
380
|
} else if (part2 === 'bottom') {
|
|
230
|
-
if (
|
|
381
|
+
if (
|
|
382
|
+
shouldFlip(
|
|
383
|
+
triggerTop + triggerHeight,
|
|
384
|
+
winHeight - triggerTop,
|
|
385
|
+
popupHeight
|
|
386
|
+
)
|
|
387
|
+
) {
|
|
231
388
|
part2 = 'top'
|
|
232
389
|
}
|
|
233
390
|
} else if (part2 === 'left') {
|
|
234
|
-
if (
|
|
391
|
+
if (
|
|
392
|
+
shouldFlip(
|
|
393
|
+
winWidth - triggerLeft,
|
|
394
|
+
triggerLeft + triggerWidth,
|
|
395
|
+
popupWidth
|
|
396
|
+
)
|
|
397
|
+
) {
|
|
235
398
|
part2 = 'right'
|
|
236
399
|
}
|
|
237
400
|
} else if (part2 === 'right') {
|
|
238
|
-
if (
|
|
401
|
+
if (
|
|
402
|
+
shouldFlip(
|
|
403
|
+
triggerLeft + triggerWidth,
|
|
404
|
+
winWidth - triggerLeft,
|
|
405
|
+
popupWidth
|
|
406
|
+
)
|
|
407
|
+
) {
|
|
239
408
|
part2 = 'left'
|
|
240
409
|
}
|
|
241
410
|
}
|
|
@@ -283,82 +452,109 @@ export default {
|
|
|
283
452
|
|
|
284
453
|
addFocusEvents(parent) {
|
|
285
454
|
const targets = parent.querySelectorAll('input, textarea')
|
|
286
|
-
|
|
455
|
+
// The trigger's input while it is marked readonly, see `mousedown`, and
|
|
456
|
+
// the events that release it again.
|
|
457
|
+
let lockedInput = null
|
|
458
|
+
let releaseEvents = null
|
|
459
|
+
|
|
460
|
+
const releaseInput = event => {
|
|
461
|
+
const input = lockedInput
|
|
462
|
+
lockedInput = null
|
|
463
|
+
releaseEvents.remove()
|
|
464
|
+
releaseEvents = null
|
|
465
|
+
// Give some time for other events to update input before it becomes
|
|
466
|
+
// editable and still focused again.
|
|
467
|
+
setTimeout(() => {
|
|
468
|
+
input.removeAttribute('readonly')
|
|
469
|
+
// Only bring the focus back from the popup, not if the focus moved
|
|
470
|
+
// elsewhere in the meantime, e.g. by clicking outside.
|
|
471
|
+
if (!isFocusableControl(event.target) && this.isPopupFocused()) {
|
|
472
|
+
input.focus()
|
|
473
|
+
}
|
|
474
|
+
}, 0)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Only close once the focus leaves the trigger, its target and the
|
|
478
|
+
// popup, e.g. when tabbing out, but not when it moves between them, e.g.
|
|
479
|
+
// when tabbing into the popup. Blur events don't bubble, so capture them.
|
|
480
|
+
const onBlur = event => {
|
|
481
|
+
if (!this.containsElement(event.relatedTarget)) {
|
|
482
|
+
this.close()
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
parent.addEventListener('blur', onBlur, true)
|
|
287
486
|
|
|
288
487
|
return combineEvents(
|
|
289
488
|
addEvents(targets, {
|
|
290
489
|
focus: () => {
|
|
291
|
-
this.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
blur: () => {
|
|
296
|
-
// Use timeout to allow clicked inputs to grab focus
|
|
297
|
-
this.blurTimer = setTimeout(() => {
|
|
298
|
-
if (!this.isPopupFocused()) {
|
|
299
|
-
this.showPopup = false
|
|
300
|
-
}
|
|
301
|
-
}, 0)
|
|
490
|
+
if (!this.isFocusingTriggerInput) {
|
|
491
|
+
this.open()
|
|
492
|
+
}
|
|
302
493
|
}
|
|
303
494
|
}),
|
|
304
495
|
|
|
305
496
|
addEvents(parent, {
|
|
306
497
|
mousedown: event => {
|
|
307
|
-
|
|
498
|
+
const { target } = event
|
|
499
|
+
const isInPopup = !!this.$refs.popup?.contains(target)
|
|
500
|
+
// Controls take the focus, except for the buttons in the popup, so
|
|
501
|
+
// that the focus stays in the trigger's input when using the mouse.
|
|
502
|
+
const takesFocus = isInPopup
|
|
503
|
+
? isEditableControl(target)
|
|
504
|
+
: isFocusableControl(target)
|
|
505
|
+
if (!takesFocus) {
|
|
308
506
|
event.preventDefault()
|
|
309
507
|
event.stopPropagation()
|
|
310
508
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
509
|
+
// Mark the trigger's input as readonly so it can't lose focus
|
|
510
|
+
// while the user does other mouse-activities in the popup, except
|
|
511
|
+
// for the controls in the popup that take the focus themselves,
|
|
512
|
+
// e.g. the fields of a color picker.
|
|
513
|
+
if (!lockedInput && !(isInPopup && takesFocus)) {
|
|
514
|
+
const input = this.getTriggerInput()
|
|
316
515
|
if (input && !input.hasAttribute('readonly')) {
|
|
317
516
|
input.setAttribute('readonly', 'true')
|
|
318
|
-
|
|
319
|
-
input
|
|
517
|
+
lockedInput = input
|
|
518
|
+
// Release the input wherever the mouse is released, also
|
|
519
|
+
// outside of `parent`, e.g. after dragging in a color picker.
|
|
520
|
+
releaseEvents = addEvents(window, { mouseup: releaseInput })
|
|
320
521
|
}
|
|
321
522
|
}
|
|
322
|
-
}
|
|
523
|
+
}
|
|
524
|
+
}),
|
|
323
525
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
// moved elsewhere in the meantime, e.g. by clicking outside.
|
|
332
|
-
if (
|
|
333
|
-
!event.target.matches('input, textarea, button') &&
|
|
334
|
-
this.isPopupFocused()
|
|
335
|
-
) {
|
|
336
|
-
input.focus()
|
|
337
|
-
}
|
|
338
|
-
input = null
|
|
339
|
-
}, 0)
|
|
340
|
-
}
|
|
526
|
+
{
|
|
527
|
+
remove() {
|
|
528
|
+
parent.removeEventListener('blur', onBlur, true)
|
|
529
|
+
releaseEvents?.remove()
|
|
530
|
+
releaseEvents = null
|
|
531
|
+
lockedInput?.removeAttribute('readonly')
|
|
532
|
+
lockedInput = null
|
|
341
533
|
}
|
|
342
|
-
}
|
|
534
|
+
}
|
|
343
535
|
)
|
|
344
536
|
},
|
|
345
537
|
|
|
346
538
|
onShowPopup(show) {
|
|
539
|
+
// The popup may have been toggled again or unmounted in the meantime:
|
|
540
|
+
if (show !== this.showPopup || !this.$refs.popup) return
|
|
347
541
|
if (show) {
|
|
348
542
|
if (this.trigger === 'focus') {
|
|
543
|
+
this.popupEvents?.remove()
|
|
349
544
|
this.popupEvents = this.addFocusEvents(this.$refs.popup)
|
|
350
545
|
}
|
|
351
|
-
this.
|
|
546
|
+
this.addCloseEvents()
|
|
547
|
+
this.observeTargetSize()
|
|
352
548
|
} else {
|
|
353
549
|
this.popupEvents?.remove()
|
|
354
550
|
this.popupEvents = null
|
|
551
|
+
this.removeCloseEvents()
|
|
552
|
+
this.unobserveTargetSize()
|
|
355
553
|
}
|
|
356
554
|
},
|
|
357
555
|
|
|
358
556
|
onClick() {
|
|
359
|
-
|
|
360
|
-
this.showPopup = true
|
|
361
|
-
}
|
|
557
|
+
this.toggle()
|
|
362
558
|
},
|
|
363
559
|
|
|
364
560
|
onHover(enter) {
|
package/src/components/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export { default as DitoDatePicker } from './DitoDatePicker.vue'
|
|
|
4
4
|
export { default as DitoDateTimePicker } from './DitoDateTimePicker.vue'
|
|
5
5
|
export { default as DitoIcon } from './DitoIcon.vue'
|
|
6
6
|
export { default as DitoInput } from './DitoInput.vue'
|
|
7
|
+
export { default as DitoMenuButton } from './DitoMenuButton.vue'
|
|
7
8
|
export { default as DitoPagination } from './DitoPagination.vue'
|
|
8
9
|
export { default as DitoSwitch } from './DitoSwitch.vue'
|
|
9
10
|
export { default as DitoTimePicker } from './DitoTimePicker.vue'
|
|
@@ -83,6 +83,13 @@
|
|
|
83
83
|
&[disabled] {
|
|
84
84
|
@extend %button-disabled;
|
|
85
85
|
}
|
|
86
|
+
|
|
87
|
+
.dito-buttons--round & {
|
|
88
|
+
padding: $button-round-padding;
|
|
89
|
+
min-width: $input-height;
|
|
90
|
+
min-height: $input-height;
|
|
91
|
+
border-radius: 1em;
|
|
92
|
+
}
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
.dito-buttons {
|
|
@@ -101,15 +108,6 @@
|
|
|
101
108
|
margin-left: 1.5em;
|
|
102
109
|
}
|
|
103
110
|
|
|
104
|
-
&--round {
|
|
105
|
-
.dito-button {
|
|
106
|
-
padding: $button-round-padding;
|
|
107
|
-
min-width: $input-height;
|
|
108
|
-
min-height: $input-height;
|
|
109
|
-
border-radius: 1em;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
111
|
&--small {
|
|
114
112
|
--button-margin: 1px;
|
|
115
113
|
}
|
|
@@ -10,4 +10,12 @@
|
|
|
10
10
|
height: 1em;
|
|
11
11
|
z-index: 10;
|
|
12
12
|
cursor: ns-resize;
|
|
13
|
+
// Let pointer drags resize instead of panning the page or selecting text:
|
|
14
|
+
touch-action: none;
|
|
15
|
+
user-select: none;
|
|
16
|
+
|
|
17
|
+
&:focus-visible {
|
|
18
|
+
outline: none;
|
|
19
|
+
box-shadow: $shadow-focus;
|
|
20
|
+
}
|
|
13
21
|
}
|
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
&.dito-input--focus {
|
|
13
13
|
border-color: $color-active;
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
// Keep showing errors while the field has focus, e.g. the first invalid
|
|
17
|
+
// field, which is focused after a failed submit:
|
|
18
|
+
.dito-container--has-errors &:is(:focus, :focus-within, .dito-input--focus) {
|
|
19
|
+
border-color: $color-error;
|
|
20
|
+
}
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
%input-borderless {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { labelize } from '@ditojs/utils'
|
|
2
|
+
|
|
3
|
+
// Returns the attributes of buttons that perform `verb`, e.g. 'delete': the
|
|
4
|
+
// modifier class that styles the verb, and the label of buttons that don't
|
|
5
|
+
// display text, named by the verb and by its `subject` if known, e.g.
|
|
6
|
+
// 'Add Section'. `hasText` states whether the button displays text, which it
|
|
7
|
+
// does by default when `text` is set.
|
|
8
|
+
export function getVerbButtonAttributes({
|
|
9
|
+
verb,
|
|
10
|
+
subject = null,
|
|
11
|
+
text = null,
|
|
12
|
+
hasText = !!text
|
|
13
|
+
}) {
|
|
14
|
+
const label = hasText
|
|
15
|
+
? null
|
|
16
|
+
: `${labelize(verb)}${subject ? ` ${subject}` : ''}`
|
|
17
|
+
return {
|
|
18
|
+
class: `dito-button--${verb}`,
|
|
19
|
+
...(label && {
|
|
20
|
+
'title': label,
|
|
21
|
+
'aria-label': label
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/utils/date.js
CHANGED
|
@@ -14,6 +14,24 @@ export function alterDate(date, overrides = {}) {
|
|
|
14
14
|
return new Date(...Object.values({ ...describeDate(date), ...overrides }))
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// Returns `date` with its part `partName`, e.g. 'month', stepped by `step`.
|
|
18
|
+
// Stepping the year or the month clamps the day to the length of the new
|
|
19
|
+
// month, so that e.g. January 31 plus a month is February 29, not March 2.
|
|
20
|
+
export function stepDate(date, partName, step) {
|
|
21
|
+
const parts = describeDate(date)
|
|
22
|
+
parts[partName] += step
|
|
23
|
+
if (partName === 'year' || partName === 'month') {
|
|
24
|
+
parts.day = Math.min(parts.day, getDaysInMonth(parts.year, parts.month))
|
|
25
|
+
}
|
|
26
|
+
return alterDate(date, parts)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getDaysInMonth(year, month) {
|
|
30
|
+
// Day 0 of the next month is the last day of `month`, and `Date` rolls
|
|
31
|
+
// `month` over into the next or previous year if it is out of range.
|
|
32
|
+
return new Date(year, month + 1, 0).getDate()
|
|
33
|
+
}
|
|
34
|
+
|
|
17
35
|
export function parseDate(string, {
|
|
18
36
|
locale = 'en-US',
|
|
19
37
|
date = true,
|