@rushdi94/hijri-datepicker 1.0.1 → 2.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.
@@ -7,8 +7,13 @@ const HIJRI_MONTHS_AR = [
7
7
  'جمادى الأولى', 'جمادى الآخرة', 'رجب', 'شعبان',
8
8
  'رمضان', 'شوال', 'ذو القعدة', 'ذو الحجة',
9
9
  ];
10
- // Sunday Saturday in Arabic
10
+ const GREGORIAN_MONTHS_EN = [
11
+ 'January', 'February', 'March', 'April', 'May', 'June',
12
+ 'July', 'August', 'September', 'October', 'November', 'December',
13
+ ];
14
+ // Sunday → Saturday
11
15
  const WEEKDAY_AR = ['الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'];
16
+ const WEEKDAY_EN = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
12
17
  // ── calendar arithmetic ──────────────────────────────────────────────────────
13
18
  function gToJDN(y, m, d) {
14
19
  const a = Math.floor((14 - m) / 12);
@@ -59,7 +64,7 @@ function jdnToH(jdn) {
59
64
  const year = 30 * n + j - 30;
60
65
  return [year, month, day];
61
66
  }
62
- // ── public helpers ───────────────────────────────────────────────────────────
67
+ // ── Hijri helpers ─────────────────────────────────────────────────────────────
63
68
  function toHijri(date) {
64
69
  const [year, month, day] = jdnToH(gToJDN(date.getFullYear(), date.getMonth() + 1, date.getDate()));
65
70
  return { year, month, day };
@@ -74,7 +79,7 @@ function daysInMonth(year, month) {
74
79
  return hToJDN(ny, nm, 1) - hToJDN(year, month, 1);
75
80
  }
76
81
  function firstWeekday(year, month) {
77
- return toGregorian(year, month, 1).getDay(); // 0 = Sunday
82
+ return toGregorian(year, month, 1).getDay();
78
83
  }
79
84
  function todayHijri() {
80
85
  return toHijri(new Date());
@@ -104,79 +109,80 @@ function parseHijri(value, fmt = 'DD/MM/YYYY') {
104
109
  return null;
105
110
  }
106
111
  }
112
+ // ── Gregorian helpers ─────────────────────────────────────────────────────────
113
+ function daysInMonthGregorian(year, month) {
114
+ return new Date(year, month, 0).getDate();
115
+ }
116
+ function firstWeekdayGregorian(year, month) {
117
+ return new Date(year, month - 1, 1).getDay();
118
+ }
119
+ function todayGregorian() {
120
+ const d = new Date();
121
+ return { year: d.getFullYear(), month: d.getMonth() + 1, day: d.getDate() };
122
+ }
123
+ function formatGregorian(g, fmt = 'DD/MM/YYYY') {
124
+ return fmt
125
+ .replace('YYYY', String(g.year))
126
+ .replace('MM', String(g.month).padStart(2, '0'))
127
+ .replace('DD', String(g.day).padStart(2, '0'))
128
+ .replace('M', String(g.month))
129
+ .replace('D', String(g.day));
130
+ }
107
131
 
108
132
  const CSS = /* css */ `
109
133
  /* ── HijriDatePicker ─────────────────────────────────────────────────────── */
110
- .hdp,
111
- .hdp *,
112
- .hdp *::before,
113
- .hdp *::after {
134
+ .hdp, .hdp *, .hdp *::before, .hdp *::after {
114
135
  box-sizing: border-box;
115
136
  font-family: system-ui, -apple-system, 'Segoe UI', Tahoma, Arial, sans-serif;
116
137
  }
117
138
 
118
- /* wrapper */
119
139
  .hdp-wrapper { position: relative; display: inline-block; }
120
140
 
121
- /* input */
141
+ /* ── input ───────────────────────────────────────────────────────────────── */
122
142
  .hdp-input-wrap { position: relative; display: block; }
123
143
  .hdp-input {
124
- width: 100%;
125
- padding: 9px 14px 9px 38px;
126
- border: 1.5px solid #d1d5db;
127
- border-radius: 8px;
128
- font-size: 14px;
129
- cursor: pointer;
130
- background: #fff;
131
- outline: none;
132
- color: #333;
133
- min-width: 210px;
144
+ width: 100%; padding: 9px 14px 9px 38px;
145
+ border: 1.5px solid #d1d5db; border-radius: 8px;
146
+ font-size: 14px; cursor: pointer; background: #fff;
147
+ outline: none; color: #333; min-width: 210px;
134
148
  transition: border-color .2s, box-shadow .2s;
135
- caret-color: transparent;
136
- text-align: inherit;
149
+ caret-color: transparent; text-align: inherit;
137
150
  }
138
151
  [dir="rtl"] .hdp-input { padding: 9px 14px 9px 38px; text-align: right; }
139
152
  [dir="ltr"] .hdp-input { padding: 9px 38px 9px 14px; text-align: left; }
140
153
  .hdp-input::placeholder { color: #aaa; }
141
- .hdp-input:focus,
142
- .hdp-input.open {
154
+ .hdp-input:focus, .hdp-input.open {
143
155
  border-color: var(--hdp-primary, #8b1a2e);
144
156
  box-shadow: 0 0 0 3px rgba(139,26,46,.14);
145
157
  }
146
158
  .hdp-input-icon {
147
- position: absolute;
148
- top: 50%;
149
- transform: translateY(-50%);
150
- pointer-events: none;
151
- color: #9ca3af;
152
- width: 18px;
153
- height: 18px;
159
+ position: absolute; top: 50%; transform: translateY(-50%);
160
+ pointer-events: none; color: #9ca3af; width: 18px; height: 18px;
154
161
  }
155
162
  [dir="rtl"] .hdp-input-icon { left: 10px; right: auto; }
156
163
  [dir="ltr"] .hdp-input-icon { right: 10px; left: auto; }
157
164
 
158
165
  /* ── popup ───────────────────────────────────────────────────────────────── */
159
166
  .hdp-popup {
160
- position: absolute;
161
- top: calc(100% + 6px);
162
- z-index: 99999;
163
- }
164
- [dir="rtl"] .hdp-popup { right: 0; left: auto; }
165
- [dir="ltr"] .hdp-popup { left: 0; right: auto; }
166
- .hdp-popup {
167
- width: 312px;
168
- border-radius: 12px;
169
- overflow: visible;
167
+ position: absolute; top: calc(100% + 8px); z-index: 99999;
168
+ width: 312px; border-radius: 12px; overflow: visible;
170
169
  box-shadow: 0 8px 32px rgba(0,0,0,.18), 0 2px 8px rgba(0,0,0,.08);
171
170
  background: var(--hdp-body-bg, #fff);
172
171
  animation: _hdpIn .15s cubic-bezier(.2,.8,.4,1);
173
172
  }
173
+ [dir="rtl"] .hdp-popup { right: 0; left: auto; }
174
+ [dir="ltr"] .hdp-popup { left: 0; right: auto; }
175
+ @keyframes _hdpIn {
176
+ from { opacity: 0; transform: translateY(-8px) scale(.97); }
177
+ to { opacity: 1; transform: translateY(0) scale(1); }
178
+ }
179
+ .hdp-popup.hdp-inline {
180
+ position: static; box-shadow: 0 2px 16px rgba(0,0,0,.1); animation: none;
181
+ }
174
182
 
175
- /* upward-pointing caret connecting popup to the input */
183
+ /* caret pointing up to input */
176
184
  .hdp-popup:not(.hdp-inline)::before {
177
- content: '';
178
- position: absolute;
179
- top: -8px;
185
+ content: ''; position: absolute; top: -8px;
180
186
  width: 0; height: 0;
181
187
  border-left: 9px solid transparent;
182
188
  border-right: 9px solid transparent;
@@ -185,192 +191,158 @@ const CSS = /* css */ `
185
191
  }
186
192
  [dir="rtl"] .hdp-popup:not(.hdp-inline)::before { right: 18px; left: auto; }
187
193
  [dir="ltr"] .hdp-popup:not(.hdp-inline)::before { left: 18px; right: auto; }
188
- @keyframes _hdpIn {
189
- from { opacity: 0; transform: translateY(-8px) scale(.97); }
190
- to { opacity: 1; transform: translateY(0) scale(1); }
191
- }
192
- .hdp-popup.hdp-inline {
193
- position: static;
194
- box-shadow: 0 2px 16px rgba(0,0,0,.1);
195
- animation: none;
196
- }
197
194
 
198
195
  /* ── header ──────────────────────────────────────────────────────────────── */
199
196
  .hdp-header {
200
197
  background: var(--hdp-primary, #8b1a2e);
201
198
  color: var(--hdp-on-primary, #fff);
202
- display: flex;
203
- flex-direction: row; /* always LTR regardless of dir attribute */
204
- align-items: stretch;
205
- height: 44px;
199
+ border-radius: 12px 12px 0 0;
200
+ display: flex; flex-direction: row;
201
+ align-items: stretch; height: 44px;
206
202
  user-select: none;
207
203
  }
208
204
 
209
- /* prev / next month arrows */
210
205
  .hdp-nav {
211
- flex-shrink: 0;
212
- width: 36px;
213
- border: none;
214
- background: transparent;
215
- color: var(--hdp-on-primary, #fff);
216
- cursor: pointer;
217
- display: flex; align-items: center; justify-content: center;
218
- font-size: 18px;
219
- transition: background .15s;
220
- line-height: 1;
206
+ flex-shrink: 0; width: 32px; border: none;
207
+ background: transparent; color: var(--hdp-on-primary, #fff);
208
+ cursor: pointer; display: flex; align-items: center; justify-content: center;
209
+ font-size: 18px; transition: background .15s; line-height: 1;
221
210
  }
222
211
  .hdp-nav:hover { background: rgba(255,255,255,.18); }
223
212
 
224
- /* ── month half (left 50%) ───────────────────────────────────────────────── */
213
+ /* month half */
225
214
  .hdp-month-half {
226
- flex: 1;
227
- display: flex;
228
- align-items: center;
229
- justify-content: center;
230
- border-right: 1px solid rgba(255,255,255,.2);
231
- padding: 0 4px;
232
- min-width: 0;
215
+ flex: 1; display: flex; align-items: center; justify-content: center;
216
+ border-right: 1px solid rgba(255,255,255,.2); padding: 0 4px; min-width: 0;
233
217
  }
234
-
235
218
  .hdp-month-select {
236
- width: 100%;
237
- appearance: none;
238
- -webkit-appearance: none;
219
+ width: 100%; appearance: none; -webkit-appearance: none;
239
220
  background: transparent url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23ffffff' stroke-width='1.8' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat right 8px center;
240
- border: none;
241
- padding: 6px 26px 6px 10px;
242
- font-size: 14px; font-weight: 700;
243
- color: var(--hdp-on-primary, #fff);
244
- cursor: pointer;
245
- outline: none;
246
- text-align: center;
247
- text-align-last: center;
248
- letter-spacing: .2px;
249
- }
250
- [dir="rtl"] .hdp-month-select {
251
- background-position: left 8px center;
252
- padding: 6px 10px 6px 26px;
221
+ border: none; padding: 6px 26px 6px 10px;
222
+ font-size: 13px; font-weight: 700; color: var(--hdp-on-primary, #fff);
223
+ cursor: pointer; outline: none; text-align: center; text-align-last: center;
253
224
  }
225
+ [dir="rtl"] .hdp-month-select { background-position: left 8px center; padding: 6px 10px 6px 26px; }
254
226
  .hdp-month-select:hover { background-color: rgba(255,255,255,.12); border-radius: 6px; }
255
227
  .hdp-month-select option { background: #fff; color: #111; font-weight: 600; }
256
228
 
257
- /* ── year half (right 50%) ───────────────────────────────────────────────── */
229
+ /* year half */
258
230
  .hdp-year-half {
259
- flex: 1;
260
- display: flex;
261
- align-items: center;
262
- justify-content: center;
263
- padding: 0 6px;
264
- min-width: 0;
231
+ flex: 1; display: flex; align-items: center; justify-content: center;
232
+ padding: 0 6px; min-width: 0;
265
233
  }
266
-
267
- /* inner wrapper: year number + arrows in a row */
268
- .hdp-year-inner {
269
- display: flex;
270
- align-items: center;
271
- gap: 3px;
272
- }
273
-
234
+ .hdp-year-inner { display: flex; align-items: center; gap: 3px; }
274
235
  .hdp-year-display {
275
- font-size: 15px; font-weight: 800;
276
- color: var(--hdp-on-primary, #fff);
277
- letter-spacing: .4px;
278
- line-height: 1;
279
- white-space: nowrap;
236
+ font-size: 15px; font-weight: 800; color: var(--hdp-on-primary, #fff);
237
+ letter-spacing: .4px; line-height: 1; white-space: nowrap;
280
238
  }
281
-
282
- /* arrows: collapsed (width 0) when not hovered, slide in on hover */
283
239
  .hdp-year-arrows {
284
- display: flex;
285
- flex-direction: column;
286
- gap: 2px;
287
- max-width: 0;
288
- overflow: hidden;
289
- opacity: 0;
290
- pointer-events: none;
240
+ display: flex; flex-direction: column; gap: 2px;
241
+ max-width: 0; overflow: hidden; opacity: 0; pointer-events: none;
291
242
  transition: max-width .18s ease, opacity .18s ease;
292
243
  }
293
- .hdp-year-half:hover .hdp-year-arrows {
294
- max-width: 22px;
295
- opacity: 1;
296
- pointer-events: auto;
297
- }
298
-
244
+ .hdp-year-half:hover .hdp-year-arrows { max-width: 22px; opacity: 1; pointer-events: auto; }
299
245
  .hdp-year-arrow {
300
- background: none;
301
- border: none;
302
- color: var(--hdp-on-primary, #fff);
303
- cursor: pointer;
304
- width: 18px; height: 13px;
246
+ background: none; border: none; color: var(--hdp-on-primary, #fff);
247
+ cursor: pointer; width: 18px; height: 13px;
305
248
  display: flex; align-items: center; justify-content: center;
306
- font-size: 7px;
307
- border-radius: 3px;
308
- padding: 0;
309
- line-height: 1;
310
- flex-shrink: 0;
311
- transition: background .12s;
249
+ font-size: 7px; border-radius: 3px; padding: 0; flex-shrink: 0;
250
+ line-height: 1; transition: background .12s;
312
251
  }
313
252
  .hdp-year-arrow:hover { background: rgba(255,255,255,.25); }
314
253
 
254
+ /* calendar mode toggle */
255
+ .hdp-mode-btn {
256
+ flex-shrink: 0; background: rgba(255,255,255,.15);
257
+ border: 1px solid rgba(255,255,255,.3); border-radius: 5px;
258
+ color: var(--hdp-on-primary, #fff); cursor: pointer;
259
+ font-size: 11px; font-weight: 800;
260
+ padding: 0 7px; margin: 8px 6px; height: 28px;
261
+ transition: background .15s; line-height: 1; white-space: nowrap;
262
+ }
263
+ .hdp-mode-btn:hover { background: rgba(255,255,255,.3); }
264
+
315
265
  /* ── weekday row ─────────────────────────────────────────────────────────── */
316
266
  .hdp-weekdays {
317
- display: grid;
318
- grid-template-columns: repeat(7, 1fr);
319
- background: var(--hdp-primary, #8b1a2e);
320
- padding: 0 10px 10px;
267
+ display: grid; grid-template-columns: repeat(7, 1fr);
268
+ background: var(--hdp-primary, #8b1a2e); padding: 0 10px 10px;
321
269
  }
322
270
  .hdp-wday {
323
- text-align: center;
324
- font-size: 11.5px;
325
- color: var(--hdp-on-primary, #fff);
326
- opacity: .82;
327
- font-weight: 500;
328
- padding: 3px 0;
271
+ text-align: center; font-size: 11.5px; color: var(--hdp-on-primary, #fff);
272
+ opacity: .82; font-weight: 500; padding: 3px 0;
329
273
  }
330
274
 
331
275
  /* ── days ────────────────────────────────────────────────────────────────── */
332
276
  .hdp-days {
333
- display: grid;
334
- grid-template-columns: repeat(7, 1fr);
335
- padding: 8px 8px 12px;
336
- gap: 2px;
337
- border-radius: 0 0 12px 12px;
277
+ display: grid; grid-template-columns: repeat(7, 1fr);
278
+ padding: 8px 8px 12px; gap: 2px;
338
279
  background: var(--hdp-body-bg, #fff);
339
280
  }
340
281
  .hdp-day {
341
- aspect-ratio: 1;
342
- display: flex; align-items: center; justify-content: center;
343
- border-radius: 50%;
344
- font-size: 13.5px;
345
- cursor: pointer;
282
+ aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
283
+ border-radius: 50%; font-size: 13.5px; cursor: pointer;
346
284
  color: var(--hdp-day-color, #1f2937);
347
285
  transition: background .12s, color .12s, transform .08s;
348
- font-weight: 400;
349
- border: 2px solid transparent;
350
- }
351
- .hdp-day:not(.hdp-empty):hover {
352
- background: var(--hdp-hover-bg, rgba(139,26,46,.1));
353
- transform: scale(1.08);
286
+ font-weight: 400; border: 2px solid transparent;
354
287
  }
288
+ .hdp-day:not(.hdp-empty):hover { background: var(--hdp-hover-bg); transform: scale(1.08); }
355
289
  .hdp-day.hdp-today:not(.hdp-selected) {
356
- border-color: var(--hdp-primary, #8b1a2e);
357
- color: var(--hdp-primary, #8b1a2e);
358
- font-weight: 700;
290
+ border-color: var(--hdp-primary, #8b1a2e); color: var(--hdp-primary, #8b1a2e); font-weight: 700;
359
291
  }
360
292
  .hdp-day.hdp-selected {
361
293
  background: var(--hdp-primary, #8b1a2e) !important;
362
294
  color: var(--hdp-on-primary, #fff) !important;
363
- font-weight: 700;
364
- border-color: transparent !important;
365
- transform: scale(1.1);
295
+ font-weight: 700; border-color: transparent !important; transform: scale(1.1);
366
296
  }
367
- .hdp-day.hdp-empty {
368
- cursor: default;
369
- pointer-events: none;
297
+ .hdp-day.hdp-empty { cursor: default; pointer-events: none; }
298
+
299
+ /* ── time section ────────────────────────────────────────────────────────── */
300
+ .hdp-time-section {
301
+ display: flex; align-items: center; justify-content: center; gap: 6px;
302
+ padding: 10px 16px 14px;
303
+ border-top: 1px solid #f0f0f0;
304
+ background: #f9fafb;
305
+ border-radius: 0 0 12px 12px;
306
+ }
307
+
308
+ .hdp-spinner {
309
+ display: flex; flex-direction: column; align-items: center; gap: 2px;
310
+ }
311
+ .hdp-spinner-btn {
312
+ background: none; border: none; color: #999; cursor: pointer;
313
+ width: 38px; height: 18px;
314
+ display: flex; align-items: center; justify-content: center;
315
+ font-size: 8px; border-radius: 4px; padding: 0; line-height: 1;
316
+ transition: background .12s, color .12s;
317
+ }
318
+ .hdp-spinner-btn:hover { background: var(--hdp-hover-bg); color: var(--hdp-primary, #8b1a2e); }
319
+ .hdp-spinner-val {
320
+ font-size: 26px; font-weight: 700;
321
+ color: var(--hdp-day-color, #1f2937);
322
+ min-width: 38px; text-align: center; line-height: 1;
323
+ }
324
+
325
+ .hdp-time-colon {
326
+ font-size: 24px; font-weight: 700;
327
+ color: var(--hdp-day-color, #1f2937);
328
+ margin-top: 18px; line-height: 1;
329
+ }
330
+
331
+ .hdp-ampm {
332
+ display: flex; flex-direction: column; gap: 4px; margin-top: 18px;
333
+ }
334
+ .hdp-ampm-btn {
335
+ background: #e5e7eb; border: none; border-radius: 6px;
336
+ padding: 5px 10px; font-size: 11px; font-weight: 700;
337
+ color: #555; cursor: pointer; transition: background .12s, color .12s;
370
338
  }
339
+ .hdp-ampm-btn.hdp-ampm-active {
340
+ background: var(--hdp-primary, #8b1a2e); color: var(--hdp-on-primary, #fff);
341
+ }
342
+ .hdp-ampm-btn:not(.hdp-ampm-active):hover { background: #d1d5db; }
371
343
  `;
372
344
 
373
- // ── style injection (once per page) ─────────────────────────────────────────
345
+ // ── style injection ───────────────────────────────────────────────────────────
374
346
  let _stylesInjected = false;
375
347
  function ensureStyles() {
376
348
  if (_stylesInjected || typeof document === 'undefined')
@@ -381,14 +353,16 @@ function ensureStyles() {
381
353
  el.textContent = CSS;
382
354
  document.head.appendChild(el);
383
355
  }
384
- // ── component ────────────────────────────────────────────────────────────────
356
+ // ── defaults ──────────────────────────────────────────────────────────────────
385
357
  const DEFAULT_WEEKDAYS_AR = [...WEEKDAY_AR];
386
358
  const DEFAULT_WEEKDAYS_EN = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
359
+ // ── component ─────────────────────────────────────────────────────────────────
387
360
  class HijriDatePicker {
388
361
  // ── constructor ────────────────────────────────────────────────────────────
389
362
  constructor(options) {
390
363
  var _a;
391
- this.selected = null;
364
+ this.selectedHijri = null;
365
+ this.selectedGregorian = null;
392
366
  // ── event handlers ─────────────────────────────────────────────────────────
393
367
  this._onDocClick = (e) => {
394
368
  if (!this.root.contains(e.target))
@@ -405,15 +379,19 @@ class HijriDatePicker {
405
379
  const defaults = {
406
380
  inline: false,
407
381
  dir,
408
- placeholder: dir === 'rtl' ? 'اختر تاريخاً هجرياً' : 'Select Hijri date',
382
+ placeholder: dir === 'rtl' ? 'اختر تاريخاً هجرياً' : 'Select date',
409
383
  weekdayLabels: dir === 'rtl' ? DEFAULT_WEEKDAYS_AR : DEFAULT_WEEKDAYS_EN,
410
384
  monthLabels: [...HIJRI_MONTHS_AR],
385
+ gregorianMonthLabels: [...GREGORIAN_MONTHS_EN],
411
386
  primaryColor: '#8b1a2e',
412
387
  onPrimaryColor: '#ffffff',
413
388
  bodyBg: '#ffffff',
414
389
  dayColor: '#1f2937',
415
390
  format: 'DD/MM/YYYY',
416
391
  initialDate: today,
392
+ initialMode: 'hijri',
393
+ showTime: false,
394
+ use24h: false,
417
395
  editable: false,
418
396
  container: options.container,
419
397
  onSelect: () => undefined,
@@ -422,8 +400,19 @@ class HijriDatePicker {
422
400
  };
423
401
  this.opts = { ...defaults, ...options, dir };
424
402
  this.isRTL = this.opts.dir === 'rtl';
425
- this.viewYear = this.opts.initialDate.year;
426
- this.viewMonth = this.opts.initialDate.month;
403
+ this.calMode = this.opts.initialMode;
404
+ if (this.calMode === 'hijri') {
405
+ this.viewYear = this.opts.initialDate.year;
406
+ this.viewMonth = this.opts.initialDate.month;
407
+ }
408
+ else {
409
+ const g = todayGregorian();
410
+ this.viewYear = g.year;
411
+ this.viewMonth = g.month;
412
+ }
413
+ this.hour = this.opts.use24h ? 0 : 12;
414
+ this.minute = 0;
415
+ this.ampm = 'AM';
427
416
  ensureStyles();
428
417
  this._mount();
429
418
  }
@@ -473,8 +462,13 @@ class HijriDatePicker {
473
462
  if (this.opts.editable) {
474
463
  this.inputEl.addEventListener('input', () => {
475
464
  const parsed = parseHijri(this.inputEl.value, this.opts.format);
476
- if (parsed)
477
- this._applySelection(parsed, false);
465
+ if (parsed) {
466
+ const gDate = toGregorian(parsed.year, parsed.month, parsed.day);
467
+ const gregorian = {
468
+ year: gDate.getFullYear(), month: gDate.getMonth() + 1, day: gDate.getDate(),
469
+ };
470
+ this._applySelection(parsed, gregorian, false);
471
+ }
478
472
  });
479
473
  }
480
474
  const icon = document.createElement('span');
@@ -500,38 +494,34 @@ class HijriDatePicker {
500
494
  popup.style.left = '0';
501
495
  }
502
496
  popup.setAttribute('role', 'dialog');
503
- popup.setAttribute('aria-label', this.isRTL ? 'منتقي التاريخ الهجري' : 'Hijri Date Picker');
497
+ popup.setAttribute('aria-label', this.isRTL ? 'منتقي التاريخ' : 'Date Picker');
504
498
  popup.appendChild(this._buildHeader());
505
499
  popup.appendChild(this._buildWeekdays());
506
500
  this.daysGrid = this._buildDaysGrid();
507
501
  popup.appendChild(this.daysGrid);
502
+ if (this.opts.showTime) {
503
+ popup.appendChild(this._buildTimeSection());
504
+ }
508
505
  return popup;
509
506
  }
510
507
  // ── header ─────────────────────────────────────────────────────────────────
511
508
  _buildHeader() {
512
509
  const hdr = document.createElement('div');
513
510
  hdr.className = 'hdp-header';
514
- // ── Month half (left) ─────────────────────────────────────────
511
+ // Month half (left)
515
512
  const monthHalf = document.createElement('div');
516
513
  monthHalf.className = 'hdp-month-half';
517
514
  this.monthSelectEl = document.createElement('select');
518
515
  this.monthSelectEl.className = 'hdp-month-select';
519
- this.monthSelectEl.setAttribute('aria-label', this.isRTL ? 'الشهر' : 'Month');
520
- this.opts.monthLabels.forEach((name, i) => {
521
- const opt = document.createElement('option');
522
- opt.value = String(i + 1);
523
- opt.textContent = name;
524
- this.monthSelectEl.appendChild(opt);
525
- });
516
+ this._rebuildMonthOptions();
526
517
  this.monthSelectEl.addEventListener('change', () => {
527
518
  this.viewMonth = Number(this.monthSelectEl.value);
528
519
  this._renderDays();
529
520
  });
530
521
  monthHalf.appendChild(this.monthSelectEl);
531
- // ── Year half (right) ─────────────────────────────────────────
522
+ // Year half (right)
532
523
  const yearHalf = document.createElement('div');
533
524
  yearHalf.className = 'hdp-year-half';
534
- // Inner group: year number + arrows side by side
535
525
  const yearInner = document.createElement('div');
536
526
  yearInner.className = 'hdp-year-inner';
537
527
  this.yearDisplayEl = document.createElement('span');
@@ -541,31 +531,25 @@ class HijriDatePicker {
541
531
  const yearUp = document.createElement('button');
542
532
  yearUp.className = 'hdp-year-arrow';
543
533
  yearUp.innerHTML = '▲';
544
- yearUp.setAttribute('aria-label', this.isRTL ? 'السنة التالية' : 'Next year');
545
- yearUp.addEventListener('click', () => {
546
- this.viewYear++;
547
- this._syncHeader();
548
- this._renderDays();
549
- });
534
+ yearUp.addEventListener('click', () => { this.viewYear++; this._syncHeader(); this._renderDays(); });
550
535
  const yearDown = document.createElement('button');
551
536
  yearDown.className = 'hdp-year-arrow';
552
537
  yearDown.innerHTML = '▼';
553
- yearDown.setAttribute('aria-label', this.isRTL ? 'السنة السابقة' : 'Previous year');
554
- yearDown.addEventListener('click', () => {
555
- this.viewYear--;
556
- this._syncHeader();
557
- this._renderDays();
558
- });
538
+ yearDown.addEventListener('click', () => { this.viewYear--; this._syncHeader(); this._renderDays(); });
559
539
  yearArrows.appendChild(yearUp);
560
540
  yearArrows.appendChild(yearDown);
561
541
  yearInner.appendChild(this.yearDisplayEl);
562
542
  yearInner.appendChild(yearArrows);
563
543
  yearHalf.appendChild(yearInner);
564
- // ── Assemble: [‹] [month | year] [›] (always LTR order) ─────
544
+ // Calendar mode toggle button
545
+ this.modeBtnEl = document.createElement('button');
546
+ this.modeBtnEl.className = 'hdp-mode-btn';
547
+ this.modeBtnEl.addEventListener('click', () => this._switchMode());
565
548
  hdr.appendChild(this._navBtn('‹', -1));
566
549
  hdr.appendChild(monthHalf);
567
550
  hdr.appendChild(yearHalf);
568
551
  hdr.appendChild(this._navBtn('›', 1));
552
+ hdr.appendChild(this.modeBtnEl);
569
553
  this._syncHeader();
570
554
  return hdr;
571
555
  }
@@ -573,13 +557,45 @@ class HijriDatePicker {
573
557
  const btn = document.createElement('button');
574
558
  btn.className = 'hdp-nav';
575
559
  btn.innerHTML = symbol;
576
- btn.setAttribute('aria-label', delta > 0 ? 'الشهر التالي' : 'الشهر السابق');
560
+ btn.setAttribute('aria-label', delta > 0 ? 'Next month' : 'Previous month');
577
561
  btn.addEventListener('click', () => this._navigate(delta));
578
562
  return btn;
579
563
  }
580
564
  _syncHeader() {
581
565
  this.monthSelectEl.value = String(this.viewMonth);
582
566
  this.yearDisplayEl.textContent = String(this.viewYear);
567
+ this.modeBtnEl.textContent = this.calMode === 'hijri' ? 'G' : 'ه';
568
+ this.modeBtnEl.title = this.calMode === 'hijri' ? 'Switch to Gregorian' : 'Switch to Hijri';
569
+ }
570
+ _rebuildMonthOptions() {
571
+ const labels = this.calMode === 'hijri'
572
+ ? this.opts.monthLabels
573
+ : this.opts.gregorianMonthLabels;
574
+ this.monthSelectEl.innerHTML = '';
575
+ labels.forEach((name, i) => {
576
+ const opt = document.createElement('option');
577
+ opt.value = String(i + 1);
578
+ opt.textContent = name;
579
+ this.monthSelectEl.appendChild(opt);
580
+ });
581
+ this.monthSelectEl.value = String(this.viewMonth);
582
+ }
583
+ _switchMode() {
584
+ if (this.calMode === 'hijri') {
585
+ const gDate = toGregorian(this.viewYear, this.viewMonth, 1);
586
+ this.viewYear = gDate.getFullYear();
587
+ this.viewMonth = gDate.getMonth() + 1;
588
+ this.calMode = 'gregorian';
589
+ }
590
+ else {
591
+ const h = toHijri(new Date(this.viewYear, this.viewMonth - 1, 1));
592
+ this.viewYear = h.year;
593
+ this.viewMonth = h.month;
594
+ this.calMode = 'hijri';
595
+ }
596
+ this._rebuildMonthOptions();
597
+ this._syncHeader();
598
+ this._renderDays();
583
599
  }
584
600
  // ── weekdays ───────────────────────────────────────────────────────────────
585
601
  _buildWeekdays() {
@@ -603,9 +619,15 @@ class HijriDatePicker {
603
619
  _renderDays(grid) {
604
620
  const g = grid !== null && grid !== void 0 ? grid : this.daysGrid;
605
621
  g.innerHTML = '';
606
- const today = todayHijri();
607
- const total = daysInMonth(this.viewYear, this.viewMonth);
608
- const start = firstWeekday(this.viewYear, this.viewMonth);
622
+ const isHijri = this.calMode === 'hijri';
623
+ const todayH = todayHijri();
624
+ const todayG = todayGregorian();
625
+ const total = isHijri
626
+ ? daysInMonth(this.viewYear, this.viewMonth)
627
+ : daysInMonthGregorian(this.viewYear, this.viewMonth);
628
+ const start = isHijri
629
+ ? firstWeekday(this.viewYear, this.viewMonth)
630
+ : firstWeekdayGregorian(this.viewYear, this.viewMonth);
609
631
  for (let i = 0; i < start; i++) {
610
632
  const blank = document.createElement('div');
611
633
  blank.className = 'hdp-day hdp-empty';
@@ -617,12 +639,14 @@ class HijriDatePicker {
617
639
  cell.textContent = String(d);
618
640
  cell.setAttribute('role', 'button');
619
641
  cell.setAttribute('tabindex', '0');
620
- cell.setAttribute('aria-label', `${d} ${this.opts.monthLabels[this.viewMonth - 1]} ${this.viewYear}`);
621
- const isToday = d === today.day && this.viewMonth === today.month && this.viewYear === today.year;
622
- const isSel = this.selected !== null &&
623
- d === this.selected.day &&
624
- this.viewMonth === this.selected.month &&
625
- this.viewYear === this.selected.year;
642
+ const isToday = isHijri
643
+ ? (d === todayH.day && this.viewMonth === todayH.month && this.viewYear === todayH.year)
644
+ : (d === todayG.day && this.viewMonth === todayG.month && this.viewYear === todayG.year);
645
+ const isSel = isHijri
646
+ ? (this.selectedHijri !== null && d === this.selectedHijri.day &&
647
+ this.viewMonth === this.selectedHijri.month && this.viewYear === this.selectedHijri.year)
648
+ : (this.selectedGregorian !== null && d === this.selectedGregorian.day &&
649
+ this.viewMonth === this.selectedGregorian.month && this.viewYear === this.selectedGregorian.year);
626
650
  if (isToday)
627
651
  cell.classList.add('hdp-today');
628
652
  if (isSel)
@@ -637,6 +661,78 @@ class HijriDatePicker {
637
661
  g.appendChild(cell);
638
662
  }
639
663
  }
664
+ // ── time section ───────────────────────────────────────────────────────────
665
+ _buildTimeSection() {
666
+ const section = document.createElement('div');
667
+ section.className = 'hdp-time-section';
668
+ const { el: hourEl, display: hourDisp } = this._buildSpinner(() => {
669
+ this.hour = this.opts.use24h ? (this.hour + 1) % 24 : (this.hour === 12 ? 1 : this.hour + 1);
670
+ this._syncTime();
671
+ }, () => {
672
+ this.hour = this.opts.use24h ? (this.hour - 1 + 24) % 24 : (this.hour === 1 ? 12 : this.hour - 1);
673
+ this._syncTime();
674
+ });
675
+ this.hourDisplayEl = hourDisp;
676
+ const colon = document.createElement('span');
677
+ colon.className = 'hdp-time-colon';
678
+ colon.textContent = ':';
679
+ const { el: minEl, display: minDisp } = this._buildSpinner(() => { this.minute = (this.minute + 1) % 60; this._syncTime(); }, () => { this.minute = (this.minute - 1 + 60) % 60; this._syncTime(); });
680
+ this.minuteDisplayEl = minDisp;
681
+ section.appendChild(hourEl);
682
+ section.appendChild(colon);
683
+ section.appendChild(minEl);
684
+ if (!this.opts.use24h) {
685
+ const ampm = document.createElement('div');
686
+ ampm.className = 'hdp-ampm';
687
+ const amBtn = document.createElement('button');
688
+ amBtn.className = 'hdp-ampm-btn';
689
+ amBtn.textContent = 'AM';
690
+ amBtn.addEventListener('click', () => { this.ampm = 'AM'; this._syncTime(); });
691
+ const pmBtn = document.createElement('button');
692
+ pmBtn.className = 'hdp-ampm-btn';
693
+ pmBtn.textContent = 'PM';
694
+ pmBtn.addEventListener('click', () => { this.ampm = 'PM'; this._syncTime(); });
695
+ ampm.appendChild(amBtn);
696
+ ampm.appendChild(pmBtn);
697
+ this.ampmContainerEl = ampm;
698
+ section.appendChild(ampm);
699
+ }
700
+ this._syncTime();
701
+ return section;
702
+ }
703
+ _buildSpinner(onUp, onDown) {
704
+ const el = document.createElement('div');
705
+ el.className = 'hdp-spinner';
706
+ const upBtn = document.createElement('button');
707
+ upBtn.className = 'hdp-spinner-btn';
708
+ upBtn.innerHTML = '▲';
709
+ upBtn.addEventListener('click', onUp);
710
+ const display = document.createElement('span');
711
+ display.className = 'hdp-spinner-val';
712
+ const downBtn = document.createElement('button');
713
+ downBtn.className = 'hdp-spinner-btn';
714
+ downBtn.innerHTML = '▼';
715
+ downBtn.addEventListener('click', onDown);
716
+ el.appendChild(upBtn);
717
+ el.appendChild(display);
718
+ el.appendChild(downBtn);
719
+ return { el, display };
720
+ }
721
+ _syncTime() {
722
+ this.hourDisplayEl.textContent = String(this.hour).padStart(2, '0');
723
+ this.minuteDisplayEl.textContent = String(this.minute).padStart(2, '0');
724
+ if (!this.opts.use24h && this.ampmContainerEl) {
725
+ const btns = this.ampmContainerEl.querySelectorAll('.hdp-ampm-btn');
726
+ btns[0].classList.toggle('hdp-ampm-active', this.ampm === 'AM');
727
+ btns[1].classList.toggle('hdp-ampm-active', this.ampm === 'PM');
728
+ }
729
+ if (this.selectedHijri && this.selectedGregorian) {
730
+ const fmt = this._buildFormatted(this.selectedHijri, this.selectedGregorian);
731
+ if (this.inputEl)
732
+ this.inputEl.value = fmt;
733
+ this.opts.onSelect({ hijri: this.selectedHijri, gregorian: this.selectedGregorian, time: { hour: this.hour, minute: this.minute, ampm: this.ampm } }, fmt);
734
+ }
735
+ }
640
736
  // ── navigation ─────────────────────────────────────────────────────────────
641
737
  _navigate(delta) {
642
738
  this.viewMonth += delta;
@@ -653,21 +749,44 @@ class HijriDatePicker {
653
749
  }
654
750
  // ── selection ──────────────────────────────────────────────────────────────
655
751
  _pickDay(day) {
656
- this._applySelection({ year: this.viewYear, month: this.viewMonth, day }, true);
752
+ let hijri;
753
+ let gregorian;
754
+ if (this.calMode === 'hijri') {
755
+ hijri = { year: this.viewYear, month: this.viewMonth, day };
756
+ const gDate = toGregorian(hijri.year, hijri.month, hijri.day);
757
+ gregorian = { year: gDate.getFullYear(), month: gDate.getMonth() + 1, day: gDate.getDate() };
758
+ }
759
+ else {
760
+ gregorian = { year: this.viewYear, month: this.viewMonth, day };
761
+ hijri = toHijri(new Date(gregorian.year, gregorian.month - 1, gregorian.day));
762
+ }
763
+ this._applySelection(hijri, gregorian, true);
657
764
  }
658
- _applySelection(date, closeAfter) {
659
- this.selected = { ...date };
660
- this.viewYear = date.year;
661
- this.viewMonth = date.month;
662
- const fmt = formatHijri(date, this.opts.format);
765
+ _applySelection(hijri, gregorian, closeAfter) {
766
+ this.selectedHijri = { ...hijri };
767
+ this.selectedGregorian = { ...gregorian };
768
+ this.viewYear = this.calMode === 'hijri' ? hijri.year : gregorian.year;
769
+ this.viewMonth = this.calMode === 'hijri' ? hijri.month : gregorian.month;
770
+ const fmt = this._buildFormatted(hijri, gregorian);
663
771
  if (this.inputEl)
664
772
  this.inputEl.value = fmt;
665
773
  this._syncHeader();
666
774
  this._renderDays();
667
- this.opts.onSelect(date, fmt);
668
- if (closeAfter && !this.opts.inline)
775
+ this.opts.onSelect({ hijri, gregorian, time: this.opts.showTime ? { hour: this.hour, minute: this.minute, ampm: this.ampm } : null }, fmt);
776
+ if (closeAfter && !this.opts.inline && !this.opts.showTime)
669
777
  this._close();
670
778
  }
779
+ _buildFormatted(hijri, gregorian) {
780
+ const dateStr = this.calMode === 'hijri'
781
+ ? formatHijri(hijri, this.opts.format)
782
+ : formatGregorian(gregorian, this.opts.format);
783
+ if (!this.opts.showTime)
784
+ return dateStr;
785
+ const hh = String(this.hour).padStart(2, '0');
786
+ const mm = String(this.minute).padStart(2, '0');
787
+ const timeStr = this.opts.use24h ? ` ${hh}:${mm}` : ` ${hh}:${mm} ${this.ampm}`;
788
+ return dateStr + timeStr;
789
+ }
671
790
  // ── open / close ───────────────────────────────────────────────────────────
672
791
  _open() {
673
792
  var _a;
@@ -686,17 +805,62 @@ class HijriDatePicker {
686
805
  this.opts.onClose();
687
806
  }
688
807
  // ── public API ─────────────────────────────────────────────────────────────
689
- /** Get the currently selected Hijri date (or null) */
808
+ /** Get the currently selected date/time value */
690
809
  getValue() {
691
- return this.selected ? { ...this.selected } : null;
810
+ if (!this.selectedHijri || !this.selectedGregorian)
811
+ return null;
812
+ return {
813
+ hijri: { ...this.selectedHijri },
814
+ gregorian: { ...this.selectedGregorian },
815
+ time: this.opts.showTime ? { hour: this.hour, minute: this.minute, ampm: this.ampm } : null,
816
+ };
692
817
  }
693
- /** Programmatically select a date */
818
+ /** Set a Hijri date programmatically */
694
819
  setValue(date) {
695
- this._applySelection(date, false);
820
+ const gDate = toGregorian(date.year, date.month, date.day);
821
+ const gregorian = {
822
+ year: gDate.getFullYear(), month: gDate.getMonth() + 1, day: gDate.getDate(),
823
+ };
824
+ if (this.calMode === 'hijri') {
825
+ this.viewYear = date.year;
826
+ this.viewMonth = date.month;
827
+ }
828
+ else {
829
+ this.viewYear = gregorian.year;
830
+ this.viewMonth = gregorian.month;
831
+ }
832
+ this._applySelection(date, gregorian, false);
833
+ }
834
+ /** Set a Gregorian date programmatically */
835
+ setValueGregorian(date) {
836
+ const hijri = toHijri(new Date(date.year, date.month - 1, date.day));
837
+ if (this.calMode === 'gregorian') {
838
+ this.viewYear = date.year;
839
+ this.viewMonth = date.month;
840
+ }
841
+ else {
842
+ this.viewYear = hijri.year;
843
+ this.viewMonth = hijri.month;
844
+ }
845
+ this._applySelection(hijri, date, false);
846
+ }
847
+ /** Set the time programmatically */
848
+ setTime(hour, minute, ampm = 'AM') {
849
+ this.hour = hour;
850
+ this.minute = minute;
851
+ this.ampm = ampm;
852
+ if (this.opts.showTime)
853
+ this._syncTime();
854
+ }
855
+ /** Switch calendar display mode */
856
+ setMode(mode) {
857
+ if (this.calMode !== mode)
858
+ this._switchMode();
696
859
  }
697
860
  /** Clear the selected date */
698
861
  clear() {
699
- this.selected = null;
862
+ this.selectedHijri = null;
863
+ this.selectedGregorian = null;
700
864
  if (this.inputEl)
701
865
  this.inputEl.value = '';
702
866
  this._renderDays();
@@ -714,14 +878,8 @@ class HijriDatePicker {
714
878
  this._applyVars(this.root);
715
879
  this._renderDays();
716
880
  }
717
- /** Open the picker programmatically */
718
- open() {
719
- this._open();
720
- }
721
- /** Close the picker programmatically */
722
- close() {
723
- this._close();
724
- }
881
+ open() { this._open(); }
882
+ close() { this._close(); }
725
883
  /** Remove the picker from the DOM and clean up listeners */
726
884
  destroy() {
727
885
  document.removeEventListener('click', this._onDocClick, true);
@@ -730,14 +888,20 @@ class HijriDatePicker {
730
888
  }
731
889
  }
732
890
 
891
+ exports.GREGORIAN_MONTHS_EN = GREGORIAN_MONTHS_EN;
733
892
  exports.HIJRI_MONTHS_AR = HIJRI_MONTHS_AR;
734
893
  exports.HijriDatePicker = HijriDatePicker;
735
894
  exports.WEEKDAY_AR = WEEKDAY_AR;
895
+ exports.WEEKDAY_EN = WEEKDAY_EN;
736
896
  exports.daysInMonth = daysInMonth;
897
+ exports.daysInMonthGregorian = daysInMonthGregorian;
737
898
  exports.firstWeekday = firstWeekday;
899
+ exports.firstWeekdayGregorian = firstWeekdayGregorian;
900
+ exports.formatGregorian = formatGregorian;
738
901
  exports.formatHijri = formatHijri;
739
902
  exports.parseHijri = parseHijri;
740
903
  exports.toGregorian = toGregorian;
741
904
  exports.toHijri = toHijri;
905
+ exports.todayGregorian = todayGregorian;
742
906
  exports.todayHijri = todayHijri;
743
907
  //# sourceMappingURL=hijri-datepicker.cjs.js.map