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