millas 0.2.12-beta → 0.2.12-beta-2
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.
- package/package.json +3 -16
- package/src/admin/ActivityLog.js +153 -52
- package/src/admin/Admin.js +400 -167
- package/src/admin/AdminAuth.js +213 -98
- package/src/admin/FormGenerator.js +372 -0
- package/src/admin/HookRegistry.js +256 -0
- package/src/admin/QueryEngine.js +263 -0
- package/src/admin/ViewContext.js +309 -0
- package/src/admin/WidgetRegistry.js +406 -0
- package/src/admin/index.js +17 -0
- package/src/admin/resources/AdminResource.js +383 -97
- package/src/admin/static/admin.css +1341 -0
- package/src/admin/static/date-picker.css +157 -0
- package/src/admin/static/date-picker.js +316 -0
- package/src/admin/static/json-editor.css +649 -0
- package/src/admin/static/json-editor.js +1429 -0
- package/src/admin/static/ui.js +1044 -0
- package/src/admin/views/layouts/base.njk +65 -1013
- package/src/admin/views/pages/detail.njk +40 -16
- package/src/admin/views/pages/form.njk +47 -599
- package/src/admin/views/pages/list.njk +145 -62
- package/src/admin/views/partials/form-field.njk +53 -0
- package/src/admin/views/partials/form-footer.njk +28 -0
- package/src/admin/views/partials/form-readonly.njk +114 -0
- package/src/admin/views/partials/form-scripts.njk +476 -0
- package/src/admin/views/partials/form-widget.njk +296 -0
- package/src/admin/views/partials/json-dialog.njk +80 -0
- package/src/admin/views/partials/json-editor.njk +37 -0
- package/src/admin.zip +0 -0
- package/src/auth/Auth.js +31 -10
- package/src/auth/AuthController.js +3 -1
- package/src/auth/AuthUser.js +119 -0
- package/src/cli.js +4 -2
- package/src/commands/createsuperuser.js +254 -0
- package/src/commands/lang.js +589 -0
- package/src/commands/migrate.js +154 -81
- package/src/commands/serve.js +82 -110
- package/src/container/AppInitializer.js +215 -0
- package/src/container/Application.js +278 -253
- package/src/container/HttpServer.js +156 -0
- package/src/container/MillasApp.js +29 -279
- package/src/container/MillasConfig.js +192 -0
- package/src/core/admin.js +5 -0
- package/src/core/auth.js +9 -0
- package/src/core/db.js +9 -0
- package/src/core/foundation.js +59 -0
- package/src/core/http.js +11 -0
- package/src/core/lang.js +1 -0
- package/src/core/mail.js +6 -0
- package/src/core/queue.js +7 -0
- package/src/core/validation.js +29 -0
- package/src/facades/Admin.js +1 -1
- package/src/facades/Auth.js +22 -39
- package/src/facades/Cache.js +21 -10
- package/src/facades/Database.js +1 -1
- package/src/facades/Events.js +18 -17
- package/src/facades/Facade.js +197 -0
- package/src/facades/Http.js +42 -45
- package/src/facades/Log.js +25 -49
- package/src/facades/Mail.js +27 -32
- package/src/facades/Queue.js +22 -15
- package/src/facades/Storage.js +18 -10
- package/src/facades/Url.js +53 -0
- package/src/http/HttpClient.js +673 -0
- package/src/http/ResponseDispatcher.js +18 -111
- package/src/http/UrlGenerator.js +375 -0
- package/src/http/WelcomePage.js +273 -0
- package/src/http/adapters/ExpressAdapter.js +315 -0
- package/src/http/adapters/HttpAdapter.js +168 -0
- package/src/http/adapters/index.js +9 -0
- package/src/i18n/I18nServiceProvider.js +91 -0
- package/src/i18n/Translator.js +635 -0
- package/src/i18n/defaults.js +122 -0
- package/src/i18n/index.js +164 -0
- package/src/i18n/locales/en.js +55 -0
- package/src/i18n/locales/sw.js +48 -0
- package/src/index.js +5 -144
- package/src/logger/formatters/PrettyFormatter.js +103 -57
- package/src/logger/internal.js +2 -2
- package/src/logger/patchConsole.js +91 -81
- package/src/middleware/MiddlewareRegistry.js +62 -82
- package/src/migrations/system/0001_users.js +21 -0
- package/src/migrations/system/0002_admin_log.js +25 -0
- package/src/migrations/system/0003_sessions.js +23 -0
- package/src/orm/fields/index.js +210 -188
- package/src/orm/migration/DefaultValueParser.js +325 -0
- package/src/orm/migration/InteractiveResolver.js +191 -0
- package/src/orm/migration/Makemigrations.js +312 -0
- package/src/orm/migration/MigrationGraph.js +227 -0
- package/src/orm/migration/MigrationRunner.js +202 -108
- package/src/orm/migration/MigrationWriter.js +463 -0
- package/src/orm/migration/ModelInspector.js +412 -344
- package/src/orm/migration/ModelScanner.js +225 -0
- package/src/orm/migration/ProjectState.js +213 -0
- package/src/orm/migration/RenameDetector.js +175 -0
- package/src/orm/migration/SchemaBuilder.js +8 -81
- package/src/orm/migration/operations/base.js +57 -0
- package/src/orm/migration/operations/column.js +191 -0
- package/src/orm/migration/operations/fields.js +252 -0
- package/src/orm/migration/operations/index.js +55 -0
- package/src/orm/migration/operations/models.js +152 -0
- package/src/orm/migration/operations/registry.js +131 -0
- package/src/orm/migration/operations/special.js +51 -0
- package/src/orm/migration/utils.js +208 -0
- package/src/orm/model/Model.js +81 -13
- package/src/providers/AdminServiceProvider.js +66 -9
- package/src/providers/AuthServiceProvider.js +46 -7
- package/src/providers/CacheStorageServiceProvider.js +5 -3
- package/src/providers/DatabaseServiceProvider.js +3 -2
- package/src/providers/EventServiceProvider.js +2 -1
- package/src/providers/LogServiceProvider.js +7 -3
- package/src/providers/MailServiceProvider.js +4 -3
- package/src/providers/QueueServiceProvider.js +4 -3
- package/src/router/Router.js +119 -152
- package/src/scaffold/templates.js +83 -26
- package/src/facades/Validation.js +0 -69
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
2
|
+
DATE PICKER
|
|
3
|
+
═══════════════════════════════════════════════════════════════ */
|
|
4
|
+
|
|
5
|
+
/* Trigger input — looks like a normal form-control */
|
|
6
|
+
.dp-trigger { cursor: pointer; user-select: none; caret-color: transparent; }
|
|
7
|
+
.dp-trigger:focus { border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-soft); }
|
|
8
|
+
|
|
9
|
+
/* Panel wrapper */
|
|
10
|
+
.dp-panel {
|
|
11
|
+
background: var(--surface);
|
|
12
|
+
border: 1px solid var(--border);
|
|
13
|
+
border-radius: var(--radius);
|
|
14
|
+
box-shadow: var(--shadow-lg);
|
|
15
|
+
width: 268px;
|
|
16
|
+
max-width: calc(100vw - 16px);
|
|
17
|
+
min-width: 240px;
|
|
18
|
+
overflow-y: visible !important; /* never scroll the calendar — flip above instead */
|
|
19
|
+
overflow-x: hidden;
|
|
20
|
+
font-size: 13px;
|
|
21
|
+
user-select: none;
|
|
22
|
+
box-sizing: border-box;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Header: ‹ Month Year › */
|
|
26
|
+
.dp-header {
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: space-between;
|
|
30
|
+
padding: 10px 12px 8px;
|
|
31
|
+
border-bottom: 1px solid var(--border-soft);
|
|
32
|
+
}
|
|
33
|
+
.dp-nav-btn {
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
width: 28px;
|
|
38
|
+
height: 28px;
|
|
39
|
+
border: 1px solid var(--border);
|
|
40
|
+
border-radius: var(--radius-sm);
|
|
41
|
+
background: none;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
color: var(--text-muted);
|
|
44
|
+
transition: background .1s, color .1s;
|
|
45
|
+
}
|
|
46
|
+
.dp-nav-btn:hover { background: var(--surface2); color: var(--text); }
|
|
47
|
+
.dp-month-label {
|
|
48
|
+
font-size: 13.5px;
|
|
49
|
+
font-weight: 600;
|
|
50
|
+
color: var(--text);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Weekday headers */
|
|
54
|
+
.dp-weekdays {
|
|
55
|
+
display: grid;
|
|
56
|
+
grid-template-columns: repeat(7, 1fr);
|
|
57
|
+
padding: 6px 10px 2px;
|
|
58
|
+
}
|
|
59
|
+
.dp-day-hdr {
|
|
60
|
+
text-align: center;
|
|
61
|
+
font-size: 11px;
|
|
62
|
+
font-weight: 600;
|
|
63
|
+
color: var(--text-xmuted);
|
|
64
|
+
text-transform: uppercase;
|
|
65
|
+
letter-spacing: .3px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Date grid */
|
|
69
|
+
.dp-grid {
|
|
70
|
+
display: grid;
|
|
71
|
+
grid-template-columns: repeat(7, 1fr);
|
|
72
|
+
padding: 2px 10px 6px;
|
|
73
|
+
gap: 1px;
|
|
74
|
+
}
|
|
75
|
+
.dp-cell {
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
justify-content: center;
|
|
79
|
+
height: 30px;
|
|
80
|
+
border-radius: var(--radius-sm);
|
|
81
|
+
border: none;
|
|
82
|
+
background: none;
|
|
83
|
+
font-size: 12.5px;
|
|
84
|
+
color: var(--text-soft);
|
|
85
|
+
cursor: pointer;
|
|
86
|
+
transition: background .1s, color .1s;
|
|
87
|
+
font-family: inherit;
|
|
88
|
+
}
|
|
89
|
+
.dp-cell:hover { background: var(--surface2); color: var(--text); }
|
|
90
|
+
.dp-muted { color: var(--text-xmuted); }
|
|
91
|
+
.dp-muted:hover { background: none; cursor: default; }
|
|
92
|
+
.dp-today { font-weight: 700; color: var(--primary); }
|
|
93
|
+
.dp-today::after { content: ''; display: block; width: 3px; height: 3px; border-radius: 99px; background: var(--primary); margin: 1px auto 0; }
|
|
94
|
+
.dp-selected { background: var(--primary) !important; color: #fff !important; font-weight: 600; }
|
|
95
|
+
.dp-selected:hover { background: var(--primary-h) !important; }
|
|
96
|
+
|
|
97
|
+
/* Shortcuts */
|
|
98
|
+
.dp-shortcuts {
|
|
99
|
+
display: flex;
|
|
100
|
+
gap: 4px;
|
|
101
|
+
padding: 6px 10px;
|
|
102
|
+
border-top: 1px solid var(--border-soft);
|
|
103
|
+
}
|
|
104
|
+
.dp-shortcut {
|
|
105
|
+
flex: 1;
|
|
106
|
+
padding: 5px 0;
|
|
107
|
+
border: 1px solid var(--border);
|
|
108
|
+
border-radius: var(--radius-sm);
|
|
109
|
+
background: none;
|
|
110
|
+
font-size: 11.5px;
|
|
111
|
+
font-weight: 500;
|
|
112
|
+
color: var(--text-muted);
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
font-family: inherit;
|
|
115
|
+
transition: background .1s, color .1s, border-color .1s;
|
|
116
|
+
text-align: center;
|
|
117
|
+
}
|
|
118
|
+
.dp-shortcut:hover { background: var(--primary-soft); color: var(--primary); border-color: var(--primary-dim); }
|
|
119
|
+
|
|
120
|
+
/* Time row (datetime mode only) */
|
|
121
|
+
.dp-time-row {
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
gap: 10px;
|
|
125
|
+
padding: 8px 12px;
|
|
126
|
+
border-top: 1px solid var(--border-soft);
|
|
127
|
+
}
|
|
128
|
+
.dp-time-label { font-size: 12px; font-weight: 500; color: var(--text-muted); flex-shrink: 0; }
|
|
129
|
+
.dp-time-input {
|
|
130
|
+
flex: 1;
|
|
131
|
+
border: 1px solid var(--border);
|
|
132
|
+
border-radius: var(--radius-sm);
|
|
133
|
+
padding: 5px 8px;
|
|
134
|
+
font-size: 13px;
|
|
135
|
+
font-family: 'DM Mono', monospace;
|
|
136
|
+
color: var(--text);
|
|
137
|
+
background: var(--surface2);
|
|
138
|
+
outline: none;
|
|
139
|
+
}
|
|
140
|
+
.dp-time-input:focus { border-color: var(--primary); background: var(--surface); box-shadow: 0 0 0 2px var(--primary-soft); }
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
/* dp-wrap: positions the calendar icon inside the trigger input */
|
|
145
|
+
.dp-wrap {
|
|
146
|
+
position: relative;
|
|
147
|
+
}
|
|
148
|
+
.dp-wrap .form-control { padding-right: 34px; }
|
|
149
|
+
.dp-icon {
|
|
150
|
+
position: absolute;
|
|
151
|
+
right: 10px;
|
|
152
|
+
top: 50%;
|
|
153
|
+
transform: translateY(-50%);
|
|
154
|
+
color: var(--text-muted);
|
|
155
|
+
pointer-events: none;
|
|
156
|
+
display: flex;
|
|
157
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* date-picker.js — Custom date/datetime picker for Millas Admin
|
|
3
|
+
*
|
|
4
|
+
* Django-style: month nav header, week labels, date grid, shortcuts.
|
|
5
|
+
* Portal-rendered via UI.Dropdown so it is never clipped by parent overflow.
|
|
6
|
+
* Works for both `date` and `datetime-local` fields.
|
|
7
|
+
*
|
|
8
|
+
* Usage (automatic — attaches to all .dp-trigger inputs on DOMContentLoaded):
|
|
9
|
+
* <input type="text" class="dp-trigger form-control"
|
|
10
|
+
* data-target="field-my_date"
|
|
11
|
+
* data-mode="date"> <!-- or "datetime" -->
|
|
12
|
+
*
|
|
13
|
+
* Exposed on window.DatePicker for programmatic use.
|
|
14
|
+
*/
|
|
15
|
+
(function ($, root) {
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
var DAYS_SHORT = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
|
19
|
+
var MONTHS = ['January','February','March','April','May','June',
|
|
20
|
+
'July','August','September','October','November','December'];
|
|
21
|
+
|
|
22
|
+
// ── Shared calendar panel (one, reused) ───────────────────────────────────
|
|
23
|
+
var _panel = null; // DOM element
|
|
24
|
+
var _dd = null; // UI.Dropdown instance
|
|
25
|
+
var _target = null; // current hidden input el
|
|
26
|
+
var _trigger = null; // current trigger input el
|
|
27
|
+
var _mode = 'date'; // 'date' | 'datetime'
|
|
28
|
+
var _current = null; // Date being viewed (month/year nav)
|
|
29
|
+
var _selected = null; // currently selected Date
|
|
30
|
+
|
|
31
|
+
function _buildPanel() {
|
|
32
|
+
var el = document.createElement('div');
|
|
33
|
+
el.className = 'dp-panel';
|
|
34
|
+
el.innerHTML =
|
|
35
|
+
'<div class="dp-header">' +
|
|
36
|
+
'<button type="button" class="dp-nav-btn" id="dp-prev" title="Previous month">' +
|
|
37
|
+
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>' +
|
|
38
|
+
'</button>' +
|
|
39
|
+
'<span class="dp-month-label" id="dp-month-label"></span>' +
|
|
40
|
+
'<button type="button" class="dp-nav-btn" id="dp-next" title="Next month">' +
|
|
41
|
+
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>' +
|
|
42
|
+
'</button>' +
|
|
43
|
+
'</div>' +
|
|
44
|
+
'<div class="dp-weekdays" id="dp-weekdays"></div>' +
|
|
45
|
+
'<div class="dp-grid" id="dp-grid"></div>' +
|
|
46
|
+
'<div class="dp-shortcuts">' +
|
|
47
|
+
'<button type="button" class="dp-shortcut" data-offset="-1">Yesterday</button>' +
|
|
48
|
+
'<button type="button" class="dp-shortcut" data-offset="0">Today</button>' +
|
|
49
|
+
'<button type="button" class="dp-shortcut" data-offset="1">Tomorrow</button>' +
|
|
50
|
+
'</div>' +
|
|
51
|
+
'<div class="dp-time-row" id="dp-time-row" style="display:none">' +
|
|
52
|
+
'<label class="dp-time-label">Time</label>' +
|
|
53
|
+
'<input type="time" class="dp-time-input" id="dp-time-input" step="60">' +
|
|
54
|
+
'</div>' +
|
|
55
|
+
'';
|
|
56
|
+
|
|
57
|
+
// Weekday headers
|
|
58
|
+
var $days = el.querySelector('#dp-weekdays');
|
|
59
|
+
DAYS_SHORT.forEach(function (d) {
|
|
60
|
+
var s = document.createElement('span');
|
|
61
|
+
s.className = 'dp-day-hdr';
|
|
62
|
+
s.textContent = d;
|
|
63
|
+
$days.appendChild(s);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Month nav
|
|
67
|
+
el.querySelector('#dp-prev').addEventListener('click', function () {
|
|
68
|
+
_current.setMonth(_current.getMonth() - 1);
|
|
69
|
+
_renderGrid();
|
|
70
|
+
});
|
|
71
|
+
el.querySelector('#dp-next').addEventListener('click', function () {
|
|
72
|
+
_current.setMonth(_current.getMonth() + 1);
|
|
73
|
+
_renderGrid();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Shortcuts
|
|
77
|
+
el.querySelectorAll('.dp-shortcut').forEach(function (btn) {
|
|
78
|
+
btn.addEventListener('click', function () {
|
|
79
|
+
var offset = parseInt(btn.dataset.offset);
|
|
80
|
+
var d = new Date();
|
|
81
|
+
d.setDate(d.getDate() + offset);
|
|
82
|
+
_selected = d;
|
|
83
|
+
_current = new Date(d.getFullYear(), d.getMonth(), 1);
|
|
84
|
+
_renderGrid();
|
|
85
|
+
if (_mode === 'date') _apply();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
return el;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function _renderGrid() {
|
|
94
|
+
var label = el('#dp-month-label');
|
|
95
|
+
if (label) label.textContent = MONTHS[_current.getMonth()] + ' ' + _current.getFullYear();
|
|
96
|
+
|
|
97
|
+
var grid = el('#dp-grid');
|
|
98
|
+
if (!grid) return;
|
|
99
|
+
grid.innerHTML = '';
|
|
100
|
+
|
|
101
|
+
var today = new Date();
|
|
102
|
+
today.setHours(0,0,0,0);
|
|
103
|
+
|
|
104
|
+
var year = _current.getFullYear();
|
|
105
|
+
var month = _current.getMonth();
|
|
106
|
+
|
|
107
|
+
// First day of month (0=Sun)
|
|
108
|
+
var firstDay = new Date(year, month, 1).getDay();
|
|
109
|
+
// Days in month
|
|
110
|
+
var daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
111
|
+
// Days in prev month
|
|
112
|
+
var daysInPrev = new Date(year, month, 0).getDate();
|
|
113
|
+
|
|
114
|
+
// Previous month filler
|
|
115
|
+
for (var p = firstDay - 1; p >= 0; p--) {
|
|
116
|
+
grid.appendChild(_cell(daysInPrev - p, false, false, true));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Current month days
|
|
120
|
+
for (var d = 1; d <= daysInMonth; d++) {
|
|
121
|
+
var date = new Date(year, month, d);
|
|
122
|
+
var isTod = date.getTime() === today.getTime();
|
|
123
|
+
var isSel = _selected
|
|
124
|
+
? (date.getFullYear() === _selected.getFullYear() &&
|
|
125
|
+
date.getMonth() === _selected.getMonth() &&
|
|
126
|
+
date.getDate() === _selected.getDate())
|
|
127
|
+
: false;
|
|
128
|
+
grid.appendChild(_cell(d, isTod, isSel, false, date));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Next month filler
|
|
132
|
+
var total = firstDay + daysInMonth;
|
|
133
|
+
var rem = total % 7 === 0 ? 0 : 7 - (total % 7);
|
|
134
|
+
for (var n = 1; n <= rem; n++) {
|
|
135
|
+
grid.appendChild(_cell(n, false, false, true));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Time row visibility
|
|
139
|
+
var timeRow = el('#dp-time-row');
|
|
140
|
+
if (timeRow) timeRow.style.display = _mode === 'datetime' ? 'flex' : 'none';
|
|
141
|
+
|
|
142
|
+
// Reposition dropdown after content change
|
|
143
|
+
if (_dd) _dd.reposition();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function _cell(day, isToday, isSelected, isMuted, date) {
|
|
147
|
+
var btn = document.createElement('button');
|
|
148
|
+
btn.type = 'button';
|
|
149
|
+
btn.className = 'dp-cell' +
|
|
150
|
+
(isToday ? ' dp-today' : '') +
|
|
151
|
+
(isSelected ? ' dp-selected' : '') +
|
|
152
|
+
(isMuted ? ' dp-muted' : '');
|
|
153
|
+
btn.textContent = day;
|
|
154
|
+
if (date) {
|
|
155
|
+
btn.addEventListener('click', function () {
|
|
156
|
+
_selected = date;
|
|
157
|
+
_renderGrid(); // re-render to update selection highlight
|
|
158
|
+
if (_mode === 'date') {
|
|
159
|
+
_apply();
|
|
160
|
+
}
|
|
161
|
+
// datetime: auto-apply immediately too
|
|
162
|
+
_apply();
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return btn;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function _apply() {
|
|
169
|
+
if (!_selected || !_target || !_trigger) return;
|
|
170
|
+
|
|
171
|
+
var y = _selected.getFullYear();
|
|
172
|
+
var mo = String(_selected.getMonth() + 1).padStart(2, '0');
|
|
173
|
+
var d = String(_selected.getDate()).padStart(2, '0');
|
|
174
|
+
|
|
175
|
+
if (_mode === 'datetime') {
|
|
176
|
+
var timeInput = el('#dp-time-input');
|
|
177
|
+
var time = (timeInput && timeInput.value) ? timeInput.value : '00:00';
|
|
178
|
+
_target.value = y + '-' + mo + '-' + d + 'T' + time;
|
|
179
|
+
_trigger.value = _formatDisplay(y + '-' + mo + '-' + d, time);
|
|
180
|
+
} else {
|
|
181
|
+
_target.value = y + '-' + mo + '-' + d;
|
|
182
|
+
_trigger.value = _formatDisplayDate(y, mo, d);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Fire change on hidden input
|
|
186
|
+
_target.dispatchEvent(new Event('change', { bubbles: true }));
|
|
187
|
+
_close();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function _formatDisplayDate(y, mo, d) {
|
|
191
|
+
return MONTHS[parseInt(mo) - 1].slice(0, 3) + ' ' + parseInt(d) + ', ' + y;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function _formatDisplay(dateStr, time) {
|
|
195
|
+
var parts = dateStr.split('-');
|
|
196
|
+
return _formatDisplayDate(parts[0], parts[1], parts[2]) + ' ' + time;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function _close() {
|
|
200
|
+
if (_dd) _dd.close();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function el(id) {
|
|
204
|
+
return _panel ? _panel.querySelector(id) : null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── Open ──────────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
function open(triggerEl, targetEl, mode) {
|
|
210
|
+
_trigger = triggerEl;
|
|
211
|
+
_target = targetEl;
|
|
212
|
+
_mode = mode || 'date';
|
|
213
|
+
|
|
214
|
+
// Parse existing value
|
|
215
|
+
var raw = targetEl.value;
|
|
216
|
+
if (raw) {
|
|
217
|
+
var d = new Date(raw);
|
|
218
|
+
if (!isNaN(d.getTime())) {
|
|
219
|
+
_selected = d;
|
|
220
|
+
// Pre-fill time input for datetime
|
|
221
|
+
if (_mode === 'datetime') {
|
|
222
|
+
var h = String(d.getHours()).padStart(2, '0');
|
|
223
|
+
var m = String(d.getMinutes()).padStart(2, '0');
|
|
224
|
+
var timeEl = _panel.querySelector('#dp-time-input');
|
|
225
|
+
if (timeEl) timeEl.value = h + ':' + m;
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
_selected = null;
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
_selected = null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Navigate to selected month or today
|
|
235
|
+
var nav = _selected ? new Date(_selected) : new Date();
|
|
236
|
+
_current = new Date(nav.getFullYear(), nav.getMonth(), 1);
|
|
237
|
+
|
|
238
|
+
_renderGrid();
|
|
239
|
+
|
|
240
|
+
// Reposition after render so the dropdown reads the panel's actual
|
|
241
|
+
// height (which varies: 5-row vs 6-row month, time row visible or not)
|
|
242
|
+
// and flips above the anchor if there isn't enough room below.
|
|
243
|
+
if (_dd && _dd.isOpen()) {
|
|
244
|
+
_dd.reposition();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Create a new dropdown each open (anchor may differ)
|
|
248
|
+
if (_dd) _dd.destroy();
|
|
249
|
+
_dd = UI.Dropdown.create({
|
|
250
|
+
anchor: triggerEl,
|
|
251
|
+
content: _panel,
|
|
252
|
+
placement: 'bottom-start',
|
|
253
|
+
offset: 4,
|
|
254
|
+
maxHeight: 440, // tallest month (6 rows) + shortcuts + time row ≈ 400px
|
|
255
|
+
onClose: function () { _dd = null; },
|
|
256
|
+
});
|
|
257
|
+
_dd.open();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── Init ──────────────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
function init() {
|
|
263
|
+
_panel = _buildPanel();
|
|
264
|
+
|
|
265
|
+
document.querySelectorAll('.dp-trigger').forEach(function (trigger) {
|
|
266
|
+
var targetId = trigger.dataset.target;
|
|
267
|
+
var mode = trigger.dataset.mode || 'date';
|
|
268
|
+
var target = document.getElementById(targetId);
|
|
269
|
+
if (!target) return;
|
|
270
|
+
|
|
271
|
+
// Show formatted display value on load
|
|
272
|
+
if (target.value) {
|
|
273
|
+
var d = new Date(target.value);
|
|
274
|
+
if (!isNaN(d.getTime())) {
|
|
275
|
+
if (mode === 'datetime') {
|
|
276
|
+
var h = String(d.getHours()).padStart(2,'0');
|
|
277
|
+
var m = String(d.getMinutes()).padStart(2,'0');
|
|
278
|
+
trigger.value = _formatDisplay(
|
|
279
|
+
d.getFullYear() + '-' +
|
|
280
|
+
String(d.getMonth()+1).padStart(2,'0') + '-' +
|
|
281
|
+
String(d.getDate()).padStart(2,'0'),
|
|
282
|
+
h + ':' + m
|
|
283
|
+
);
|
|
284
|
+
} else {
|
|
285
|
+
trigger.value = _formatDisplayDate(
|
|
286
|
+
d.getFullYear(),
|
|
287
|
+
String(d.getMonth()+1).padStart(2,'0'),
|
|
288
|
+
String(d.getDate()).padStart(2,'0')
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
trigger.addEventListener('click', function () {
|
|
295
|
+
open(trigger, target, mode);
|
|
296
|
+
});
|
|
297
|
+
trigger.addEventListener('keydown', function (e) {
|
|
298
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(trigger, target, mode); }
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Make it read-only so browser native picker doesn't interfere
|
|
302
|
+
trigger.setAttribute('readonly', 'readonly');
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
$(function () {
|
|
307
|
+
if (typeof UI === 'undefined') {
|
|
308
|
+
console.warn('[DatePicker] UI not loaded — date picker will not initialise');
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
init();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
root.DatePicker = { open: open, init: init };
|
|
315
|
+
|
|
316
|
+
}(jQuery, window));
|