hotwire_combobox 0.1.11 → 0.1.13
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.
- checksums.yaml +4 -4
- data/MIT-LICENSE +1 -1
- data/app/assets/javascripts/controllers/hw_combobox_controller.js +49 -242
- data/app/assets/javascripts/helpers.js +52 -0
- data/app/assets/javascripts/models/combobox/actors.js +24 -0
- data/app/assets/javascripts/models/combobox/async_loading.js +7 -0
- data/app/assets/javascripts/models/combobox/autocomplete.js +39 -0
- data/app/assets/javascripts/models/combobox/base.js +3 -0
- data/app/assets/javascripts/models/combobox/dialog.js +50 -0
- data/app/assets/javascripts/models/combobox/filtering.js +57 -0
- data/app/assets/javascripts/models/combobox/navigation.js +39 -0
- data/app/assets/javascripts/models/combobox/options.js +41 -0
- data/app/assets/javascripts/models/combobox/selection.js +62 -0
- data/app/assets/javascripts/models/combobox/toggle.js +104 -0
- data/app/assets/javascripts/models/combobox/validity.js +34 -0
- data/app/assets/javascripts/models/combobox.js +14 -0
- data/app/assets/javascripts/vendor/bodyScrollLock.js +299 -0
- data/app/assets/stylesheets/hotwire_combobox.css +181 -0
- data/app/helpers/hotwire_combobox/helper.rb +81 -26
- data/app/presenters/hotwire_combobox/component.rb +150 -20
- data/app/presenters/hotwire_combobox/listbox/option.rb +4 -2
- data/app/views/hotwire_combobox/_combobox.html.erb +4 -13
- data/app/views/hotwire_combobox/_next_page.turbo_stream.erb +5 -0
- data/app/views/hotwire_combobox/_paginated_options.turbo_stream.erb +7 -0
- data/app/views/hotwire_combobox/_pagination.html.erb +3 -0
- data/app/views/hotwire_combobox/combobox/_dialog.html.erb +7 -0
- data/app/views/hotwire_combobox/combobox/_hidden_field.html.erb +4 -0
- data/app/views/hotwire_combobox/combobox/_input.html.erb +2 -0
- data/app/views/hotwire_combobox/combobox/_paginated_listbox.html.erb +11 -0
- data/lib/hotwire_combobox/version.rb +1 -1
- data/lib/hotwire_combobox.rb +4 -2
- metadata +52 -2
@@ -0,0 +1,62 @@
|
|
1
|
+
import Combobox from "models/combobox/base"
|
2
|
+
import { wrapAroundAccess } from "helpers"
|
3
|
+
|
4
|
+
Combobox.Selection = Base => class extends Base {
|
5
|
+
selectOption(event) {
|
6
|
+
this._select(event.currentTarget)
|
7
|
+
this.close()
|
8
|
+
}
|
9
|
+
|
10
|
+
_connectSelection() {
|
11
|
+
if (this.hiddenFieldTarget.value) {
|
12
|
+
this._selectOptionByValue(this.hiddenFieldTarget.value)
|
13
|
+
}
|
14
|
+
}
|
15
|
+
|
16
|
+
_select(option, { force = false } = {}) {
|
17
|
+
this._resetOptions()
|
18
|
+
|
19
|
+
if (option) {
|
20
|
+
if (this.hasSelectedClass) option.classList.add(this.selectedClass)
|
21
|
+
|
22
|
+
this._markValid()
|
23
|
+
this._maybeAutocompleteWith(option, { force })
|
24
|
+
this._commitSelection(option, { selected: true })
|
25
|
+
} else {
|
26
|
+
this._markInvalid()
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
_commitSelection(option, { selected }) {
|
31
|
+
option?.setAttribute("aria-selected", selected)
|
32
|
+
option?.scrollIntoView({ block: "nearest" })
|
33
|
+
|
34
|
+
if (selected) {
|
35
|
+
this.hiddenFieldTarget.value = option?.dataset.value
|
36
|
+
} else {
|
37
|
+
this.hiddenFieldTarget.value = null
|
38
|
+
}
|
39
|
+
}
|
40
|
+
|
41
|
+
_deselect() {
|
42
|
+
const option = this._selectedOptionElement
|
43
|
+
|
44
|
+
if (this.hasSelectedClass) option?.classList.remove(this.selectedClass)
|
45
|
+
this._commitSelection(option, { selected: false })
|
46
|
+
}
|
47
|
+
|
48
|
+
_selectNew(query) {
|
49
|
+
this._resetOptions()
|
50
|
+
this.hiddenFieldTarget.value = query
|
51
|
+
this.hiddenFieldTarget.name = this.nameWhenNewValue
|
52
|
+
}
|
53
|
+
|
54
|
+
_selectIndex(index) {
|
55
|
+
const option = wrapAroundAccess(this._visibleOptionElements, index)
|
56
|
+
this._select(option, { force: true })
|
57
|
+
}
|
58
|
+
|
59
|
+
_selectOptionByValue(value) {
|
60
|
+
this._allOptions.find(option => option.dataset.value === value)?.click()
|
61
|
+
}
|
62
|
+
}
|
@@ -0,0 +1,104 @@
|
|
1
|
+
import Combobox from "models/combobox/base"
|
2
|
+
import { disableBodyScroll, enableBodyScroll } from "vendor/bodyScrollLock"
|
3
|
+
|
4
|
+
Combobox.Toggle = Base => class extends Base {
|
5
|
+
open() {
|
6
|
+
this.expandedValue = true
|
7
|
+
}
|
8
|
+
|
9
|
+
close() {
|
10
|
+
if (this._isOpen) {
|
11
|
+
this._ensureSelection()
|
12
|
+
this.expandedValue = false
|
13
|
+
}
|
14
|
+
}
|
15
|
+
|
16
|
+
toggle() {
|
17
|
+
if (this.expandedValue) {
|
18
|
+
this.close()
|
19
|
+
} else {
|
20
|
+
this._openByFocusing()
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
closeOnClickOutside({ target }) {
|
25
|
+
if (this.element.contains(target) && !this._isDialogDismisser(target)) return
|
26
|
+
|
27
|
+
this.close()
|
28
|
+
}
|
29
|
+
|
30
|
+
closeOnFocusOutside({ target }) {
|
31
|
+
if (!this._isOpen) return
|
32
|
+
if (this.element.contains(target)) return
|
33
|
+
if (target.matches("main")) return
|
34
|
+
|
35
|
+
this.close()
|
36
|
+
}
|
37
|
+
|
38
|
+
_ensureSelection() {
|
39
|
+
if (!this._isValidNewOption(this._actingCombobox.value, { ignoreAutocomplete: true })) {
|
40
|
+
this._select(this._selectedOptionElement, { force: true })
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
_openByFocusing() {
|
45
|
+
this._actingCombobox.focus()
|
46
|
+
}
|
47
|
+
|
48
|
+
_isDialogDismisser(target) {
|
49
|
+
return target.closest("dialog") && target.role != "combobox"
|
50
|
+
}
|
51
|
+
|
52
|
+
async _expand() {
|
53
|
+
if (this._autocompletesList && this._smallViewport) {
|
54
|
+
this._openInDialog()
|
55
|
+
} else {
|
56
|
+
this._openInline()
|
57
|
+
}
|
58
|
+
|
59
|
+
this._actingCombobox.setAttribute("aria-expanded", true) // needs to happen after setting acting combobox
|
60
|
+
}
|
61
|
+
|
62
|
+
_collapse() {
|
63
|
+
this._actingCombobox.setAttribute("aria-expanded", false) // needs to happen before resetting acting combobox
|
64
|
+
|
65
|
+
if (this.dialogTarget.open) {
|
66
|
+
this._closeInDialog()
|
67
|
+
} else {
|
68
|
+
this._closeInline()
|
69
|
+
}
|
70
|
+
}
|
71
|
+
|
72
|
+
_openInDialog() {
|
73
|
+
this._moveArtifactsToDialog()
|
74
|
+
this._preventFocusingComboboxAfterClosingDialog()
|
75
|
+
this._preventBodyScroll()
|
76
|
+
this.dialogTarget.showModal()
|
77
|
+
}
|
78
|
+
|
79
|
+
_openInline() {
|
80
|
+
this.listboxTarget.hidden = false
|
81
|
+
}
|
82
|
+
|
83
|
+
_closeInDialog() {
|
84
|
+
this._moveArtifactsInline()
|
85
|
+
this.dialogTarget.close()
|
86
|
+
this._restoreBodyScroll()
|
87
|
+
}
|
88
|
+
|
89
|
+
_closeInline() {
|
90
|
+
this.listboxTarget.hidden = true
|
91
|
+
}
|
92
|
+
|
93
|
+
_preventBodyScroll() {
|
94
|
+
disableBodyScroll(this.dialogListboxTarget)
|
95
|
+
}
|
96
|
+
|
97
|
+
_restoreBodyScroll() {
|
98
|
+
enableBodyScroll(this.dialogListboxTarget)
|
99
|
+
}
|
100
|
+
|
101
|
+
get _isOpen() {
|
102
|
+
return this.expandedValue
|
103
|
+
}
|
104
|
+
}
|
@@ -0,0 +1,34 @@
|
|
1
|
+
import Combobox from "models/combobox/base"
|
2
|
+
|
3
|
+
Combobox.Validity = Base => class extends Base {
|
4
|
+
_markValid() {
|
5
|
+
if (this._valueIsInvalid) return
|
6
|
+
|
7
|
+
if (this.hasInvalidClass) {
|
8
|
+
this.comboboxTarget.classList.remove(this.invalidClass)
|
9
|
+
}
|
10
|
+
|
11
|
+
this.comboboxTarget.removeAttribute("aria-invalid")
|
12
|
+
this.comboboxTarget.removeAttribute("aria-errormessage")
|
13
|
+
}
|
14
|
+
|
15
|
+
_markInvalid() {
|
16
|
+
if (this._valueIsValid) return
|
17
|
+
|
18
|
+
if (this.hasInvalidClass) {
|
19
|
+
this.comboboxTarget.classList.add(this.invalidClass)
|
20
|
+
}
|
21
|
+
|
22
|
+
this.comboboxTarget.setAttribute("aria-invalid", true)
|
23
|
+
this.comboboxTarget.setAttribute("aria-errormessage", `Please select a valid option for ${this.comboboxTarget.name}`)
|
24
|
+
}
|
25
|
+
|
26
|
+
get _valueIsValid() {
|
27
|
+
return !this._valueIsInvalid
|
28
|
+
}
|
29
|
+
|
30
|
+
get _valueIsInvalid() {
|
31
|
+
const isRequiredAndEmpty = this.comboboxTarget.required && !this.hiddenFieldTarget.value
|
32
|
+
return isRequiredAndEmpty
|
33
|
+
}
|
34
|
+
}
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import Combobox from "models/combobox/base"
|
2
|
+
|
3
|
+
import "models/combobox/actors"
|
4
|
+
import "models/combobox/async_loading"
|
5
|
+
import "models/combobox/autocomplete"
|
6
|
+
import "models/combobox/dialog"
|
7
|
+
import "models/combobox/filtering"
|
8
|
+
import "models/combobox/navigation"
|
9
|
+
import "models/combobox/options"
|
10
|
+
import "models/combobox/selection"
|
11
|
+
import "models/combobox/toggle"
|
12
|
+
import "models/combobox/validity"
|
13
|
+
|
14
|
+
export default Combobox
|
@@ -0,0 +1,299 @@
|
|
1
|
+
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
|
2
|
+
|
3
|
+
// Older browsers don't support event options, feature detect it.
|
4
|
+
|
5
|
+
// Adopted and modified solution from Bohdan Didukh (2017)
|
6
|
+
// https://stackoverflow.com/questions/41594997/ios-10-safari-prevent-scrolling-behind-a-fixed-overlay-and-maintain-scroll-posi
|
7
|
+
|
8
|
+
var hasPassiveEvents = false;
|
9
|
+
if (typeof window !== 'undefined') {
|
10
|
+
var passiveTestOptions = {
|
11
|
+
get passive() {
|
12
|
+
hasPassiveEvents = true;
|
13
|
+
return undefined;
|
14
|
+
}
|
15
|
+
};
|
16
|
+
window.addEventListener('testPassive', null, passiveTestOptions);
|
17
|
+
window.removeEventListener('testPassive', null, passiveTestOptions);
|
18
|
+
}
|
19
|
+
|
20
|
+
var isIosDevice = typeof window !== 'undefined' && window.navigator && window.navigator.platform && (/iP(ad|hone|od)/.test(window.navigator.platform) || window.navigator.platform === 'MacIntel' && window.navigator.maxTouchPoints > 1);
|
21
|
+
|
22
|
+
|
23
|
+
var locks = [];
|
24
|
+
var documentListenerAdded = false;
|
25
|
+
var initialClientY = -1;
|
26
|
+
var previousBodyOverflowSetting = void 0;
|
27
|
+
var previousBodyPosition = void 0;
|
28
|
+
var previousBodyPaddingRight = void 0;
|
29
|
+
|
30
|
+
// returns true if `el` should be allowed to receive touchmove events.
|
31
|
+
var allowTouchMove = function allowTouchMove(el) {
|
32
|
+
return locks.some(function (lock) {
|
33
|
+
if (lock.options.allowTouchMove && lock.options.allowTouchMove(el)) {
|
34
|
+
return true;
|
35
|
+
}
|
36
|
+
|
37
|
+
return false;
|
38
|
+
});
|
39
|
+
};
|
40
|
+
|
41
|
+
var preventDefault = function preventDefault(rawEvent) {
|
42
|
+
var e = rawEvent || window.event;
|
43
|
+
|
44
|
+
// For the case whereby consumers adds a touchmove event listener to document.
|
45
|
+
// Recall that we do document.addEventListener('touchmove', preventDefault, { passive: false })
|
46
|
+
// in disableBodyScroll - so if we provide this opportunity to allowTouchMove, then
|
47
|
+
// the touchmove event on document will break.
|
48
|
+
if (allowTouchMove(e.target)) {
|
49
|
+
return true;
|
50
|
+
}
|
51
|
+
|
52
|
+
// Do not prevent if the event has more than one touch (usually meaning this is a multi touch gesture like pinch to zoom).
|
53
|
+
if (e.touches.length > 1) return true;
|
54
|
+
|
55
|
+
if (e.preventDefault) e.preventDefault();
|
56
|
+
|
57
|
+
return false;
|
58
|
+
};
|
59
|
+
|
60
|
+
var setOverflowHidden = function setOverflowHidden(options) {
|
61
|
+
// If previousBodyPaddingRight is already set, don't set it again.
|
62
|
+
if (previousBodyPaddingRight === undefined) {
|
63
|
+
var _reserveScrollBarGap = !!options && options.reserveScrollBarGap === true;
|
64
|
+
var scrollBarGap = window.innerWidth - document.documentElement.clientWidth;
|
65
|
+
|
66
|
+
if (_reserveScrollBarGap && scrollBarGap > 0) {
|
67
|
+
var computedBodyPaddingRight = parseInt(window.getComputedStyle(document.body).getPropertyValue('padding-right'), 10);
|
68
|
+
previousBodyPaddingRight = document.body.style.paddingRight;
|
69
|
+
document.body.style.paddingRight = computedBodyPaddingRight + scrollBarGap + 'px';
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
// If previousBodyOverflowSetting is already set, don't set it again.
|
74
|
+
if (previousBodyOverflowSetting === undefined) {
|
75
|
+
previousBodyOverflowSetting = document.body.style.overflow;
|
76
|
+
document.body.style.overflow = 'hidden';
|
77
|
+
}
|
78
|
+
};
|
79
|
+
|
80
|
+
var restoreOverflowSetting = function restoreOverflowSetting() {
|
81
|
+
if (previousBodyPaddingRight !== undefined) {
|
82
|
+
document.body.style.paddingRight = previousBodyPaddingRight;
|
83
|
+
|
84
|
+
// Restore previousBodyPaddingRight to undefined so setOverflowHidden knows it
|
85
|
+
// can be set again.
|
86
|
+
previousBodyPaddingRight = undefined;
|
87
|
+
}
|
88
|
+
|
89
|
+
if (previousBodyOverflowSetting !== undefined) {
|
90
|
+
document.body.style.overflow = previousBodyOverflowSetting;
|
91
|
+
|
92
|
+
// Restore previousBodyOverflowSetting to undefined
|
93
|
+
// so setOverflowHidden knows it can be set again.
|
94
|
+
previousBodyOverflowSetting = undefined;
|
95
|
+
}
|
96
|
+
};
|
97
|
+
|
98
|
+
var setPositionFixed = function setPositionFixed() {
|
99
|
+
return window.requestAnimationFrame(function () {
|
100
|
+
// If previousBodyPosition is already set, don't set it again.
|
101
|
+
if (previousBodyPosition === undefined) {
|
102
|
+
previousBodyPosition = {
|
103
|
+
position: document.body.style.position,
|
104
|
+
top: document.body.style.top,
|
105
|
+
left: document.body.style.left
|
106
|
+
};
|
107
|
+
|
108
|
+
// Update the dom inside an animation frame
|
109
|
+
var _window = window,
|
110
|
+
scrollY = _window.scrollY,
|
111
|
+
scrollX = _window.scrollX,
|
112
|
+
innerHeight = _window.innerHeight;
|
113
|
+
|
114
|
+
document.body.style.position = 'fixed';
|
115
|
+
document.body.style.top = -scrollY + 'px';
|
116
|
+
document.body.style.left = -scrollX + 'px';
|
117
|
+
|
118
|
+
setTimeout(function () {
|
119
|
+
return window.requestAnimationFrame(function () {
|
120
|
+
// Attempt to check if the bottom bar appeared due to the position change
|
121
|
+
var bottomBarHeight = innerHeight - window.innerHeight;
|
122
|
+
if (bottomBarHeight && scrollY >= innerHeight) {
|
123
|
+
// Move the content further up so that the bottom bar doesn't hide it
|
124
|
+
document.body.style.top = -(scrollY + bottomBarHeight);
|
125
|
+
}
|
126
|
+
});
|
127
|
+
}, 300);
|
128
|
+
}
|
129
|
+
});
|
130
|
+
};
|
131
|
+
|
132
|
+
var restorePositionSetting = function restorePositionSetting() {
|
133
|
+
if (previousBodyPosition !== undefined) {
|
134
|
+
// Convert the position from "px" to Int
|
135
|
+
var y = -parseInt(document.body.style.top, 10);
|
136
|
+
var x = -parseInt(document.body.style.left, 10);
|
137
|
+
|
138
|
+
// Restore styles
|
139
|
+
document.body.style.position = previousBodyPosition.position;
|
140
|
+
document.body.style.top = previousBodyPosition.top;
|
141
|
+
document.body.style.left = previousBodyPosition.left;
|
142
|
+
|
143
|
+
// Restore scroll
|
144
|
+
window.scrollTo(x, y);
|
145
|
+
|
146
|
+
previousBodyPosition = undefined;
|
147
|
+
}
|
148
|
+
};
|
149
|
+
|
150
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Problems_and_solutions
|
151
|
+
var isTargetElementTotallyScrolled = function isTargetElementTotallyScrolled(targetElement) {
|
152
|
+
return targetElement ? targetElement.scrollHeight - targetElement.scrollTop <= targetElement.clientHeight : false;
|
153
|
+
};
|
154
|
+
|
155
|
+
var handleScroll = function handleScroll(event, targetElement) {
|
156
|
+
var clientY = event.targetTouches[0].clientY - initialClientY;
|
157
|
+
|
158
|
+
if (allowTouchMove(event.target)) {
|
159
|
+
return false;
|
160
|
+
}
|
161
|
+
|
162
|
+
if (targetElement && targetElement.scrollTop === 0 && clientY > 0) {
|
163
|
+
// element is at the top of its scroll.
|
164
|
+
return preventDefault(event);
|
165
|
+
}
|
166
|
+
|
167
|
+
if (isTargetElementTotallyScrolled(targetElement) && clientY < 0) {
|
168
|
+
// element is at the bottom of its scroll.
|
169
|
+
return preventDefault(event);
|
170
|
+
}
|
171
|
+
|
172
|
+
event.stopPropagation();
|
173
|
+
return true;
|
174
|
+
};
|
175
|
+
|
176
|
+
export var disableBodyScroll = function disableBodyScroll(targetElement, options) {
|
177
|
+
// targetElement must be provided
|
178
|
+
if (!targetElement) {
|
179
|
+
// eslint-disable-next-line no-console
|
180
|
+
console.error('disableBodyScroll unsuccessful - targetElement must be provided when calling disableBodyScroll on IOS devices.');
|
181
|
+
return;
|
182
|
+
}
|
183
|
+
|
184
|
+
// disableBodyScroll must not have been called on this targetElement before
|
185
|
+
if (locks.some(function (lock) {
|
186
|
+
return lock.targetElement === targetElement;
|
187
|
+
})) {
|
188
|
+
return;
|
189
|
+
}
|
190
|
+
|
191
|
+
var lock = {
|
192
|
+
targetElement: targetElement,
|
193
|
+
options: options || {}
|
194
|
+
};
|
195
|
+
|
196
|
+
locks = [].concat(_toConsumableArray(locks), [lock]);
|
197
|
+
|
198
|
+
if (isIosDevice) {
|
199
|
+
setPositionFixed();
|
200
|
+
} else {
|
201
|
+
setOverflowHidden(options);
|
202
|
+
}
|
203
|
+
|
204
|
+
if (isIosDevice) {
|
205
|
+
targetElement.ontouchstart = function (event) {
|
206
|
+
if (event.targetTouches.length === 1) {
|
207
|
+
// detect single touch.
|
208
|
+
initialClientY = event.targetTouches[0].clientY;
|
209
|
+
}
|
210
|
+
};
|
211
|
+
targetElement.ontouchmove = function (event) {
|
212
|
+
if (event.targetTouches.length === 1) {
|
213
|
+
// detect single touch.
|
214
|
+
handleScroll(event, targetElement);
|
215
|
+
}
|
216
|
+
};
|
217
|
+
|
218
|
+
if (!documentListenerAdded) {
|
219
|
+
document.addEventListener('touchmove', preventDefault, hasPassiveEvents ? { passive: false } : undefined);
|
220
|
+
documentListenerAdded = true;
|
221
|
+
}
|
222
|
+
}
|
223
|
+
};
|
224
|
+
|
225
|
+
export var clearAllBodyScrollLocks = function clearAllBodyScrollLocks() {
|
226
|
+
if (isIosDevice) {
|
227
|
+
// Clear all locks ontouchstart/ontouchmove handlers, and the references.
|
228
|
+
locks.forEach(function (lock) {
|
229
|
+
lock.targetElement.ontouchstart = null;
|
230
|
+
lock.targetElement.ontouchmove = null;
|
231
|
+
});
|
232
|
+
|
233
|
+
if (documentListenerAdded) {
|
234
|
+
document.removeEventListener('touchmove', preventDefault, hasPassiveEvents ? { passive: false } : undefined);
|
235
|
+
documentListenerAdded = false;
|
236
|
+
}
|
237
|
+
|
238
|
+
// Reset initial clientY.
|
239
|
+
initialClientY = -1;
|
240
|
+
}
|
241
|
+
|
242
|
+
if (isIosDevice) {
|
243
|
+
restorePositionSetting();
|
244
|
+
} else {
|
245
|
+
restoreOverflowSetting();
|
246
|
+
}
|
247
|
+
|
248
|
+
locks = [];
|
249
|
+
};
|
250
|
+
|
251
|
+
export var enableBodyScroll = function enableBodyScroll(targetElement) {
|
252
|
+
if (!targetElement) {
|
253
|
+
// eslint-disable-next-line no-console
|
254
|
+
console.error('enableBodyScroll unsuccessful - targetElement must be provided when calling enableBodyScroll on IOS devices.');
|
255
|
+
return;
|
256
|
+
}
|
257
|
+
|
258
|
+
locks = locks.filter(function (lock) {
|
259
|
+
return lock.targetElement !== targetElement;
|
260
|
+
});
|
261
|
+
|
262
|
+
if (isIosDevice) {
|
263
|
+
targetElement.ontouchstart = null;
|
264
|
+
targetElement.ontouchmove = null;
|
265
|
+
|
266
|
+
if (documentListenerAdded && locks.length === 0) {
|
267
|
+
document.removeEventListener('touchmove', preventDefault, hasPassiveEvents ? { passive: false } : undefined);
|
268
|
+
documentListenerAdded = false;
|
269
|
+
}
|
270
|
+
}
|
271
|
+
|
272
|
+
if (isIosDevice) {
|
273
|
+
restorePositionSetting();
|
274
|
+
} else {
|
275
|
+
restoreOverflowSetting();
|
276
|
+
}
|
277
|
+
};
|
278
|
+
|
279
|
+
// MIT License
|
280
|
+
|
281
|
+
// Copyright (c) 2018 Will Po
|
282
|
+
|
283
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
284
|
+
// of this software and associated documentation files (the "Software"), to deal
|
285
|
+
// in the Software without restriction, including without limitation the rights
|
286
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
287
|
+
// copies of the Software, and to permit persons to whom the Software is
|
288
|
+
// furnished to do so, subject to the following conditions:
|
289
|
+
|
290
|
+
// The above copyright notice and this permission notice shall be included in all
|
291
|
+
// copies or substantial portions of the Software.
|
292
|
+
|
293
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
294
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
295
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
296
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
297
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
298
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
299
|
+
// SOFTWARE.
|
@@ -0,0 +1,181 @@
|
|
1
|
+
:root {
|
2
|
+
--hw-active-bg-color: #F3F4F6;
|
3
|
+
|
4
|
+
--hw-border-color: #D1D5DB;
|
5
|
+
--hw-border-radius: 0.375rem;
|
6
|
+
--hw-border-width--slim: 1px;
|
7
|
+
--hw-border-width--thick: 2px;
|
8
|
+
|
9
|
+
--hw-dialog-bottom-padding: 0;
|
10
|
+
--hw-dialog-font-size: 1.25rem;
|
11
|
+
--hw-dialog-input-height: 2.5rem;
|
12
|
+
--hw-dialog-label-alignment: center;
|
13
|
+
--hw-dialog-label-color: #1D1D1D;
|
14
|
+
--hw-dialog-label-padding: 0.5rem 0 0.375rem;
|
15
|
+
--hw-dialog-label-size: 1.05rem;
|
16
|
+
--hw-dialog-listbox-margin: 1.25rem 0 0;
|
17
|
+
--hw-dialog-padding: 1rem 1rem 0;
|
18
|
+
--hw-dialog-top-offset: 4rem;
|
19
|
+
|
20
|
+
--hw-font-size: 1rem;
|
21
|
+
|
22
|
+
--hw-handle-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");
|
23
|
+
--hw-handle-offset-right: 0.375rem;
|
24
|
+
--hw-handle-width: 1.5em;
|
25
|
+
|
26
|
+
--hw-combobox-width: 10rem;
|
27
|
+
|
28
|
+
--hw-focus-color: #2563EB;
|
29
|
+
|
30
|
+
--hw-line-height: 1.5rem;
|
31
|
+
|
32
|
+
--hw-listbox-height: calc(var(--hw-line-height) * 10);
|
33
|
+
--hw-listbox-offset-top: var(--hw-line-height);
|
34
|
+
--hw-listbox-z-index: 10;
|
35
|
+
|
36
|
+
--hw-option-bg-color: #FFFFFF;
|
37
|
+
|
38
|
+
--hw-padding--slim: 0.375rem;
|
39
|
+
--hw-padding--thick: 0.75rem;
|
40
|
+
}
|
41
|
+
|
42
|
+
.hw-combobox {
|
43
|
+
border-width: 0;
|
44
|
+
display: inline-block;
|
45
|
+
font-size: var(--hw-font-size);
|
46
|
+
margin: 0;
|
47
|
+
padding: 0;
|
48
|
+
position: relative;
|
49
|
+
isolation: isolate;
|
50
|
+
|
51
|
+
&, * {
|
52
|
+
box-sizing: border-box;
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
.hw-combobox__input {
|
57
|
+
border: var(--hw-border-width--slim) solid var(--hw-border-color);
|
58
|
+
border-radius: var(--hw-border-radius);
|
59
|
+
font-size: inherit;
|
60
|
+
line-height: var(--hw-line-height);
|
61
|
+
padding: var(--hw-padding--slim) var(--hw-handle-width) var(--hw-padding--slim) var(--hw-padding--thick);
|
62
|
+
position: relative;
|
63
|
+
width: var(--hw-combobox-width);
|
64
|
+
text-overflow: ellipsis;
|
65
|
+
}
|
66
|
+
|
67
|
+
.hw-combobox__handle {
|
68
|
+
height: 100%;
|
69
|
+
position: absolute;
|
70
|
+
right: var(--hw-handle-offset-right);
|
71
|
+
top: 0;
|
72
|
+
width: var(--hw-handle-width);
|
73
|
+
}
|
74
|
+
|
75
|
+
.hw-combobox__handle::before {
|
76
|
+
background-image: var(--hw-handle-image);
|
77
|
+
background-position: center;
|
78
|
+
background-repeat: no-repeat;
|
79
|
+
background-size: var(--hw-handle-width);
|
80
|
+
bottom: 0;
|
81
|
+
content: '';
|
82
|
+
left: 0;
|
83
|
+
position: absolute;
|
84
|
+
right: 0;
|
85
|
+
top: 0;
|
86
|
+
}
|
87
|
+
|
88
|
+
.hw-combobox__input:focus-visible {
|
89
|
+
box-shadow: 0 0 0 var(--hw-border-width--thick) var(--hw-focus-color);
|
90
|
+
outline: none;
|
91
|
+
}
|
92
|
+
|
93
|
+
.hw-combobox__listbox {
|
94
|
+
border-color: transparent;
|
95
|
+
border-radius: var(--hw-border-radius);
|
96
|
+
border-style: solid;
|
97
|
+
border-width: var(--hw-border-width--slim);
|
98
|
+
left: 0;
|
99
|
+
line-height: var(--hw-line-height);
|
100
|
+
list-style: none;
|
101
|
+
max-height: var(--hw-listbox-height);
|
102
|
+
overflow: auto;
|
103
|
+
padding: 0;
|
104
|
+
position: absolute;
|
105
|
+
top: var(--hw-listbox-offset-top);
|
106
|
+
width: 100%;
|
107
|
+
z-index: var(--hw-listbox-z-index);
|
108
|
+
|
109
|
+
&:has([role="option"]:not([hidden])) {
|
110
|
+
border-color: var(--hw-border-color);
|
111
|
+
}
|
112
|
+
}
|
113
|
+
|
114
|
+
.hw-combobox__option {
|
115
|
+
background-color: var(--hw-option-bg-color);
|
116
|
+
padding: var(--hw-padding--slim) var(--hw-padding--thick);
|
117
|
+
user-select: none;
|
118
|
+
white-space: nowrap;
|
119
|
+
overflow: hidden;
|
120
|
+
text-overflow: ellipsis;
|
121
|
+
}
|
122
|
+
|
123
|
+
.hw-combobox__option:hover,
|
124
|
+
.hw-combobox__option--selected {
|
125
|
+
background-color: var(--hw-active-bg-color);
|
126
|
+
}
|
127
|
+
|
128
|
+
.hw-combobox__dialog {
|
129
|
+
border: 0;
|
130
|
+
font-size: var(--hw-dialog-font-size);
|
131
|
+
height: auto;
|
132
|
+
margin: 0;
|
133
|
+
max-height: none;
|
134
|
+
max-width: none;
|
135
|
+
overflow: hidden;
|
136
|
+
padding: var(--hw-dialog-padding) var(--hw-dialog-padding) var(--hw-dialog-bottom-padding) var(--hw-dialog-padding);
|
137
|
+
position: fixed;
|
138
|
+
top: var(--hw-dialog-top-offset);
|
139
|
+
width: auto;
|
140
|
+
|
141
|
+
&[open] {
|
142
|
+
align-items: center;
|
143
|
+
display: flex;
|
144
|
+
flex-direction: column;
|
145
|
+
justify-content: start;
|
146
|
+
}
|
147
|
+
|
148
|
+
&::backdrop {
|
149
|
+
background-color: rgba(0, 0, 0, 0.6);
|
150
|
+
}
|
151
|
+
}
|
152
|
+
|
153
|
+
.hw-combobox__dialog__label {
|
154
|
+
align-self: var(--hw-dialog-label-alignment);
|
155
|
+
color: var(--hw-dialog-label-color);
|
156
|
+
font-size: var(--hw-dialog-label-size);
|
157
|
+
padding: var(--hw-dialog-label-padding);
|
158
|
+
}
|
159
|
+
|
160
|
+
.hw-combobox__dialog__input {
|
161
|
+
border: var(--hw-border-width--slim) solid var(--hw-border-color);
|
162
|
+
border-radius: var(--hw-border-radius);
|
163
|
+
font-size: inherit;
|
164
|
+
height: var(--hw-dialog-input-height);
|
165
|
+
line-height: var(--hw-dialog-input-height);
|
166
|
+
padding: var(--hw-padding--slim) var(--hw-padding--thick);
|
167
|
+
text-overflow: ellipsis;
|
168
|
+
width: 90%;
|
169
|
+
}
|
170
|
+
|
171
|
+
.hw-combobox__dialog__listbox {
|
172
|
+
margin: var(--hw-dialog-listbox-margin);
|
173
|
+
overflow: auto;
|
174
|
+
padding: 0;
|
175
|
+
width: 100%;
|
176
|
+
|
177
|
+
[role="option"] {
|
178
|
+
border-radius: var(--hw-border-radius);
|
179
|
+
padding: var(--hw-padding--thick);
|
180
|
+
}
|
181
|
+
}
|