@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.
- 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 +250 -89
- package/src/components/index.js +1 -0
- 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,7 +334,7 @@ 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
|
|
@@ -318,82 +452,109 @@ export default {
|
|
|
318
452
|
|
|
319
453
|
addFocusEvents(parent) {
|
|
320
454
|
const targets = parent.querySelectorAll('input, textarea')
|
|
321
|
-
|
|
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)
|
|
322
486
|
|
|
323
487
|
return combineEvents(
|
|
324
488
|
addEvents(targets, {
|
|
325
489
|
focus: () => {
|
|
326
|
-
this.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
blur: () => {
|
|
331
|
-
// Use timeout to allow clicked inputs to grab focus
|
|
332
|
-
this.blurTimer = setTimeout(() => {
|
|
333
|
-
if (!this.isPopupFocused()) {
|
|
334
|
-
this.showPopup = false
|
|
335
|
-
}
|
|
336
|
-
}, 0)
|
|
490
|
+
if (!this.isFocusingTriggerInput) {
|
|
491
|
+
this.open()
|
|
492
|
+
}
|
|
337
493
|
}
|
|
338
494
|
}),
|
|
339
495
|
|
|
340
496
|
addEvents(parent, {
|
|
341
497
|
mousedown: event => {
|
|
342
|
-
|
|
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) {
|
|
343
506
|
event.preventDefault()
|
|
344
507
|
event.stopPropagation()
|
|
345
508
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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()
|
|
351
515
|
if (input && !input.hasAttribute('readonly')) {
|
|
352
516
|
input.setAttribute('readonly', 'true')
|
|
353
|
-
|
|
354
|
-
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 })
|
|
355
521
|
}
|
|
356
522
|
}
|
|
357
|
-
}
|
|
523
|
+
}
|
|
524
|
+
}),
|
|
358
525
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
// moved elsewhere in the meantime, e.g. by clicking outside.
|
|
367
|
-
if (
|
|
368
|
-
!event.target.matches('input, textarea, button') &&
|
|
369
|
-
this.isPopupFocused()
|
|
370
|
-
) {
|
|
371
|
-
input.focus()
|
|
372
|
-
}
|
|
373
|
-
input = null
|
|
374
|
-
}, 0)
|
|
375
|
-
}
|
|
526
|
+
{
|
|
527
|
+
remove() {
|
|
528
|
+
parent.removeEventListener('blur', onBlur, true)
|
|
529
|
+
releaseEvents?.remove()
|
|
530
|
+
releaseEvents = null
|
|
531
|
+
lockedInput?.removeAttribute('readonly')
|
|
532
|
+
lockedInput = null
|
|
376
533
|
}
|
|
377
|
-
}
|
|
534
|
+
}
|
|
378
535
|
)
|
|
379
536
|
},
|
|
380
537
|
|
|
381
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
|
|
382
541
|
if (show) {
|
|
383
542
|
if (this.trigger === 'focus') {
|
|
543
|
+
this.popupEvents?.remove()
|
|
384
544
|
this.popupEvents = this.addFocusEvents(this.$refs.popup)
|
|
385
545
|
}
|
|
386
|
-
this.
|
|
546
|
+
this.addCloseEvents()
|
|
547
|
+
this.observeTargetSize()
|
|
387
548
|
} else {
|
|
388
549
|
this.popupEvents?.remove()
|
|
389
550
|
this.popupEvents = null
|
|
551
|
+
this.removeCloseEvents()
|
|
552
|
+
this.unobserveTargetSize()
|
|
390
553
|
}
|
|
391
554
|
},
|
|
392
555
|
|
|
393
556
|
onClick() {
|
|
394
|
-
|
|
395
|
-
this.showPopup = true
|
|
396
|
-
}
|
|
557
|
+
this.toggle()
|
|
397
558
|
},
|
|
398
559
|
|
|
399
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'
|
|
@@ -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,
|
package/src/utils/event.js
CHANGED
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
import { asArray } from '@ditojs/utils'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// Adds the `events` handlers to all `targets`, and returns an object with a
|
|
4
|
+
// `remove()` method that removes them again. When a `signal` is passed, the
|
|
5
|
+
// handlers are also removed when it aborts, and not added at all when it
|
|
6
|
+
// already has.
|
|
7
|
+
export function addEvents(targets, events, { signal } = {}) {
|
|
4
8
|
targets =
|
|
5
9
|
targets instanceof NodeList
|
|
6
10
|
? Array.from(targets)
|
|
7
11
|
: asArray(targets)
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
const controller = new AbortController()
|
|
14
|
+
if (!signal?.aborted) {
|
|
15
|
+
// Remove the handlers along with `signal`, through a listener that is
|
|
16
|
+
// itself removed when the handlers are removed first:
|
|
17
|
+
signal?.addEventListener('abort', () => controller.abort(), {
|
|
18
|
+
once: true,
|
|
19
|
+
signal: controller.signal
|
|
20
|
+
})
|
|
21
|
+
for (const [type, handler] of Object.entries(events)) {
|
|
22
|
+
for (const target of targets) {
|
|
23
|
+
target.addEventListener(type, handler, { signal: controller.signal })
|
|
24
|
+
}
|
|
12
25
|
}
|
|
13
26
|
}
|
|
14
27
|
|
|
15
28
|
return {
|
|
16
29
|
remove() {
|
|
17
|
-
|
|
18
|
-
for (const target of targets) {
|
|
19
|
-
target.removeEventListener(type, handler, false)
|
|
20
|
-
}
|
|
21
|
-
}
|
|
30
|
+
controller.abort()
|
|
22
31
|
}
|
|
23
32
|
}
|
|
24
33
|
}
|
package/src/utils/index.js
CHANGED