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