@stimulus-plumbers/controllers 0.3.3 → 0.4.1
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/README.md +31 -8
- package/dist/controllers.manifest.json +386 -0
- package/dist/stimulus-plumbers-controllers.es.js +756 -304
- package/dist/stimulus-plumbers-controllers.umd.js +1 -1
- package/package.json +3 -4
- package/src/accessibility/aria.js +6 -0
- package/src/accessibility/focus.js +4 -20
- package/src/accessibility/keyboard.js +134 -28
- package/src/controllers/calendar_decade_controller.js +99 -0
- package/src/controllers/calendar_decade_selector_controller.js +14 -0
- package/src/controllers/calendar_month_controller.js +71 -19
- package/src/controllers/calendar_month_selector_controller.js +14 -0
- package/src/controllers/calendar_year_controller.js +104 -0
- package/src/controllers/calendar_year_selector_controller.js +14 -0
- package/src/controllers/combobox_date_controller.js +90 -5
- package/src/controllers/combobox_dropdown_controller.js +12 -25
- package/src/controllers/input_combobox_controller.js +2 -42
- package/src/controllers/modal_controller.js +4 -1
- package/src/controllers/popover_controller.js +39 -12
- package/src/controllers/timeline_controller.js +71 -0
- package/src/index.js +6 -1
- package/src/plumbers/calendar-selector.js +85 -0
- package/src/plumbers/calendar.js +44 -10
- package/src/plumbers/index.js +5 -0
- package/src/plumbers/visibility.js +3 -3
- package/src/controllers/calendar_month_observer_controller.js +0 -27
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
import { DateFormatter } from '../plumbers/formatters/date';
|
|
3
|
+
import { RovingTabIndex } from '../accessibility/keyboard';
|
|
4
|
+
import { setExpanded, setHidden, announce } from '../accessibility/aria';
|
|
5
|
+
|
|
6
|
+
export default class extends Controller {
|
|
7
|
+
static targets = ['trigger', 'detail', 'time'];
|
|
8
|
+
static values = {
|
|
9
|
+
dateFormat: { type: Object, default: {} },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
connect() {
|
|
13
|
+
this.rovingTabIndex = new RovingTabIndex(this.triggerTargets, { orientation: 'vertical' });
|
|
14
|
+
this.rovingTabIndex.activate();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
disconnect() {
|
|
18
|
+
this.rovingTabIndex?.deactivate();
|
|
19
|
+
this.rovingTabIndex = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
toggle(event) {
|
|
23
|
+
const trigger = event.currentTarget;
|
|
24
|
+
trigger.getAttribute('aria-expanded') === 'true' ? this.collapseItem(trigger) : this.expandItem(trigger);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
expand(event) {
|
|
28
|
+
this.expandItem(event.currentTarget);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
collapse(event) {
|
|
32
|
+
this.collapseItem(event.currentTarget);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
expandItem(trigger) {
|
|
36
|
+
const detail = this.detailTargets.find((d) => d.id === trigger.getAttribute('aria-controls')) ?? null;
|
|
37
|
+
this.dispatch('expand', { detail: { trigger, detail } });
|
|
38
|
+
setExpanded(trigger, true);
|
|
39
|
+
if (detail) {
|
|
40
|
+
setHidden(detail, false);
|
|
41
|
+
announce(`${trigger.textContent.trim()} expanded`);
|
|
42
|
+
}
|
|
43
|
+
this.dispatch('expanded', { detail: { trigger, detail } });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
collapseItem(trigger) {
|
|
47
|
+
const detail = this.detailTargets.find((d) => d.id === trigger.getAttribute('aria-controls')) ?? null;
|
|
48
|
+
this.dispatch('collapse', { detail: { trigger, detail } });
|
|
49
|
+
setExpanded(trigger, false);
|
|
50
|
+
if (detail) {
|
|
51
|
+
setHidden(detail, true);
|
|
52
|
+
announce(`${trigger.textContent.trim()} collapsed`);
|
|
53
|
+
}
|
|
54
|
+
this.dispatch('collapsed', { detail: { trigger, detail } });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
triggerTargetConnected(trigger) {
|
|
58
|
+
if (!trigger.hasAttribute('aria-expanded')) trigger.setAttribute('aria-expanded', 'false');
|
|
59
|
+
this.rovingTabIndex?.updateItems(this.triggerTargets);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
triggerTargetDisconnected() {
|
|
63
|
+
this.rovingTabIndex?.updateItems(this.triggerTargets);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
timeTargetConnected(el) {
|
|
67
|
+
if (!Object.keys(this.dateFormatValue).length || el.textContent.trim()) return;
|
|
68
|
+
const formatted = DateFormatter.format(el.getAttribute('datetime'), this.dateFormatValue);
|
|
69
|
+
if (formatted) el.textContent = formatted;
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/index.js
CHANGED
|
@@ -17,8 +17,12 @@ export { fuzzyMatcher, filterOptions } from './researcher.js';
|
|
|
17
17
|
export { Formatter, FORMATTER_TYPES } from './plumbers/formatter.js';
|
|
18
18
|
|
|
19
19
|
// Export Stimulus controllers
|
|
20
|
+
export { default as CalendarDecadeController } from './controllers/calendar_decade_controller.js';
|
|
21
|
+
export { default as CalendarDecadeSelectorController } from './controllers/calendar_decade_selector_controller.js';
|
|
20
22
|
export { default as CalendarMonthController } from './controllers/calendar_month_controller.js';
|
|
21
|
-
export { default as
|
|
23
|
+
export { default as CalendarMonthSelectorController } from './controllers/calendar_month_selector_controller.js';
|
|
24
|
+
export { default as CalendarYearController } from './controllers/calendar_year_controller.js';
|
|
25
|
+
export { default as CalendarYearSelectorController } from './controllers/calendar_year_selector_controller.js';
|
|
22
26
|
export { default as ClipboardController } from './controllers/clipboard_controller.js';
|
|
23
27
|
export { default as ComboboxDateController } from './controllers/combobox_date_controller.js';
|
|
24
28
|
export { default as ComboboxDropdownController } from './controllers/combobox_dropdown_controller.js';
|
|
@@ -31,3 +35,4 @@ export { default as InputClearableController } from './controllers/input_clearab
|
|
|
31
35
|
export { default as ModalController } from './controllers/modal_controller.js';
|
|
32
36
|
export { default as PannerController } from './controllers/panner_controller.js';
|
|
33
37
|
export { default as PopoverController } from './controllers/popover_controller.js';
|
|
38
|
+
export { default as TimelineController } from './controllers/timeline_controller.js';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import Plumber from './plumber';
|
|
2
|
+
import { tryParseDate } from './plumber/date';
|
|
3
|
+
|
|
4
|
+
export class CalendarDaySelector extends Plumber {
|
|
5
|
+
constructor(controller, options = {}) {
|
|
6
|
+
super(controller, options);
|
|
7
|
+
this.handle = this.handle.bind(this);
|
|
8
|
+
this.onSelect = options.onSelect || null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
attach() {
|
|
12
|
+
this.element.addEventListener('click', this.handle);
|
|
13
|
+
}
|
|
14
|
+
detach() {
|
|
15
|
+
this.element.removeEventListener('click', this.handle);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
handle(event) {
|
|
19
|
+
if (!(event.target instanceof HTMLElement)) return;
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
const input = event.target instanceof HTMLTimeElement ? event.target.parentElement : event.target;
|
|
22
|
+
if (input.disabled || input.getAttribute('aria-disabled') === 'true') return;
|
|
23
|
+
const time = event.target instanceof HTMLTimeElement ? event.target : event.target.querySelector('time');
|
|
24
|
+
if (!time) return;
|
|
25
|
+
const date = tryParseDate(time.dateTime);
|
|
26
|
+
if (!date) return;
|
|
27
|
+
this.dispatch('selecting', { target: input });
|
|
28
|
+
const iso = date.toISOString();
|
|
29
|
+
if (this.onSelect) {
|
|
30
|
+
this.awaitCallback(this.onSelect, iso);
|
|
31
|
+
} else {
|
|
32
|
+
this.dispatch('selected', { detail: { epoch: date.getTime(), iso } });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class CalendarMonthSelector extends Plumber {
|
|
38
|
+
constructor(controller, options = {}) {
|
|
39
|
+
super(controller, options);
|
|
40
|
+
this.handle = this.handle.bind(this);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
attach() {
|
|
44
|
+
this.element.addEventListener('click', this.handle);
|
|
45
|
+
}
|
|
46
|
+
detach() {
|
|
47
|
+
this.element.removeEventListener('click', this.handle);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
handle(event) {
|
|
51
|
+
const btn = event.target.closest('button[data-month]');
|
|
52
|
+
if (!btn || btn.disabled || btn.getAttribute('aria-disabled') === 'true') return;
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
const month = parseInt(btn.dataset.month, 10);
|
|
55
|
+
if (isNaN(month)) return;
|
|
56
|
+
this.dispatch('selected', { detail: { month } });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class CalendarYearSelector extends Plumber {
|
|
61
|
+
constructor(controller, options = {}) {
|
|
62
|
+
super(controller, options);
|
|
63
|
+
this.handle = this.handle.bind(this);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
attach() {
|
|
67
|
+
this.element.addEventListener('click', this.handle);
|
|
68
|
+
}
|
|
69
|
+
detach() {
|
|
70
|
+
this.element.removeEventListener('click', this.handle);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
handle(event) {
|
|
74
|
+
const btn = event.target.closest('button[data-year]');
|
|
75
|
+
if (!btn || btn.disabled || btn.getAttribute('aria-disabled') === 'true') return;
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
const year = parseInt(btn.dataset.year, 10);
|
|
78
|
+
if (isNaN(year)) return;
|
|
79
|
+
this.dispatch('selected', { detail: { year } });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const attachCalendarDaySelector = (controller, options) => new CalendarDaySelector(controller, options);
|
|
84
|
+
export const attachCalendarMonthSelector = (controller, options) => new CalendarMonthSelector(controller, options);
|
|
85
|
+
export const attachCalendarYearSelector = (controller, options) => new CalendarYearSelector(controller, options);
|
package/src/plumbers/calendar.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import Plumber from './plumber';
|
|
2
2
|
import { isValidDate, tryParseDate } from './plumber/date';
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const DAYS_PER_ROW = 7;
|
|
5
|
+
const YEAR_SIZE = 12;
|
|
6
|
+
const DECADE_SIZE = 10;
|
|
5
7
|
|
|
6
8
|
const defaultOptions = {
|
|
7
9
|
locales: ['default'],
|
|
@@ -77,6 +79,7 @@ export class Calendar extends Plumber {
|
|
|
77
79
|
this.daysOfWeek = this.buildDaysOfWeek();
|
|
78
80
|
this.daysOfMonth = this.buildDaysOfMonth();
|
|
79
81
|
this.monthsOfYear = this.buildMonthsOfYear();
|
|
82
|
+
this.yearsOfDecade = this.buildYearsOfDecade();
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
/**
|
|
@@ -130,8 +133,8 @@ export class Calendar extends Plumber {
|
|
|
130
133
|
const current = new Date(currentYear, currentMonth, i);
|
|
131
134
|
daysOfMonth.push(parseDate(current));
|
|
132
135
|
}
|
|
133
|
-
const mod = daysOfMonth.length %
|
|
134
|
-
const trailing = mod === 0 ? 0 :
|
|
136
|
+
const mod = daysOfMonth.length % DAYS_PER_ROW;
|
|
137
|
+
const trailing = mod === 0 ? 0 : DAYS_PER_ROW - mod;
|
|
135
138
|
for (let i = 1; i <= trailing; i++) {
|
|
136
139
|
const next = new Date(currentYear, currentMonth + 1, i);
|
|
137
140
|
daysOfMonth.push(parseDate(next));
|
|
@@ -149,19 +152,47 @@ export class Calendar extends Plumber {
|
|
|
149
152
|
const numericFormatter = new Intl.DateTimeFormat(this.localesValue, { month: 'numeric' });
|
|
150
153
|
|
|
151
154
|
const monthsOfYear = [];
|
|
152
|
-
for (let i = 0; i <
|
|
153
|
-
const
|
|
155
|
+
for (let i = 0; i < YEAR_SIZE; i++) {
|
|
156
|
+
const date = new Date(this.year, i);
|
|
157
|
+
const monthStart = new Date(this.year, i, 1);
|
|
158
|
+
const monthEnd = new Date(this.year, i + 1, 0);
|
|
159
|
+
const shortName = shortFormatter.format(date);
|
|
160
|
+
const longName = longFormatter.format(date);
|
|
161
|
+
const rangeDisabled = (this.since && monthEnd < this.since) || (this.till && monthStart > this.till);
|
|
162
|
+
const listDisabled = this.disabledMonths.some((str) => i == str || shortName === str || longName === str);
|
|
154
163
|
monthsOfYear.push({
|
|
155
|
-
date:
|
|
156
|
-
value:
|
|
157
|
-
long:
|
|
158
|
-
short:
|
|
159
|
-
numeric: numericFormatter.format(
|
|
164
|
+
date: date,
|
|
165
|
+
value: date.getMonth(),
|
|
166
|
+
long: longName,
|
|
167
|
+
short: shortName,
|
|
168
|
+
numeric: numericFormatter.format(date),
|
|
169
|
+
disabled: rangeDisabled || listDisabled,
|
|
160
170
|
});
|
|
161
171
|
}
|
|
162
172
|
return monthsOfYear;
|
|
163
173
|
}
|
|
164
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Builds array of year objects spanning the decade containing the current year.
|
|
177
|
+
* Returns 12 entries: decade start - 1 (buffer) through decade start + 10 (buffer).
|
|
178
|
+
* @returns {Array<Object>} Array of year objects
|
|
179
|
+
*/
|
|
180
|
+
buildYearsOfDecade() {
|
|
181
|
+
const decadeStart = Math.floor(this.year / DECADE_SIZE) * DECADE_SIZE;
|
|
182
|
+
const yearsOfDecade = [];
|
|
183
|
+
for (let i = decadeStart - 1; i <= decadeStart + DECADE_SIZE; i++) {
|
|
184
|
+
const bufferYear = i < decadeStart || i > decadeStart + DECADE_SIZE - 1;
|
|
185
|
+
const rangeDisabled = (this.since && i < this.since.getFullYear()) || (this.till && i > this.till.getFullYear());
|
|
186
|
+
const listDisabled = this.disabledYears.some((str) => i == str);
|
|
187
|
+
yearsOfDecade.push({
|
|
188
|
+
value: i,
|
|
189
|
+
current: i === this.year,
|
|
190
|
+
disabled: bufferYear || rangeDisabled || listDisabled,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return yearsOfDecade;
|
|
194
|
+
}
|
|
195
|
+
|
|
165
196
|
/**
|
|
166
197
|
* Gets the current "today" reference date.
|
|
167
198
|
* @returns {Date} Today's date
|
|
@@ -380,6 +411,9 @@ export class Calendar extends Plumber {
|
|
|
380
411
|
get monthsOfYear() {
|
|
381
412
|
return context.monthsOfYear;
|
|
382
413
|
},
|
|
414
|
+
get yearsOfDecade() {
|
|
415
|
+
return context.yearsOfDecade;
|
|
416
|
+
},
|
|
383
417
|
navigate: async (to) => await context.navigate(to),
|
|
384
418
|
step: async (type, value) => await context.step(type, value),
|
|
385
419
|
isDisabled: (date) => context.isDisabled(date),
|
package/src/plumbers/index.js
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export { initCalendar } from './calendar';
|
|
6
|
+
export {
|
|
7
|
+
attachCalendarDaySelector,
|
|
8
|
+
attachCalendarMonthSelector,
|
|
9
|
+
attachCalendarYearSelector,
|
|
10
|
+
} from './calendar-selector';
|
|
6
11
|
export { attachContentLoader } from './content_loader';
|
|
7
12
|
export { attachDismisser } from './dismisser';
|
|
8
13
|
export { attachFlipper } from './flipper';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Plumber from './plumber';
|
|
2
2
|
import { visibilityConfig } from './plumber/config';
|
|
3
|
+
import { setExpanded, setHidden } from '../accessibility/aria';
|
|
3
4
|
|
|
4
5
|
const defaultOptions = {
|
|
5
6
|
visibility: 'visibility',
|
|
@@ -62,8 +63,7 @@ export class Visibility extends Plumber {
|
|
|
62
63
|
if (visible) target.classList.remove(hiddenClass);
|
|
63
64
|
else target.classList.add(hiddenClass);
|
|
64
65
|
} else {
|
|
65
|
-
|
|
66
|
-
else target.setAttribute('hidden', true);
|
|
66
|
+
setHidden(target, !visible);
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -72,7 +72,7 @@ export class Visibility extends Plumber {
|
|
|
72
72
|
* @param {boolean} isExpanded - True to mark as expanded, false to mark as collapsed
|
|
73
73
|
*/
|
|
74
74
|
activate(isExpanded) {
|
|
75
|
-
if (this.activator) this.activator
|
|
75
|
+
if (this.activator) setExpanded(this.activator, isExpanded);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/**
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { Controller } from '@hotwired/stimulus';
|
|
2
|
-
import { tryParseDate } from '../plumbers/plumber/date';
|
|
3
|
-
|
|
4
|
-
export default class extends Controller {
|
|
5
|
-
onSelect(event) {
|
|
6
|
-
if (!(event.target instanceof HTMLElement)) return;
|
|
7
|
-
|
|
8
|
-
event.preventDefault();
|
|
9
|
-
const input = event.target instanceof HTMLTimeElement ? event.target.parentElement : event.target;
|
|
10
|
-
if (input.disabled || input.getAttribute('aria-disabled') === 'true') return;
|
|
11
|
-
|
|
12
|
-
this.dispatch('selecting', { target: input });
|
|
13
|
-
const time = event.target instanceof HTMLTimeElement ? event.target : event.target.querySelector('time');
|
|
14
|
-
if (!time) return console.error(`unable to locate time element within ${input}`);
|
|
15
|
-
|
|
16
|
-
const date = tryParseDate(time.dateTime);
|
|
17
|
-
if (!date) return console.error(`unable to parse ${time.dateTime} found within the time element`);
|
|
18
|
-
|
|
19
|
-
this.select(date.toISOString());
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
select(iso) {
|
|
23
|
-
const date = tryParseDate(iso);
|
|
24
|
-
if (!date) return;
|
|
25
|
-
this.dispatch('selected', { detail: { epoch: date.getTime(), iso } });
|
|
26
|
-
}
|
|
27
|
-
}
|