hotwire_combobox 0.1.10 → 0.1.12
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/MIT-LICENSE +1 -1
- data/README.md +1 -4
- data/app/assets/javascripts/controllers/hw_combobox_controller.js +55 -211
- 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 +62 -73
- data/app/presenters/hotwire_combobox/component.rb +257 -0
- data/app/presenters/hotwire_combobox/listbox/option.rb +53 -0
- data/app/views/hotwire_combobox/_combobox.html.erb +5 -20
- 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/engine.rb +12 -2
- data/lib/hotwire_combobox/version.rb +1 -1
- data/lib/hotwire_combobox.rb +4 -2
- metadata +54 -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
|
+
}
|