@stimulus-plumbers/controllers 0.3.2 → 0.4.0

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.
@@ -5,27 +5,27 @@ import { attachFormatter } from '../plumbers';
5
5
  export default class extends Controller {
6
6
  static targets = ['input', 'toggle'];
7
7
  static values = {
8
- type: { type: String, default: 'plain' },
8
+ format: { type: String, default: 'plain' },
9
9
  options: { type: Object, default: {} },
10
10
  revealed: { type: Boolean, default: false },
11
11
  };
12
12
 
13
13
  connect() {
14
- attachFormatter(this, { type: this.typeValue, options: this.optionsValue });
14
+ attachFormatter(this, { type: this.formatValue, options: this.optionsValue });
15
15
  this.format(this.readValue());
16
16
  this.drawToggle();
17
17
  }
18
18
 
19
- typeValueChanged() {
19
+ formatValueChanged() {
20
20
  if (!this.formatter) return;
21
- attachFormatter(this, { type: this.typeValue, options: this.optionsValue });
21
+ attachFormatter(this, { type: this.formatValue, options: this.optionsValue });
22
22
  this.format(this.readValue());
23
23
  this.drawToggle();
24
24
  }
25
25
 
26
26
  optionsValueChanged() {
27
27
  if (!this.formatter) return;
28
- attachFormatter(this, { type: this.typeValue, options: this.optionsValue });
28
+ attachFormatter(this, { type: this.formatValue, options: this.optionsValue });
29
29
  this.format(this.readValue());
30
30
  }
31
31
 
@@ -45,7 +45,7 @@ export default class extends Controller {
45
45
  }
46
46
 
47
47
  toggle() {
48
- if (!this.formatter.maskable() && this.typeValue !== 'password') return;
48
+ if (!this.formatter.maskable() && this.formatValue !== 'password') return;
49
49
  this.revealedValue = !this.revealedValue;
50
50
  }
51
51
 
@@ -59,7 +59,7 @@ export default class extends Controller {
59
59
 
60
60
  drawToggle() {
61
61
  if (!this.hasToggleTarget) return;
62
- const hasToggleBehavior = this.formatter?.maskable() || this.typeValue === 'password';
62
+ const hasToggleBehavior = this.formatter?.maskable() || this.formatValue === 'password';
63
63
  this.toggleTarget.hidden = !hasToggleBehavior;
64
64
  if (hasToggleBehavior) setPressed(this.toggleTarget, this.revealedValue);
65
65
  }
@@ -72,7 +72,7 @@ export default class extends Controller {
72
72
  onFormatting(raw) {
73
73
  if (!this.formatter) return;
74
74
 
75
- if (this.typeValue === 'password') {
75
+ if (this.formatValue === 'password') {
76
76
  if (this.hasInputTarget) this.inputTarget.type = this.revealedValue ? 'text' : 'password';
77
77
  return;
78
78
  }
@@ -1,47 +1,69 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
- import { attachContentLoader, attachVisibility } from '../plumbers';
2
+ import { focusFirst } from '../accessibility/focus';
3
+ import { attachContentLoader, attachDismisser, attachVisibility } from '../plumbers';
3
4
 
4
5
  export default class extends Controller {
5
- static targets = ['content', 'template', 'loader', 'activator'];
6
+ static targets = ['trigger', 'panel', 'template', 'loader'];
6
7
  static classes = ['hidden'];
7
8
  static values = {
8
9
  url: String,
9
10
  loadedAt: String,
10
11
  reload: { type: String, default: 'never' },
11
12
  staleAfter: { type: Number, default: 3600 },
13
+ closeOnSelect: { type: Boolean, default: true },
12
14
  };
13
15
 
14
16
  connect() {
15
17
  attachContentLoader(this, {
16
- element: this.hasContentTarget ? this.contentTarget : null,
18
+ element: this.hasPanelTarget ? this.panelTarget : null,
17
19
  url: this.hasUrlValue ? this.urlValue : null,
18
20
  });
19
21
 
20
- if (this.hasContentTarget) {
22
+ if (this.hasPanelTarget) {
21
23
  attachVisibility(this, {
22
- element: this.contentTarget,
23
- activator: this.hasActivatorTarget ? this.activatorTarget : null,
24
+ element: this.panelTarget,
25
+ activator: this.hasTriggerTarget ? this.triggerTarget : null,
24
26
  });
27
+ attachDismisser(this);
25
28
  }
26
29
  if (this.hasLoaderTarget)
27
30
  attachVisibility(this, { element: this.loaderTarget, visibility: 'contentLoaderVisibility' });
28
31
  }
29
32
 
30
- async show() {
33
+ async dismissed() {
34
+ await this.close();
35
+ }
36
+
37
+ async open() {
38
+ if (!this.hasPanelTarget) return;
31
39
  await this.visibility.show();
32
40
  }
33
41
 
34
- async hide() {
42
+ async close() {
43
+ if (!this.hasPanelTarget) return;
35
44
  await this.visibility.hide();
36
45
  }
37
46
 
47
+ async toggle() {
48
+ this.visibility?.visible ? await this.close() : await this.open();
49
+ }
50
+
51
+ async closeOnSelect() {
52
+ if (this.closeOnSelectValue) await this.close();
53
+ }
54
+
38
55
  async shown() {
39
56
  await this.load();
57
+ if (this.hasPanelTarget) focusFirst(this.panelTarget);
58
+ }
59
+
60
+ async hidden() {
61
+ if (this.hasTriggerTarget) this.triggerTarget.focus();
40
62
  }
41
63
 
42
64
  canLoad() {
43
- if (this.hasContentTarget && this.contentTarget.tagName.toLowerCase() === 'turbo-frame') {
44
- if (this.hasUrlValue) this.contentTarget.setAttribute('src', this.urlValue);
65
+ if (this.hasPanelTarget && this.panelTarget.tagName.toLowerCase() === 'turbo-frame') {
66
+ if (this.hasUrlValue) this.panelTarget.setAttribute('src', this.urlValue);
45
67
  return false;
46
68
  }
47
69
  return true;
@@ -52,8 +74,8 @@ export default class extends Controller {
52
74
  }
53
75
 
54
76
  async contentLoaded({ content }) {
55
- if (this.hasContentTarget) {
56
- this.contentTarget.replaceChildren(this.getContentNode(content));
77
+ if (this.hasPanelTarget) {
78
+ this.panelTarget.replaceChildren(this.getContentNode(content));
57
79
  }
58
80
  if (this.hasLoaderTarget) await this.contentLoaderVisibility.hide();
59
81
  }
package/src/index.js CHANGED
@@ -18,7 +18,7 @@ export { Formatter, FORMATTER_TYPES } from './plumbers/formatter.js';
18
18
 
19
19
  // Export Stimulus controllers
20
20
  export { default as CalendarMonthController } from './controllers/calendar_month_controller.js';
21
- export { default as CalendarMonthObserverController } from './controllers/calendar_month_observer_controller.js';
21
+ export { default as CalendarObserverController } from './controllers/calendar_observer_controller.js';
22
22
  export { default as ClipboardController } from './controllers/clipboard_controller.js';
23
23
  export { default as ComboboxDateController } from './controllers/combobox_date_controller.js';
24
24
  export { default as ComboboxDropdownController } from './controllers/combobox_dropdown_controller.js';
@@ -26,8 +26,8 @@ export { default as ComboboxTimeController } from './controllers/combobox_time_c
26
26
  export { default as DismisserController } from './controllers/dismisser_controller.js';
27
27
  export { default as FlipperController } from './controllers/flipper_controller.js';
28
28
  export { default as InputComboboxController } from './controllers/input_combobox_controller.js';
29
- export { default as InputFormatController } from './controllers/input_format_controller.js';
30
- export { default as InputSearchController } from './controllers/input_search_controller.js';
29
+ export { default as InputFormatterController } from './controllers/input_formatter_controller.js';
30
+ export { default as InputClearableController } from './controllers/input_clearable_controller.js';
31
31
  export { default as ModalController } from './controllers/modal_controller.js';
32
32
  export { default as PannerController } from './controllers/panner_controller.js';
33
33
  export { default as PopoverController } from './controllers/popover_controller.js';
@@ -77,6 +77,7 @@ export class Calendar extends Plumber {
77
77
  this.daysOfWeek = this.buildDaysOfWeek();
78
78
  this.daysOfMonth = this.buildDaysOfMonth();
79
79
  this.monthsOfYear = this.buildMonthsOfYear();
80
+ this.yearsOfDecade = this.buildYearsOfDecade();
80
81
  }
81
82
 
82
83
  /**
@@ -162,6 +163,24 @@ export class Calendar extends Plumber {
162
163
  return monthsOfYear;
163
164
  }
164
165
 
166
+ /**
167
+ * Builds array of year objects spanning the decade containing the current year.
168
+ * Returns 12 entries: decade start - 1 (buffer) through decade start + 10 (buffer).
169
+ * @returns {Array<Object>} Array of year objects
170
+ */
171
+ buildYearsOfDecade() {
172
+ const decadeStart = Math.floor(this.year / 10) * 10;
173
+ const yearsOfDecade = [];
174
+ for (let i = decadeStart - 1; i <= decadeStart + 10; i++) {
175
+ yearsOfDecade.push({
176
+ value: i,
177
+ current: i === this.year,
178
+ outside: i < decadeStart || i > decadeStart + 9,
179
+ });
180
+ }
181
+ return yearsOfDecade;
182
+ }
183
+
165
184
  /**
166
185
  * Gets the current "today" reference date.
167
186
  * @returns {Date} Today's date
@@ -177,11 +196,11 @@ export class Calendar extends Plumber {
177
196
  set today(value) {
178
197
  if (!isValidDate(value)) return;
179
198
 
180
- const month = this.month ? this.month : value.getMonth();
181
- const year = this.year ? this.year : value.getFullYear();
199
+ const month = this.month ?? value.getMonth();
200
+ const year = this.year ?? value.getFullYear();
182
201
  const sameMonthYear = month == value.getMonth() && year == value.getFullYear();
183
- const day = this.hasDayValue ? this.day : sameMonthYear ? value.getDate() : 1;
184
- this.now = new Date(year, month, day).toISOString();
202
+ const day = sameMonthYear ? value.getDate() : 1;
203
+ this.now = new Date(year, month, day);
185
204
  }
186
205
 
187
206
  /**
@@ -380,6 +399,9 @@ export class Calendar extends Plumber {
380
399
  get monthsOfYear() {
381
400
  return context.monthsOfYear;
382
401
  },
402
+ get yearsOfDecade() {
403
+ return context.yearsOfDecade;
404
+ },
383
405
  navigate: async (to) => await context.navigate(to),
384
406
  step: async (type, value) => await context.step(type, value),
385
407
  isDisabled: (date) => context.isDisabled(date),
@@ -1,3 +1,5 @@
1
+ const DATE_ONLY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
2
+
1
3
  export function isValidDate(value) {
2
4
  return value instanceof Date && !isNaN(value);
3
5
  }
@@ -5,8 +7,17 @@ export function isValidDate(value) {
5
7
  export function tryParseDate(...values) {
6
8
  if (values.length === 0) throw 'Missing values to parse as date';
7
9
  if (values.length === 1) {
8
- const parsed = new Date(values[0]);
9
- if (values[0] && isValidDate(parsed)) return parsed;
10
+ const dateValue = values[0];
11
+ if (!dateValue) return undefined;
12
+ if (typeof dateValue === 'string' && DATE_ONLY_PATTERN.test(dateValue)) {
13
+ // YYYY-MM-DD strings are UTC by spec; use local constructor instead
14
+ const [year, month, day] = dateValue.split('-').map(Number);
15
+ const parsed = new Date(year, month - 1, day);
16
+ if (isValidDate(parsed)) return parsed;
17
+ } else {
18
+ const parsed = new Date(dateValue);
19
+ if (isValidDate(parsed)) return parsed;
20
+ }
10
21
  } else {
11
22
  const parsed = new Date(...values);
12
23
  if (isValidDate(parsed)) return parsed;