shadcn-ui 0.0.13 → 0.0.15
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/app/helpers/components/date_picker_helper.rb +10 -0
- data/app/helpers/components/filter_helper.rb +1 -1
- data/app/helpers/components/table_helper.rb +6 -6
- data/app/javascript/controllers/ui/date-picker_controller.js +766 -0
- data/app/javascript/controllers/ui/select_controller.js +130 -0
- data/app/javascript/utils/bodyScrollLock.js +289 -0
- data/app/javascript/utils/iso_date.js +198 -0
- data/app/views/components/ui/_card.html.erb +1 -1
- data/app/views/components/ui/_date_picker.html.erb +13 -0
- data/app/views/components/ui/_filter.html.erb +16 -13
- data/app/views/components/ui/_list.html.erb +4 -4
- data/app/views/examples/components/accordion/code/_usage.erb +3 -3
- data/app/views/examples/components/date-picker/_usage.html.erb +19 -0
- data/app/views/examples/components/date-picker/code/_preview.erb +1 -0
- data/app/views/examples/components/date-picker/code/_usage.erb +1 -0
- data/app/views/examples/components/date-picker.html.erb +22 -0
- data/app/views/examples/components/dropdown-menu/code/_preview.erb +2 -2
- data/app/views/examples/components/dropdown-menu/code/_usage.erb +1 -1
- data/app/views/layouts/shared/_components.html.erb +1 -1
- data/config/importmap.rb +4 -1
- data/lib/shadcn-ui/version.rb +1 -1
- data/shadcn-ui.gemspec +1 -1
- metadata +14 -4
@@ -0,0 +1,130 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
import { useClickOutside } from "stimulus-use";
|
3
|
+
import { disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks } from "utils/bodyScrollLock";
|
4
|
+
|
5
|
+
export default class UISelectController extends Controller {
|
6
|
+
static targets = ["value", "menu", "wrapper"];
|
7
|
+
static values = { value: String };
|
8
|
+
|
9
|
+
connect() {
|
10
|
+
useClickOutside(this);
|
11
|
+
this.valueTarget.textContent =
|
12
|
+
this.valueValue || this.valueTarget.textContent || "Select an option";
|
13
|
+
this.selectedOption = null;
|
14
|
+
}
|
15
|
+
|
16
|
+
disconnect() {
|
17
|
+
clearAllBodyScrollLocks();
|
18
|
+
}
|
19
|
+
|
20
|
+
clickOutside(event) {
|
21
|
+
this.menuTarget.classList.add("hidden");
|
22
|
+
}
|
23
|
+
|
24
|
+
toggle() {
|
25
|
+
this.menuTarget.classList.toggle("hidden");
|
26
|
+
this.wrapperTarget.querySelector("button").focus();
|
27
|
+
|
28
|
+
const optionList = this.menuTarget.children;
|
29
|
+
const currentValue = this.valueTarget.textContent;
|
30
|
+
let childElement = null;
|
31
|
+
|
32
|
+
if (!this.menuTarget.classList.contains("hidden")) {
|
33
|
+
this.adjustScrollPosition();
|
34
|
+
disableBodyScroll(this.menuTarget);
|
35
|
+
} else {
|
36
|
+
enableBodyScroll(this.menuTarget);
|
37
|
+
}
|
38
|
+
|
39
|
+
Array.from(optionList).forEach(function (child) {
|
40
|
+
if (currentValue == child.textContent) {
|
41
|
+
child.classList.add("bg-gray-200", "text-gray-900");
|
42
|
+
childElement = child;
|
43
|
+
}
|
44
|
+
});
|
45
|
+
|
46
|
+
if (childElement) {
|
47
|
+
this.selectedOption = childElement;
|
48
|
+
childElement.scrollIntoView({ behavior: "instant", block: "nearest", inline: "start" });
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
adjustScrollPosition() {
|
53
|
+
const menuHeight = this.menuTarget.offsetHeight;
|
54
|
+
const optionsHeight = this.menuTarget.scrollHeight;
|
55
|
+
if (optionsHeight > menuHeight) {
|
56
|
+
this.menuTarget.style.maxHeight = `${menuHeight}px`;
|
57
|
+
this.menuTarget.style.overflowY = "scroll";
|
58
|
+
} else {
|
59
|
+
this.menuTarget.style.maxHeight = "auto";
|
60
|
+
this.menuTarget.style.overflowY = "auto";
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
select(event) {
|
65
|
+
const option = event.target;
|
66
|
+
this.setSelectedOption(option);
|
67
|
+
this.selectCurrentOption();
|
68
|
+
}
|
69
|
+
|
70
|
+
setValue(value) {
|
71
|
+
this.valueValue = value;
|
72
|
+
this.valueTarget.textContent = value;
|
73
|
+
}
|
74
|
+
|
75
|
+
key(event) {
|
76
|
+
if (this.menuTarget.classList.contains("hidden")) return;
|
77
|
+
|
78
|
+
switch (event.key) {
|
79
|
+
case "Escape":
|
80
|
+
this.menuTarget.classList.add("hidden");
|
81
|
+
break;
|
82
|
+
case "ArrowUp":
|
83
|
+
this.selectPreviousOption(event);
|
84
|
+
break;
|
85
|
+
case "ArrowDown":
|
86
|
+
this.selectNextOption(event);
|
87
|
+
break;
|
88
|
+
case "Enter":
|
89
|
+
this.selectCurrentOption();
|
90
|
+
break;
|
91
|
+
}
|
92
|
+
}
|
93
|
+
|
94
|
+
selectPreviousOption(event) {
|
95
|
+
const selected = this.selectedOption; //this.options.querySelector(".selected")
|
96
|
+
const prevOption = selected ? selected.previousElementSibling : this.options.lastElementChild;
|
97
|
+
this.setSelectedOption(prevOption);
|
98
|
+
}
|
99
|
+
|
100
|
+
selectNextOption(event) {
|
101
|
+
const selected = this.selectedOption; //this.options.querySelector(".selected")
|
102
|
+
const nextOption = selected ? selected.nextElementSibling : this.options.firstElementChild;
|
103
|
+
this.setSelectedOption(nextOption);
|
104
|
+
}
|
105
|
+
|
106
|
+
selectCurrentOption() {
|
107
|
+
const selected = this.selectedOption;
|
108
|
+
if (selected) {
|
109
|
+
this.valueTarget.textContent = selected.textContent;
|
110
|
+
this.menuTarget.classList.add("hidden");
|
111
|
+
|
112
|
+
this.wrapperTarget.textContent = selected.getAttribute("value");
|
113
|
+
this.wrapperTarget.dispatchEvent(new Event("change"));
|
114
|
+
}
|
115
|
+
}
|
116
|
+
|
117
|
+
setSelectedOption(option) {
|
118
|
+
if (!option) return;
|
119
|
+
|
120
|
+
// Reset the previously selected option
|
121
|
+
if (this.selectedOption) {
|
122
|
+
this.selectedOption.classList.remove("bg-gray-200", "text-gray-900");
|
123
|
+
}
|
124
|
+
|
125
|
+
// Set the new selected option
|
126
|
+
option.classList.add("bg-gray-200", "text-gray-900");
|
127
|
+
this.selectedOption = option;
|
128
|
+
option.scrollIntoView({ behavior: "instant", block: "nearest", inline: "start" });
|
129
|
+
}
|
130
|
+
}
|
@@ -0,0 +1,289 @@
|
|
1
|
+
// Imported from https://github.com/willmcpo/body-scroll-lock/blob/master/lib/bodyScrollLock.js
|
2
|
+
|
3
|
+
function _toConsumableArray(arr) {
|
4
|
+
if (Array.isArray(arr)) {
|
5
|
+
for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) {
|
6
|
+
arr2[i] = arr[i];
|
7
|
+
}
|
8
|
+
|
9
|
+
return arr2;
|
10
|
+
} else {
|
11
|
+
return Array.from(arr);
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
15
|
+
// Older browsers don't support event options, feature detect it.
|
16
|
+
|
17
|
+
// Adopted and modified solution from Bohdan Didukh (2017)
|
18
|
+
// https://stackoverflow.com/questions/41594997/ios-10-safari-prevent-scrolling-behind-a-fixed-overlay-and-maintain-scroll-posi
|
19
|
+
|
20
|
+
var hasPassiveEvents = false;
|
21
|
+
if (typeof window !== 'undefined') {
|
22
|
+
var passiveTestOptions = {
|
23
|
+
get passive() {
|
24
|
+
hasPassiveEvents = true;
|
25
|
+
return undefined;
|
26
|
+
}
|
27
|
+
};
|
28
|
+
window.addEventListener('testPassive', null, passiveTestOptions);
|
29
|
+
window.removeEventListener('testPassive', null, passiveTestOptions);
|
30
|
+
}
|
31
|
+
|
32
|
+
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);
|
33
|
+
|
34
|
+
|
35
|
+
var locks = [];
|
36
|
+
var documentListenerAdded = false;
|
37
|
+
var initialClientY = -1;
|
38
|
+
var previousBodyOverflowSetting = void 0;
|
39
|
+
var previousBodyPosition = void 0;
|
40
|
+
var previousBodyPaddingRight = void 0;
|
41
|
+
|
42
|
+
// returns true if `el` should be allowed to receive touchmove events.
|
43
|
+
var allowTouchMove = function allowTouchMove(el) {
|
44
|
+
return locks.some(function (lock) {
|
45
|
+
if (lock.options.allowTouchMove && lock.options.allowTouchMove(el)) {
|
46
|
+
return true;
|
47
|
+
}
|
48
|
+
|
49
|
+
return false;
|
50
|
+
});
|
51
|
+
};
|
52
|
+
|
53
|
+
var preventDefault = function preventDefault(rawEvent) {
|
54
|
+
var e = rawEvent || window.event;
|
55
|
+
|
56
|
+
// For the case whereby consumers adds a touchmove event listener to document.
|
57
|
+
// Recall that we do document.addEventListener('touchmove', preventDefault, { passive: false })
|
58
|
+
// in disableBodyScroll - so if we provide this opportunity to allowTouchMove, then
|
59
|
+
// the touchmove event on document will break.
|
60
|
+
if (allowTouchMove(e.target)) {
|
61
|
+
return true;
|
62
|
+
}
|
63
|
+
|
64
|
+
// Do not prevent if the event has more than one touch (usually meaning this is a multi touch gesture like pinch to zoom).
|
65
|
+
if (e.touches.length > 1) return true;
|
66
|
+
|
67
|
+
if (e.preventDefault) e.preventDefault();
|
68
|
+
|
69
|
+
return false;
|
70
|
+
};
|
71
|
+
|
72
|
+
var setOverflowHidden = function setOverflowHidden(options) {
|
73
|
+
// If previousBodyPaddingRight is already set, don't set it again.
|
74
|
+
if (previousBodyPaddingRight === undefined) {
|
75
|
+
var _reserveScrollBarGap = !!options && options.reserveScrollBarGap === true;
|
76
|
+
var scrollBarGap = window.innerWidth - document.documentElement.clientWidth;
|
77
|
+
|
78
|
+
if (_reserveScrollBarGap && scrollBarGap > 0) {
|
79
|
+
var computedBodyPaddingRight = parseInt(window.getComputedStyle(document.body).getPropertyValue('padding-right'), 10);
|
80
|
+
previousBodyPaddingRight = document.body.style.paddingRight;
|
81
|
+
document.body.style.paddingRight = computedBodyPaddingRight + scrollBarGap + 'px';
|
82
|
+
}
|
83
|
+
}
|
84
|
+
|
85
|
+
// If previousBodyOverflowSetting is already set, don't set it again.
|
86
|
+
if (previousBodyOverflowSetting === undefined) {
|
87
|
+
previousBodyOverflowSetting = document.body.style.overflow;
|
88
|
+
document.body.style.overflow = 'hidden';
|
89
|
+
}
|
90
|
+
};
|
91
|
+
|
92
|
+
var restoreOverflowSetting = function restoreOverflowSetting() {
|
93
|
+
if (previousBodyPaddingRight !== undefined) {
|
94
|
+
document.body.style.paddingRight = previousBodyPaddingRight;
|
95
|
+
|
96
|
+
// Restore previousBodyPaddingRight to undefined so setOverflowHidden knows it
|
97
|
+
// can be set again.
|
98
|
+
previousBodyPaddingRight = undefined;
|
99
|
+
}
|
100
|
+
|
101
|
+
if (previousBodyOverflowSetting !== undefined) {
|
102
|
+
document.body.style.overflow = previousBodyOverflowSetting;
|
103
|
+
|
104
|
+
// Restore previousBodyOverflowSetting to undefined
|
105
|
+
// so setOverflowHidden knows it can be set again.
|
106
|
+
previousBodyOverflowSetting = undefined;
|
107
|
+
}
|
108
|
+
};
|
109
|
+
|
110
|
+
var setPositionFixed = function setPositionFixed() {
|
111
|
+
return window.requestAnimationFrame(function () {
|
112
|
+
// If previousBodyPosition is already set, don't set it again.
|
113
|
+
if (previousBodyPosition === undefined) {
|
114
|
+
previousBodyPosition = {
|
115
|
+
position: document.body.style.position,
|
116
|
+
top: document.body.style.top,
|
117
|
+
left: document.body.style.left
|
118
|
+
};
|
119
|
+
|
120
|
+
// Update the dom inside an animation frame
|
121
|
+
var _window = window,
|
122
|
+
scrollY = _window.scrollY,
|
123
|
+
scrollX = _window.scrollX,
|
124
|
+
innerHeight = _window.innerHeight;
|
125
|
+
|
126
|
+
document.body.style.position = 'fixed';
|
127
|
+
document.body.style.top = -scrollY + 'px';
|
128
|
+
document.body.style.left = -scrollX + 'px';
|
129
|
+
|
130
|
+
setTimeout(function () {
|
131
|
+
return window.requestAnimationFrame(function () {
|
132
|
+
// Attempt to check if the bottom bar appeared due to the position change
|
133
|
+
var bottomBarHeight = innerHeight - window.innerHeight;
|
134
|
+
if (bottomBarHeight && scrollY >= innerHeight) {
|
135
|
+
// Move the content further up so that the bottom bar doesn't hide it
|
136
|
+
document.body.style.top = -(scrollY + bottomBarHeight);
|
137
|
+
}
|
138
|
+
});
|
139
|
+
}, 300);
|
140
|
+
}
|
141
|
+
});
|
142
|
+
};
|
143
|
+
|
144
|
+
var restorePositionSetting = function restorePositionSetting() {
|
145
|
+
if (previousBodyPosition !== undefined) {
|
146
|
+
// Convert the position from "px" to Int
|
147
|
+
var y = -parseInt(document.body.style.top, 10);
|
148
|
+
var x = -parseInt(document.body.style.left, 10);
|
149
|
+
|
150
|
+
// Restore styles
|
151
|
+
document.body.style.position = previousBodyPosition.position;
|
152
|
+
document.body.style.top = previousBodyPosition.top;
|
153
|
+
document.body.style.left = previousBodyPosition.left;
|
154
|
+
|
155
|
+
// Restore scroll
|
156
|
+
window.scrollTo(x, y);
|
157
|
+
|
158
|
+
previousBodyPosition = undefined;
|
159
|
+
}
|
160
|
+
};
|
161
|
+
|
162
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Problems_and_solutions
|
163
|
+
var isTargetElementTotallyScrolled = function isTargetElementTotallyScrolled(targetElement) {
|
164
|
+
return targetElement ? targetElement.scrollHeight - targetElement.scrollTop <= targetElement.clientHeight : false;
|
165
|
+
};
|
166
|
+
|
167
|
+
var handleScroll = function handleScroll(event, targetElement) {
|
168
|
+
var clientY = event.targetTouches[0].clientY - initialClientY;
|
169
|
+
|
170
|
+
if (allowTouchMove(event.target)) {
|
171
|
+
return false;
|
172
|
+
}
|
173
|
+
|
174
|
+
if (targetElement && targetElement.scrollTop === 0 && clientY > 0) {
|
175
|
+
// element is at the top of its scroll.
|
176
|
+
return preventDefault(event);
|
177
|
+
}
|
178
|
+
|
179
|
+
if (isTargetElementTotallyScrolled(targetElement) && clientY < 0) {
|
180
|
+
// element is at the bottom of its scroll.
|
181
|
+
return preventDefault(event);
|
182
|
+
}
|
183
|
+
|
184
|
+
event.stopPropagation();
|
185
|
+
return true;
|
186
|
+
};
|
187
|
+
|
188
|
+
export var disableBodyScroll = function disableBodyScroll(targetElement, options) {
|
189
|
+
// targetElement must be provided
|
190
|
+
if (!targetElement) {
|
191
|
+
// eslint-disable-next-line no-console
|
192
|
+
console.error('disableBodyScroll unsuccessful - targetElement must be provided when calling disableBodyScroll on IOS devices.');
|
193
|
+
return;
|
194
|
+
}
|
195
|
+
|
196
|
+
// disableBodyScroll must not have been called on this targetElement before
|
197
|
+
if (locks.some(function (lock) {
|
198
|
+
return lock.targetElement === targetElement;
|
199
|
+
})) {
|
200
|
+
return;
|
201
|
+
}
|
202
|
+
|
203
|
+
var lock = {
|
204
|
+
targetElement: targetElement,
|
205
|
+
options: options || {}
|
206
|
+
};
|
207
|
+
|
208
|
+
locks = [].concat(_toConsumableArray(locks), [lock]);
|
209
|
+
|
210
|
+
if (isIosDevice) {
|
211
|
+
setPositionFixed();
|
212
|
+
} else {
|
213
|
+
setOverflowHidden(options);
|
214
|
+
}
|
215
|
+
|
216
|
+
if (isIosDevice) {
|
217
|
+
targetElement.ontouchstart = function (event) {
|
218
|
+
if (event.targetTouches.length === 1) {
|
219
|
+
// detect single touch.
|
220
|
+
initialClientY = event.targetTouches[0].clientY;
|
221
|
+
}
|
222
|
+
};
|
223
|
+
targetElement.ontouchmove = function (event) {
|
224
|
+
if (event.targetTouches.length === 1) {
|
225
|
+
// detect single touch.
|
226
|
+
handleScroll(event, targetElement);
|
227
|
+
}
|
228
|
+
};
|
229
|
+
|
230
|
+
if (!documentListenerAdded) {
|
231
|
+
document.addEventListener('touchmove', preventDefault, hasPassiveEvents ? { passive: false } : undefined);
|
232
|
+
documentListenerAdded = true;
|
233
|
+
}
|
234
|
+
}
|
235
|
+
};
|
236
|
+
|
237
|
+
export var clearAllBodyScrollLocks = function clearAllBodyScrollLocks() {
|
238
|
+
if (isIosDevice) {
|
239
|
+
// Clear all locks ontouchstart/ontouchmove handlers, and the references.
|
240
|
+
locks.forEach(function (lock) {
|
241
|
+
lock.targetElement.ontouchstart = null;
|
242
|
+
lock.targetElement.ontouchmove = null;
|
243
|
+
});
|
244
|
+
|
245
|
+
if (documentListenerAdded) {
|
246
|
+
document.removeEventListener('touchmove', preventDefault, hasPassiveEvents ? { passive: false } : undefined);
|
247
|
+
documentListenerAdded = false;
|
248
|
+
}
|
249
|
+
|
250
|
+
// Reset initial clientY.
|
251
|
+
initialClientY = -1;
|
252
|
+
}
|
253
|
+
|
254
|
+
if (isIosDevice) {
|
255
|
+
restorePositionSetting();
|
256
|
+
} else {
|
257
|
+
restoreOverflowSetting();
|
258
|
+
}
|
259
|
+
|
260
|
+
locks = [];
|
261
|
+
};
|
262
|
+
|
263
|
+
export var enableBodyScroll = function enableBodyScroll(targetElement) {
|
264
|
+
if (!targetElement) {
|
265
|
+
// eslint-disable-next-line no-console
|
266
|
+
console.error('enableBodyScroll unsuccessful - targetElement must be provided when calling enableBodyScroll on IOS devices.');
|
267
|
+
return;
|
268
|
+
}
|
269
|
+
|
270
|
+
locks = locks.filter(function (lock) {
|
271
|
+
return lock.targetElement !== targetElement;
|
272
|
+
});
|
273
|
+
|
274
|
+
if (isIosDevice) {
|
275
|
+
targetElement.ontouchstart = null;
|
276
|
+
targetElement.ontouchmove = null;
|
277
|
+
|
278
|
+
if (documentListenerAdded && locks.length === 0) {
|
279
|
+
document.removeEventListener('touchmove', preventDefault, hasPassiveEvents ? { passive: false } : undefined);
|
280
|
+
documentListenerAdded = false;
|
281
|
+
}
|
282
|
+
}
|
283
|
+
|
284
|
+
if (isIosDevice) {
|
285
|
+
restorePositionSetting();
|
286
|
+
} else {
|
287
|
+
restoreOverflowSetting();
|
288
|
+
}
|
289
|
+
};
|
@@ -0,0 +1,198 @@
|
|
1
|
+
// Imported from https://raw.githubusercontent.com/airblade/stimulus-datepicker/main/src/iso_date.js
|
2
|
+
|
3
|
+
export default class IsoDate {
|
4
|
+
|
5
|
+
// new IsoDate()
|
6
|
+
// new IsoDate('')
|
7
|
+
//
|
8
|
+
// new IsoDate(date)
|
9
|
+
//
|
10
|
+
// new IsoDate("2022-05-16")
|
11
|
+
//
|
12
|
+
// new IsoDate("2022", "05", "16")
|
13
|
+
constructor(dateOrYear, month, day) {
|
14
|
+
if (dateOrYear && month && day) {
|
15
|
+
this.yyyy = dateOrYear.toString()
|
16
|
+
this.mm = this.zeroPad(month)
|
17
|
+
this.dd = this.zeroPad(day)
|
18
|
+
} else if (dateOrYear instanceof Date) {
|
19
|
+
this.yyyy = dateOrYear.getFullYear().toString()
|
20
|
+
this.mm = this.zeroPad(dateOrYear.getMonth() + 1)
|
21
|
+
this.dd = this.zeroPad(dateOrYear.getDate())
|
22
|
+
} else if (dateOrYear) {
|
23
|
+
[this.yyyy, this.mm, this.dd] = dateOrYear.split('-')
|
24
|
+
} else {
|
25
|
+
const today = new Date()
|
26
|
+
this.yyyy = today.getFullYear().toString()
|
27
|
+
this.mm = this.zeroPad(today.getMonth() + 1)
|
28
|
+
this.dd = this.zeroPad(today.getDate())
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
toString() {
|
33
|
+
return [this.yyyy, this.mm, this.dd].join('-')
|
34
|
+
}
|
35
|
+
|
36
|
+
setDayOfMonth(dayOfMonth) {
|
37
|
+
const date = this.toDate()
|
38
|
+
date.setDate(dayOfMonth)
|
39
|
+
return new IsoDate(date)
|
40
|
+
}
|
41
|
+
|
42
|
+
// @param [Number] first day of the week (Sunday is 0)
|
43
|
+
firstDayOfWeek(weekStart) {
|
44
|
+
const date = this.toDate()
|
45
|
+
date.setDate(date.getDate() - (7 + date.getDay() - weekStart) % 7)
|
46
|
+
return new IsoDate(date)
|
47
|
+
}
|
48
|
+
|
49
|
+
// @param [Number] first day of the week (Sunday is 0)
|
50
|
+
lastDayOfWeek(weekStart) {
|
51
|
+
const date = this.toDate()
|
52
|
+
date.setDate(date.getDate() + (weekStart + 6 - date.getDay()) % 7)
|
53
|
+
return new IsoDate(date)
|
54
|
+
}
|
55
|
+
|
56
|
+
previousYear() {
|
57
|
+
return this.increment('yyyy', -1)
|
58
|
+
}
|
59
|
+
|
60
|
+
nextYear() {
|
61
|
+
return this.increment('yyyy', 1)
|
62
|
+
}
|
63
|
+
|
64
|
+
// @param [Boolean] whether to return the same day in the previous month (true)
|
65
|
+
// or the same day of the week in the previous month (false).
|
66
|
+
previousMonth(sameDayOfMonth = true) {
|
67
|
+
if (sameDayOfMonth) {
|
68
|
+
return this.increment('mm', -1)
|
69
|
+
} else {
|
70
|
+
const month = this.mm
|
71
|
+
let isoDate = this.increment('dd', -28)
|
72
|
+
if (isoDate.mm == month) isoDate = isoDate.increment('dd', -7)
|
73
|
+
return isoDate
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
// @param [Boolean] whether to return the same day in the next month (true)
|
78
|
+
// or the same day of the week in the next month (false).
|
79
|
+
nextMonth(sameDayOfMonth = true) {
|
80
|
+
if (sameDayOfMonth) {
|
81
|
+
return this.increment('mm', 1)
|
82
|
+
} else {
|
83
|
+
const month = this.mm
|
84
|
+
let isoDate = this.increment('dd', 28)
|
85
|
+
if (isoDate.mm == month) isoDate = isoDate.increment('dd', 7)
|
86
|
+
return isoDate
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
previousWeek() {
|
91
|
+
return this.increment('dd', -7)
|
92
|
+
}
|
93
|
+
|
94
|
+
nextWeek() {
|
95
|
+
return this.increment('dd', 7)
|
96
|
+
}
|
97
|
+
|
98
|
+
previousDay() {
|
99
|
+
return this.increment('dd', -1)
|
100
|
+
}
|
101
|
+
|
102
|
+
nextDay() {
|
103
|
+
return this.increment('dd', 1)
|
104
|
+
}
|
105
|
+
|
106
|
+
isWeekend() {
|
107
|
+
return [0, 6].includes(this.toDate().getDay())
|
108
|
+
}
|
109
|
+
|
110
|
+
isToday() {
|
111
|
+
return this.equals(new IsoDate())
|
112
|
+
}
|
113
|
+
|
114
|
+
isFirstDayOfWeek(weekStart) {
|
115
|
+
return this.toDate().getDay() == weekStart
|
116
|
+
}
|
117
|
+
|
118
|
+
isLastDayOfWeek(weekStart) {
|
119
|
+
const days = {0: 6, 1: 0, 2: 1, 3: 2, 4: 3, 5: 4, 6: 5}
|
120
|
+
return this.toDate().getDay() == days[weekStart]
|
121
|
+
}
|
122
|
+
|
123
|
+
equals(isoDate) {
|
124
|
+
return this.toString() == isoDate.toString()
|
125
|
+
}
|
126
|
+
|
127
|
+
before(isoDate) {
|
128
|
+
return this.toString() < isoDate.toString()
|
129
|
+
}
|
130
|
+
|
131
|
+
after(isoDate) {
|
132
|
+
return this.toString() > isoDate.toString()
|
133
|
+
}
|
134
|
+
|
135
|
+
// @param unit [String] 'dd' | 'mm' | 'yyyy'
|
136
|
+
// @param count [Number]
|
137
|
+
increment(unit, count) {
|
138
|
+
let date
|
139
|
+
if (unit == 'dd') {
|
140
|
+
date = this.toDate()
|
141
|
+
date.setDate(date.getDate() + count)
|
142
|
+
} else {
|
143
|
+
date = unit == 'yyyy'
|
144
|
+
? new Date(+this.yyyy + count, +this.mm - 1)
|
145
|
+
: new Date(+this.yyyy, +this.mm - 1 + count)
|
146
|
+
const endOfMonth = IsoDate.daysInMonth(date.getMonth() + 1, date.getYear())
|
147
|
+
date.setDate(+this.dd > endOfMonth ? endOfMonth : +this.dd)
|
148
|
+
}
|
149
|
+
return new IsoDate(date)
|
150
|
+
}
|
151
|
+
|
152
|
+
static isValidStr(str) {
|
153
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(str)) return false
|
154
|
+
return this.isValidDate(...str.split('-').map(s => +s))
|
155
|
+
}
|
156
|
+
|
157
|
+
// @param year [Number] four-digit year
|
158
|
+
// @param month [Number] month number (January is 1)
|
159
|
+
// @param day [Number] day in month
|
160
|
+
static isValidDate(year, month, day) {
|
161
|
+
if (year < 1000 || year > 9999) return false
|
162
|
+
if (month < 1 || month > 12) return false
|
163
|
+
if (day < 1 || day > this.daysInMonth(month, year)) return false
|
164
|
+
return true
|
165
|
+
}
|
166
|
+
|
167
|
+
// Returns the number of days in the month.
|
168
|
+
//
|
169
|
+
// @param month [Number] the month (1 is January)
|
170
|
+
// @param year [Number] the year (e.g. 2022)
|
171
|
+
// @return [Number] the number of days
|
172
|
+
static daysInMonth(month, year) {
|
173
|
+
if ([1, 3, 5, 7, 8, 10, 12].includes(month)) return 31
|
174
|
+
if ([4, 6, 9, 11].includes(month)) return 30
|
175
|
+
return this.isLeapYear(year) ? 29 : 28
|
176
|
+
}
|
177
|
+
|
178
|
+
static isLeapYear(year) {
|
179
|
+
if ((year % 400) == 0) return true
|
180
|
+
if ((year % 100) == 0) return false
|
181
|
+
return year % 4 == 0
|
182
|
+
}
|
183
|
+
|
184
|
+
// Returns a two-digit zero-padded string.
|
185
|
+
zeroPad(num) {
|
186
|
+
return num.toString().padStart(2, '0')
|
187
|
+
}
|
188
|
+
|
189
|
+
toDate() {
|
190
|
+
// Cannot use `new Date('YYYY-MM-DD')`: it is treated as UTC, not local.
|
191
|
+
return new Date(+this.yyyy, +this.mm - 1, +this.dd)
|
192
|
+
}
|
193
|
+
|
194
|
+
getMonthName() {
|
195
|
+
const month = ["January","February","March","April","May","June","July","August","September","October","November","December"]
|
196
|
+
return month[this.toDate().getMonth()];
|
197
|
+
}
|
198
|
+
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
<div class="<%= tw("rounded-lg border bg-card text-card-foreground shadow-sm w-[350px]", options[:class])
|
1
|
+
<div class="<%= tw("rounded-lg border bg-card text-card-foreground shadow-sm w-[350px]", options[:class]) %>">
|
2
2
|
<% if title || subtitle %>
|
3
3
|
<div class="flex flex-col space-y-1.5 p-6">
|
4
4
|
<% if title %>
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<div data-controller="ui--date-picker">
|
2
|
+
<button data-ui--date-picker-target="toggle"
|
3
|
+
class="inline-flex items-center whitespace-nowrap rounded-md text-sm ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2 w-[280px] justify-start text-left font-normal text-muted-foreground" type="button">
|
4
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-calendar mr-2 h-4 w-4">
|
5
|
+
<path d="M8 2v4"></path>
|
6
|
+
<path d="M16 2v4"></path>
|
7
|
+
<rect width="18" height="18" x="3" y="4" rx="2"></rect>
|
8
|
+
<path d="M3 10h18"></path>
|
9
|
+
</svg>
|
10
|
+
<span name=<%= name %> data-ui--date-picker-target="input"><%= value %></span>
|
11
|
+
<input type="hidden" name=<%=name%> value=<%value%> data-ui--date-picker-target="input">
|
12
|
+
</button>
|
13
|
+
</div>
|
@@ -1,15 +1,18 @@
|
|
1
1
|
<div data-controller="ui--filter">
|
2
|
-
<%= render_card do %>
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
<%=
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
<% end %>
|
2
|
+
<%= render_card do %>
|
3
|
+
<div class="flex items-center">
|
4
|
+
<%= content_for :filter_icon %>
|
5
|
+
<%= render_input name: "filter", placeholder: "Filter items...", variant: :borderless, class: input_class, data: {"ui--filter-target": "source", action: "input->ui--filter#filter"} %>
|
6
|
+
</div>
|
7
|
+
<%= render_separator %>
|
8
|
+
<div class="<%= options[:class] %>">
|
9
|
+
<%= content_tag :div, role: "group" do %>
|
10
|
+
<% items.each do |item| %>
|
11
|
+
<div data-ui--filter-target="item">
|
12
|
+
<%= list_item(value: item[:value], name: item[:name], selected: item[:selected]) %>
|
13
|
+
</div>
|
14
|
+
<% end %>
|
15
|
+
<% end %>
|
16
|
+
</div>
|
17
|
+
<% end %>
|
15
18
|
</div>
|
@@ -1,5 +1,5 @@
|
|
1
|
-
<%= content_tag as, role: "group" do
|
2
|
-
items.each do |item| %>
|
1
|
+
<%= content_tag as, role: "group" do %>
|
2
|
+
<% items.each do |item| %>
|
3
3
|
<%= list_item(value: item[:value], name: item[:name], selected: item[:selected]) %>
|
4
|
-
<% end
|
5
|
-
end %>
|
4
|
+
<% end %>
|
5
|
+
<% end %>
|
@@ -7,10 +7,10 @@
|
|
7
7
|
<% end %>
|
8
8
|
|
9
9
|
# Title and Description Block
|
10
|
-
|
11
|
-
<%= accordion_title do
|
10
|
+
<%= render_accordion do %>
|
11
|
+
<%= accordion_title do %>
|
12
12
|
<% end %>
|
13
13
|
|
14
|
-
<%= accordion_description do
|
14
|
+
<%= accordion_description do %>
|
15
15
|
<% end %>
|
16
16
|
<% end %>
|