esseal-date-picker 1.0.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.
Files changed (3) hide show
  1. package/README.md +413 -0
  2. package/index.js +375 -0
  3. package/package.json +16 -0
package/README.md ADDED
@@ -0,0 +1,413 @@
1
+ # EssealDatePicker
2
+
3
+ A lightweight, dependency-free date picker that works seamlessly with vanilla HTML, React, Vue, Angular, and other JavaScript frameworks.
4
+
5
+ ## ✨ Features
6
+
7
+ - **Zero Dependencies** - Pure JavaScript, no external libraries required
8
+ - **Framework Agnostic** - Works with React, Vue, Angular, and vanilla JS
9
+ - **React-Safe** - Properly triggers React's synthetic event system
10
+ - **Lightweight** - ~8KB minified
11
+ - **Range Selection** - Support for both single date and date range selection
12
+ - **Customizable** - Easy theming with color options
13
+ - **Accessible** - ARIA labels and keyboard support
14
+ - **Localized** - Automatic locale detection for date formatting
15
+
16
+ ## 📦 Installation
17
+
18
+ ```bash
19
+ npm install esseal-date-picker
20
+ ```
21
+
22
+ Or use via CDN:
23
+
24
+ ```html
25
+ <script type="module">
26
+ import EssealDatePicker from "https://cdn.jsdelivr.net/npm/esseal-date-picker/dist/esseal-date-picker.esm.js";
27
+ </script>
28
+ ```
29
+
30
+ ## 🚀 Quick Start
31
+
32
+ ### Vanilla HTML/JavaScript
33
+
34
+ ```html
35
+ <!DOCTYPE html>
36
+ <html>
37
+ <head>
38
+ <title>Date Picker Demo</title>
39
+ </head>
40
+ <body>
41
+ <input type="text" id="myDatePicker" placeholder="Select a date" />
42
+
43
+ <script type="module">
44
+ import EssealDatePicker from "./esseal-date-picker.js";
45
+
46
+ new EssealDatePicker("#myDatePicker", {
47
+ onChange: (date) => {
48
+ console.log("Selected date:", date);
49
+ },
50
+ });
51
+ </script>
52
+ </body>
53
+ </html>
54
+ ```
55
+
56
+ ### React
57
+
58
+ ```jsx
59
+ import { useEffect, useRef } from "react";
60
+ import EssealDatePicker from "esseal-date-picker";
61
+
62
+ function DatePickerComponent() {
63
+ const inputRef = useRef(null);
64
+ const pickerRef = useRef(null);
65
+
66
+ useEffect(() => {
67
+ // Initialize the date picker
68
+ pickerRef.current = new EssealDatePicker(inputRef.current, {
69
+ primaryColor: "#3b82f6",
70
+ onChange: (date) => {
71
+ console.log("Selected:", date);
72
+ },
73
+ });
74
+
75
+ // Cleanup on unmount
76
+ return () => {
77
+ if (pickerRef.current) {
78
+ pickerRef.current.destroy();
79
+ }
80
+ };
81
+ }, []);
82
+
83
+ return (
84
+ <div>
85
+ <label htmlFor="date-picker">Select Date:</label>
86
+ <input
87
+ ref={inputRef}
88
+ type="text"
89
+ id="date-picker"
90
+ placeholder="Click to select date"
91
+ readOnly
92
+ />
93
+ </div>
94
+ );
95
+ }
96
+
97
+ export default DatePickerComponent;
98
+ ```
99
+
100
+ ### Vue 3
101
+
102
+ ```vue
103
+ <template>
104
+ <div>
105
+ <label for="date-picker">Select Date:</label>
106
+ <input
107
+ ref="dateInput"
108
+ type="text"
109
+ id="date-picker"
110
+ placeholder="Click to select date"
111
+ readonly
112
+ />
113
+ </div>
114
+ </template>
115
+
116
+ <script setup>
117
+ import { ref, onMounted, onUnmounted } from "vue";
118
+ import EssealDatePicker from "esseal-date-picker";
119
+
120
+ const dateInput = ref(null);
121
+ let picker = null;
122
+
123
+ onMounted(() => {
124
+ picker = new EssealDatePicker(dateInput.value, {
125
+ onChange: (date) => {
126
+ console.log("Selected:", date);
127
+ },
128
+ });
129
+ });
130
+
131
+ onUnmounted(() => {
132
+ if (picker) {
133
+ picker.destroy();
134
+ }
135
+ });
136
+ </script>
137
+ ```
138
+
139
+ ### Angular
140
+
141
+ ```typescript
142
+ import {
143
+ Component,
144
+ ElementRef,
145
+ ViewChild,
146
+ OnInit,
147
+ OnDestroy,
148
+ } from "@angular/core";
149
+ import EssealDatePicker from "esseal-date-picker";
150
+
151
+ @Component({
152
+ selector: "app-date-picker",
153
+ template: `
154
+ <div>
155
+ <label for="date-picker">Select Date:</label>
156
+ <input
157
+ #dateInput
158
+ type="text"
159
+ id="date-picker"
160
+ placeholder="Click to select date"
161
+ readonly
162
+ />
163
+ </div>
164
+ `,
165
+ })
166
+ export class DatePickerComponent implements OnInit, OnDestroy {
167
+ @ViewChild("dateInput", { static: true }) dateInput!: ElementRef;
168
+ private picker: any;
169
+
170
+ ngOnInit() {
171
+ this.picker = new EssealDatePicker(this.dateInput.nativeElement, {
172
+ onChange: (date: Date) => {
173
+ console.log("Selected:", date);
174
+ },
175
+ });
176
+ }
177
+
178
+ ngOnDestroy() {
179
+ if (this.picker) {
180
+ this.picker.destroy();
181
+ }
182
+ }
183
+ }
184
+ ```
185
+
186
+ ## 📖 API Reference
187
+
188
+ ### Constructor
189
+
190
+ ```javascript
191
+ new EssealDatePicker(target, options);
192
+ ```
193
+
194
+ **Parameters:**
195
+
196
+ - `target` (string | HTMLInputElement) - CSS selector or DOM element
197
+ - `options` (object) - Configuration options
198
+
199
+ ### Options
200
+
201
+ | Option | Type | Default | Description |
202
+ | -------------- | ---------------------------- | -------------------------------------------- | --------------------------------------------------- |
203
+ | `mode` | `'single'` \| `'range'` | `'single'` | Selection mode |
204
+ | `locale` | `string` | `navigator.language` | Locale for date formatting (e.g., 'en-US', 'fr-FR') |
205
+ | `minDate` | `Date` \| `string` \| `null` | `null` | Minimum selectable date |
206
+ | `maxDate` | `Date` \| `string` \| `null` | `null` | Maximum selectable date |
207
+ | `primaryColor` | `string` | `'#3b82f6'` | Primary color for selections (must be hex format) |
208
+ | `textColor` | `string` | `'#1f2937'` | Text color for the calendar |
209
+ | `zIndex` | `number` | `9999` | Z-index for the calendar popup |
210
+ | `format` | `function` | `(date) => date.toLocaleDateString('en-CA')` | Custom date formatter function |
211
+ | `onChange` | `function` \| `null` | `null` | Callback when date is selected |
212
+
213
+ ### Methods
214
+
215
+ #### `open()`
216
+
217
+ Opens the date picker calendar.
218
+
219
+ ```javascript
220
+ picker.open();
221
+ ```
222
+
223
+ #### `close()`
224
+
225
+ Closes the date picker calendar.
226
+
227
+ ```javascript
228
+ picker.close();
229
+ ```
230
+
231
+ #### `destroy()`
232
+
233
+ Removes all event listeners and DOM elements. **Always call this when removing the picker** (especially important in React/Vue/Angular).
234
+
235
+ ```javascript
236
+ picker.destroy();
237
+ ```
238
+
239
+ ## 🎨 Customization Examples
240
+
241
+ ### Custom Color Theme
242
+
243
+ ```javascript
244
+ new EssealDatePicker("#date-picker", {
245
+ primaryColor: "#10b981", // Green theme
246
+ textColor: "#374151",
247
+ });
248
+ ```
249
+
250
+ ### Date Range Selection
251
+
252
+ ```javascript
253
+ new EssealDatePicker("#dateRange", {
254
+ mode: "range",
255
+ onChange: (range) => {
256
+ console.log("Start:", range.start);
257
+ console.log("End:", range.end);
258
+ },
259
+ });
260
+ ```
261
+
262
+ ### Date Restrictions
263
+
264
+ ```javascript
265
+ new EssealDatePicker("#date-picker", {
266
+ minDate: new Date(2024, 0, 1), // January 1, 2024
267
+ maxDate: new Date(2024, 11, 31), // December 31, 2024
268
+ onChange: (date) => {
269
+ console.log("Selected date within 2024:", date);
270
+ },
271
+ });
272
+ ```
273
+
274
+ ### Custom Date Format
275
+
276
+ ```javascript
277
+ new EssealDatePicker("#date-picker", {
278
+ format: (date) => {
279
+ const day = date.getDate().toString().padStart(2, "0");
280
+ const month = (date.getMonth() + 1).toString().padStart(2, "0");
281
+ const year = date.getFullYear();
282
+ return `${month}/${day}/${year}`; // MM/DD/YYYY
283
+ },
284
+ });
285
+ ```
286
+
287
+ ### Localized Calendar
288
+
289
+ ```javascript
290
+ new EssealDatePicker("#date-picker", {
291
+ locale: "fr-FR", // French locale
292
+ format: (date) => date.toLocaleDateString("fr-FR"),
293
+ });
294
+ ```
295
+
296
+ ## 🔧 Advanced Usage
297
+
298
+ ### Multiple Pickers on One Page
299
+
300
+ ```javascript
301
+ // Each instance is independent
302
+ const picker1 = new EssealDatePicker("#date1", {
303
+ primaryColor: "#3b82f6",
304
+ });
305
+
306
+ const picker2 = new EssealDatePicker("#date2", {
307
+ primaryColor: "#ef4444",
308
+ });
309
+
310
+ const picker3 = new EssealDatePicker("#date3", {
311
+ mode: "range",
312
+ primaryColor: "#10b981",
313
+ });
314
+ ```
315
+
316
+ ### Programmatic Control
317
+
318
+ ```javascript
319
+ const picker = new EssealDatePicker("#date-picker");
320
+
321
+ // Open programmatically
322
+ document.querySelector("#openBtn").addEventListener("click", () => {
323
+ picker.open();
324
+ });
325
+
326
+ // Close programmatically
327
+ document.querySelector("#closeBtn").addEventListener("click", () => {
328
+ picker.close();
329
+ });
330
+ ```
331
+
332
+ ### Working with Forms
333
+
334
+ ```javascript
335
+ const form = document.querySelector("#myForm");
336
+ const picker = new EssealDatePicker("#dateInput", {
337
+ onChange: (date) => {
338
+ // The input value is automatically updated
339
+ // Form submission will include the formatted date
340
+ console.log("Form will submit:", form.dateInput.value);
341
+ },
342
+ });
343
+
344
+ form.addEventListener("submit", (e) => {
345
+ e.preventDefault();
346
+ const formData = new FormData(form);
347
+ console.log("Date submitted:", formData.get("dateInput"));
348
+ });
349
+ ```
350
+
351
+ ## ⚠️ Important Notes
352
+
353
+ ### Color Format
354
+
355
+ The `primaryColor` option **must be in hexadecimal format** (`#RRGGBB` or `#RGB`). RGB and HSL formats are not currently supported.
356
+
357
+ ✅ Valid:
358
+
359
+ - `#3b82f6`
360
+ - `#f00`
361
+ - `#FF5733`
362
+
363
+ ❌ Invalid:
364
+
365
+ - `rgb(59, 130, 246)`
366
+ - `hsl(217, 91%, 60%)`
367
+
368
+ ### React Integration
369
+
370
+ Always use the `destroy()` method in cleanup functions to prevent memory leaks:
371
+
372
+ ```jsx
373
+ useEffect(() => {
374
+ const picker = new EssealDatePicker(inputRef.current);
375
+
376
+ return () => picker.destroy(); // ✅ Critical for preventing memory leaks
377
+ }, []);
378
+ ```
379
+
380
+ ### Read-Only Input Recommendation
381
+
382
+ It's recommended to set the input as `readonly` to prevent manual typing:
383
+
384
+ ```html
385
+ <input type="text" id="date-picker" readonly />
386
+ ```
387
+
388
+ ## 🎯 Browser Support
389
+
390
+ - Chrome/Edge (latest)
391
+ - Firefox (latest)
392
+ - Safari (latest)
393
+ - Opera (latest)
394
+
395
+ **IE11 is not supported** (uses modern JavaScript features like `classList`, `dataset`, etc.)
396
+
397
+ ## 📄 License
398
+
399
+ MIT License - feel free to use in personal and commercial projects.
400
+
401
+ ## 📝 Changelog
402
+
403
+ ### v1.0.0
404
+
405
+ - Complete rewrite with React-safe event handling
406
+ - Added range selection mode
407
+ - Improved performance with DocumentFragment rendering
408
+ - Better accessibility support
409
+ - TypeScript version (coming soon)
410
+
411
+ ---
412
+
413
+ **Made with ❤️ for developers who need a simple, reliable date picker**
package/index.js ADDED
@@ -0,0 +1,375 @@
1
+ /**
2
+ * EssealDatePicker v2.0.0
3
+ * A dependency-free, React-safe date picker.
4
+ */
5
+ class EssealDatePicker {
6
+ constructor(target, options = {}) {
7
+ this.input = typeof target === 'string' ? document.querySelector(target) : target;
8
+ if (!this.input) throw new Error('EssealDatePicker: Target input not found.');
9
+
10
+ this.options = {
11
+ mode: 'single',
12
+ locale: navigator.language || 'en-US',
13
+ minDate: null,
14
+ maxDate: null,
15
+ primaryColor: '#3b82f6',
16
+ textColor: '#1f2937',
17
+ zIndex: 9999,
18
+ format: (date) => date.toLocaleDateString('en-CA'),
19
+ onChange: null,
20
+ ...options,
21
+ };
22
+
23
+ this.state = {
24
+ viewDate: new Date(),
25
+ selectedDate: null,
26
+ rangeStart: null,
27
+ rangeEnd: null,
28
+ isVisible: false,
29
+ view: 'day',
30
+ };
31
+
32
+ if (this.options.minDate) this.options.minDate = this._normalizeDate(this.options.minDate);
33
+ if (this.options.maxDate) this.options.maxDate = this._normalizeDate(this.options.maxDate);
34
+
35
+ this._handleInputClick = this._handleInputClick.bind(this);
36
+ this._handleDocumentClick = this._handleDocumentClick.bind(this);
37
+ this._handleResize = this._handleResize.bind(this);
38
+
39
+ this._init();
40
+ }
41
+
42
+ _init() {
43
+ this._injectStyles();
44
+ this._createDOM();
45
+ this._attachListeners();
46
+ }
47
+
48
+ _injectStyles() {
49
+ const styleId = 'esseal-datepicker-styles';
50
+ if (document.getElementById(styleId)) return;
51
+
52
+ const css = `
53
+ .dp-container {
54
+ position: fixed;
55
+ background: #fff;
56
+ border: 1px solid #e5e7eb;
57
+ border-radius: 8px;
58
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
59
+ font-family: system-ui, -apple-system, sans-serif;
60
+ width: 280px;
61
+ padding: 16px;
62
+ display: none;
63
+ z-index: ${this.options.zIndex};
64
+ color: ${this.options.textColor};
65
+ user-select: none;
66
+ }
67
+ .dp-container.dp-visible { display: block; }
68
+ .dp-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
69
+ .dp-nav-btn { background: none; border: none; cursor: pointer; padding: 4px; border-radius: 4px; color: inherit; }
70
+ .dp-nav-btn:hover { background: #f3f4f6; }
71
+ .dp-title { font-weight: 600; cursor: pointer; padding: 4px 8px; border-radius: 4px; }
72
+ .dp-title:hover { background: #f3f4f6; }
73
+ .dp-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
74
+ .dp-grid-wide { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
75
+ .dp-cell {
76
+ height: 36px; display: flex; align-items: center; justify-content: center;
77
+ font-size: 0.875rem; cursor: pointer; border-radius: 4px;
78
+ }
79
+ .dp-label { font-size: 0.75rem; font-weight: 500; color: #9ca3af; cursor: default; }
80
+ .dp-cell:not(.dp-label):not(.dp-disabled):hover { background-color: #f3f4f6; }
81
+ .dp-other-month { color: #d1d5db; }
82
+ .dp-disabled { opacity: 0.3; cursor: not-allowed; text-decoration: line-through; }
83
+ .dp-selected, .dp-range-start, .dp-range-end { color: #fff !important; }
84
+ .dp-in-range { border-radius: 0; }
85
+ .dp-today { border: 1px solid ${this.options.primaryColor}; }
86
+ `;
87
+
88
+ const style = document.createElement('style');
89
+ style.id = styleId;
90
+ style.textContent = css;
91
+ document.head.appendChild(style);
92
+ }
93
+
94
+ _createDOM() {
95
+ this.root = document.createElement('div');
96
+ this.root.className = 'dp-container';
97
+ this.root.setAttribute('role', 'dialog');
98
+
99
+ // Construct Header
100
+ const header = document.createElement('div');
101
+ header.className = 'dp-header';
102
+
103
+ const prevBtn = document.createElement('button');
104
+ prevBtn.className = 'dp-nav-btn';
105
+ prevBtn.dataset.action = 'prev';
106
+ prevBtn.innerHTML = '&lt;';
107
+
108
+ const title = document.createElement('span');
109
+ title.className = 'dp-title';
110
+ title.dataset.action = 'switch-view';
111
+
112
+ const nextBtn = document.createElement('button');
113
+ nextBtn.className = 'dp-nav-btn';
114
+ nextBtn.dataset.action = 'next';
115
+ nextBtn.innerHTML = '&gt;';
116
+
117
+ header.append(prevBtn, title, nextBtn);
118
+
119
+ const body = document.createElement('div');
120
+ body.className = 'dp-body';
121
+
122
+ this.root.append(header, body);
123
+ document.body.appendChild(this.root);
124
+
125
+ // Event Delegation
126
+ this.root.addEventListener('click', (e) => {
127
+ e.stopPropagation();
128
+ const target = e.target.closest('[data-action]') || e.target.closest('.dp-cell');
129
+ if (!target) return;
130
+
131
+ if (target.dataset.action) {
132
+ this._handleNavigation(target.dataset.action);
133
+ } else if (target.classList.contains('dp-cell') && !target.classList.contains('dp-disabled') && !target.classList.contains('dp-label')) {
134
+ this._handleSelection(target);
135
+ }
136
+ });
137
+ }
138
+
139
+ _attachListeners() {
140
+ this.input.addEventListener('click', this._handleInputClick);
141
+ this.input.addEventListener('focus', this._handleInputClick);
142
+ document.addEventListener('click', this._handleDocumentClick);
143
+ window.addEventListener('resize', this._handleResize);
144
+ window.addEventListener('scroll', this._handleResize, true);
145
+ }
146
+
147
+ destroy() {
148
+ this.root.remove();
149
+ this.input.removeEventListener('click', this._handleInputClick);
150
+ this.input.removeEventListener('focus', this._handleInputClick);
151
+ document.removeEventListener('click', this._handleDocumentClick);
152
+ window.removeEventListener('resize', this._handleResize);
153
+ window.removeEventListener('scroll', this._handleResize, true);
154
+ }
155
+
156
+ /* ================= Rendering ================= */
157
+
158
+ _render() {
159
+ const body = this.root.querySelector('.dp-body');
160
+ const title = this.root.querySelector('.dp-title');
161
+ body.replaceChildren(); // Safe and fast clearing
162
+
163
+ if (this.state.view === 'day') this._renderDays(body, title);
164
+ else if (this.state.view === 'month') this._renderMonths(body, title);
165
+ else this._renderYears(body, title);
166
+ }
167
+
168
+ _renderDays(container, titleEl) {
169
+ container.className = 'dp-body dp-grid';
170
+ const year = this.state.viewDate.getFullYear();
171
+ const month = this.state.viewDate.getMonth();
172
+ titleEl.textContent = this.state.viewDate.toLocaleString(this.options.locale, { month: 'long', year: 'numeric' });
173
+
174
+ const frag = document.createDocumentFragment();
175
+
176
+ ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].forEach(d => {
177
+ const el = document.createElement('div');
178
+ el.className = 'dp-cell dp-label';
179
+ el.textContent = d;
180
+ frag.appendChild(el);
181
+ });
182
+
183
+ const firstDay = new Date(year, month, 1).getDay();
184
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
185
+
186
+ for (let i = 0; i < firstDay; i++) {
187
+ const el = document.createElement('div');
188
+ el.className = 'dp-cell dp-other-month';
189
+ frag.appendChild(el);
190
+ }
191
+
192
+ for (let i = 1; i <= daysInMonth; i++) {
193
+ const date = new Date(year, month, i);
194
+ const ts = date.getTime();
195
+ const el = document.createElement('div');
196
+ el.className = 'dp-cell';
197
+ el.dataset.ts = ts;
198
+ el.textContent = i;
199
+
200
+ if ((this.options.minDate && ts < this.options.minDate.getTime()) ||
201
+ (this.options.maxDate && ts > this.options.maxDate.getTime())) {
202
+ el.classList.add('dp-disabled');
203
+ } else {
204
+ if (this.options.mode === 'single' && this.state.selectedDate && ts === this.state.selectedDate.getTime()) {
205
+ el.classList.add('dp-selected');
206
+ el.style.background = this.options.primaryColor;
207
+ }
208
+ if (this.options.mode === 'range' && this.state.rangeStart) {
209
+ const startTs = this.state.rangeStart.getTime();
210
+ if (ts === startTs) {
211
+ el.classList.add('dp-range-start');
212
+ el.style.background = this.options.primaryColor;
213
+ }
214
+ if (this.state.rangeEnd) {
215
+ const endTs = this.state.rangeEnd.getTime();
216
+ if (ts === endTs) {
217
+ el.classList.add('dp-range-end');
218
+ el.style.background = this.options.primaryColor;
219
+ }
220
+ if (ts > startTs && ts < endTs) {
221
+ el.classList.add('dp-in-range');
222
+ el.style.background = `${this.options.primaryColor}20`;
223
+ }
224
+ }
225
+ }
226
+ }
227
+ frag.appendChild(el);
228
+ }
229
+ container.appendChild(frag);
230
+ }
231
+
232
+ _renderMonths(container, titleEl) {
233
+ container.className = 'dp-body dp-grid-wide';
234
+ titleEl.textContent = this.state.viewDate.getFullYear();
235
+ const currentMonth = new Date().getMonth();
236
+ const currentYear = new Date().getFullYear();
237
+ const frag = document.createDocumentFragment();
238
+
239
+ for (let i = 0; i < 12; i++) {
240
+ const date = new Date(this.state.viewDate.getFullYear(), i, 1);
241
+ const el = document.createElement('div');
242
+ el.className = 'dp-cell';
243
+ if (i === currentMonth && currentYear === this.state.viewDate.getFullYear()) el.classList.add('dp-today');
244
+ el.dataset.ts = date.getTime();
245
+ el.textContent = date.toLocaleString(this.options.locale, { month: 'short' });
246
+ frag.appendChild(el);
247
+ }
248
+ container.appendChild(frag);
249
+ }
250
+
251
+ _renderYears(container, titleEl) {
252
+ container.className = 'dp-body dp-grid-wide';
253
+ const startYear = Math.floor(this.state.viewDate.getFullYear() / 10) * 10;
254
+ titleEl.textContent = `${startYear} - ${startYear + 9}`;
255
+ const currentYear = new Date().getFullYear();
256
+ const frag = document.createDocumentFragment();
257
+
258
+ for (let i = 0; i < 12; i++) {
259
+ const year = startYear - 1 + i;
260
+ const date = new Date(year, 0, 1);
261
+ const el = document.createElement('div');
262
+ el.className = 'dp-cell';
263
+ if (year === currentYear) el.classList.add('dp-today');
264
+ if (i === 0 || i === 11) el.classList.add('dp-other-month');
265
+ el.dataset.ts = date.getTime();
266
+ el.textContent = year;
267
+ frag.appendChild(el);
268
+ }
269
+ container.appendChild(frag);
270
+ }
271
+
272
+ /* ================= Logic & Helpers ================= */
273
+
274
+ _updateInput(value) {
275
+ const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
276
+ nativeInputValueSetter.call(this.input, value);
277
+ this.input.dispatchEvent(new Event('input', { bubbles: true }));
278
+ this.input.dispatchEvent(new Event('change', { bubbles: true }));
279
+ }
280
+
281
+ _handleSelection(target) {
282
+ const timestamp = parseInt(target.dataset.ts);
283
+ if (isNaN(timestamp)) return;
284
+ const rawDate = new Date(timestamp);
285
+
286
+ if (this.state.view !== 'day') {
287
+ if (this.state.view === 'year') {
288
+ this.state.viewDate.setFullYear(rawDate.getFullYear());
289
+ this.state.view = 'month';
290
+ } else {
291
+ this.state.viewDate.setMonth(rawDate.getMonth());
292
+ this.state.view = 'day';
293
+ }
294
+ this._render();
295
+ return;
296
+ }
297
+
298
+ if (this.options.mode === 'single') {
299
+ this.state.selectedDate = rawDate;
300
+ this._updateInput(this.options.format(rawDate));
301
+ if (this.options.onChange) this.options.onChange(rawDate);
302
+ this.close();
303
+ } else {
304
+ if (!this.state.rangeStart || (this.state.rangeStart && this.state.rangeEnd)) {
305
+ this.state.rangeStart = rawDate;
306
+ this.state.rangeEnd = null;
307
+ this._updateInput(`${this.options.format(rawDate)} - ...`);
308
+ } else if (rawDate < this.state.rangeStart) {
309
+ this.state.rangeStart = rawDate;
310
+ this._updateInput(`${this.options.format(rawDate)} - ...`);
311
+ } else {
312
+ this.state.rangeEnd = rawDate;
313
+ this._updateInput(`${this.options.format(this.state.rangeStart)} - ${this.options.format(this.state.rangeEnd)}`);
314
+ if (this.options.onChange) this.options.onChange({ start: this.state.rangeStart, end: this.state.rangeEnd });
315
+ this.close();
316
+ }
317
+ }
318
+ this._render();
319
+ }
320
+
321
+ _handleNavigation(action) {
322
+ const { view, viewDate } = this.state;
323
+ if (action === 'switch-view') {
324
+ this.state.view = view === 'day' ? 'month' : 'year';
325
+ } else {
326
+ const dir = action === 'next' ? 1 : -1;
327
+ if (view === 'day') viewDate.setMonth(viewDate.getMonth() + dir);
328
+ if (view === 'month') viewDate.setFullYear(viewDate.getFullYear() + dir);
329
+ if (view === 'year') viewDate.setFullYear(viewDate.getFullYear() + (dir * 10));
330
+ }
331
+ this._render();
332
+ }
333
+
334
+ _position() {
335
+ if (!this.state.isVisible) return;
336
+ const rect = this.input.getBoundingClientRect();
337
+ this.root.style.top = `${rect.bottom + window.scrollY + 4}px`;
338
+ this.root.style.left = `${rect.left + window.scrollX}px`;
339
+ }
340
+
341
+ open() {
342
+ this.state.isVisible = true;
343
+ this.root.classList.add('dp-visible');
344
+ this._position();
345
+ this._render();
346
+ }
347
+
348
+ close() {
349
+ this.state.isVisible = false;
350
+ this.root.classList.remove('dp-visible');
351
+ }
352
+
353
+ _normalizeDate(d) {
354
+ const date = new Date(d);
355
+ date.setHours(0, 0, 0, 0);
356
+ return date;
357
+ }
358
+
359
+ _handleInputClick(e) {
360
+ e.preventDefault();
361
+ this.open();
362
+ }
363
+
364
+ _handleDocumentClick(e) {
365
+ if (this.state.isVisible && !this.root.contains(e.target) && e.target !== this.input) {
366
+ this.close();
367
+ }
368
+ }
369
+
370
+ _handleResize() {
371
+ if (this.state.isVisible) this._position();
372
+ }
373
+ }
374
+
375
+ export default EssealDatePicker;
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "esseal-date-picker",
3
+ "version": "1.0.0",
4
+ "description": "A lightweight, dependency-free vanilla JS date picker with customisation support.",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "style": "style.css",
8
+ "keywords": [
9
+ "date-picker",
10
+ "calendar",
11
+ "vanilla-js",
12
+ "range-picker"
13
+ ],
14
+ "author": "",
15
+ "license": "MIT"
16
+ }