@stimulus-plumbers/controllers 0.4.0 → 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.
@@ -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);
@@ -1,7 +1,9 @@
1
1
  import Plumber from './plumber';
2
2
  import { isValidDate, tryParseDate } from './plumber/date';
3
3
 
4
- const DAYS_OF_WEEK = 7;
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'],
@@ -131,8 +133,8 @@ export class Calendar extends Plumber {
131
133
  const current = new Date(currentYear, currentMonth, i);
132
134
  daysOfMonth.push(parseDate(current));
133
135
  }
134
- const mod = daysOfMonth.length % DAYS_OF_WEEK;
135
- const trailing = mod === 0 ? 0 : DAYS_OF_WEEK - mod;
136
+ const mod = daysOfMonth.length % DAYS_PER_ROW;
137
+ const trailing = mod === 0 ? 0 : DAYS_PER_ROW - mod;
136
138
  for (let i = 1; i <= trailing; i++) {
137
139
  const next = new Date(currentYear, currentMonth + 1, i);
138
140
  daysOfMonth.push(parseDate(next));
@@ -150,14 +152,21 @@ export class Calendar extends Plumber {
150
152
  const numericFormatter = new Intl.DateTimeFormat(this.localesValue, { month: 'numeric' });
151
153
 
152
154
  const monthsOfYear = [];
153
- for (let i = 0; i < 12; i++) {
154
- const month = new Date(this.year, i);
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);
155
163
  monthsOfYear.push({
156
- date: month,
157
- value: month.getMonth(),
158
- long: longFormatter.format(month),
159
- short: shortFormatter.format(month),
160
- numeric: numericFormatter.format(month),
164
+ date: date,
165
+ value: date.getMonth(),
166
+ long: longName,
167
+ short: shortName,
168
+ numeric: numericFormatter.format(date),
169
+ disabled: rangeDisabled || listDisabled,
161
170
  });
162
171
  }
163
172
  return monthsOfYear;
@@ -169,13 +178,16 @@ export class Calendar extends Plumber {
169
178
  * @returns {Array<Object>} Array of year objects
170
179
  */
171
180
  buildYearsOfDecade() {
172
- const decadeStart = Math.floor(this.year / 10) * 10;
181
+ const decadeStart = Math.floor(this.year / DECADE_SIZE) * DECADE_SIZE;
173
182
  const yearsOfDecade = [];
174
- for (let i = decadeStart - 1; i <= decadeStart + 10; i++) {
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);
175
187
  yearsOfDecade.push({
176
188
  value: i,
177
189
  current: i === this.year,
178
- outside: i < decadeStart || i > decadeStart + 9,
190
+ disabled: bufferYear || rangeDisabled || listDisabled,
179
191
  });
180
192
  }
181
193
  return yearsOfDecade;
@@ -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
- if (visible) target.removeAttribute('hidden');
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.setAttribute('aria-expanded', isExpanded ? 'true' : 'false');
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
- }