@redkhat/timepicker 0.0.1
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/LICENSE.md +9 -0
- package/README.md +219 -0
- package/_rk-timepicker-theme.scss +289 -0
- package/fesm2022/redkhat-timepicker.mjs +1415 -0
- package/fesm2022/redkhat-timepicker.mjs.map +1 -0
- package/index.d.ts +5 -0
- package/lib/clock.d.ts +18 -0
- package/lib/timepicker-dial.d.ts +39 -0
- package/lib/timepicker-input-label.d.ts +28 -0
- package/lib/timepicker-input.d.ts +51 -0
- package/lib/timepicker-toggle.d.ts +13 -0
- package/lib/timepicker.d.ts +42 -0
- package/lib/utils/angle.d.ts +7 -0
- package/lib/utils/animation.d.ts +1 -0
- package/lib/utils/clock-iterable.d.ts +42 -0
- package/lib/utils/constants.d.ts +4 -0
- package/lib/utils/events.d.ts +16 -0
- package/lib/utils/index.d.ts +4 -0
- package/lib/utils/time.d.ts +39 -0
- package/package.json +46 -0
- package/public-api.d.ts +5 -0
@@ -0,0 +1,1415 @@
|
|
1
|
+
import * as i0 from '@angular/core';
|
2
|
+
import { model, computed, viewChild, inject, ElementRef, Renderer2, DestroyRef, Injector, signal, afterNextRender, effect, untracked, Component, ChangeDetectionStrategy, ViewEncapsulation, input, output, afterRender, booleanAttribute, Directive } from '@angular/core';
|
3
|
+
import { DOCUMENT, NgClass } from '@angular/common';
|
4
|
+
import { MatButton, MatIconButton } from '@angular/material/button';
|
5
|
+
import { MatIcon } from '@angular/material/icon';
|
6
|
+
import { Dialog, DialogModule } from '@angular/cdk/dialog';
|
7
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
8
|
+
import { fromEvent, merge, tap, switchMap, takeUntil } from 'rxjs';
|
9
|
+
import * as i1 from '@angular/material/core';
|
10
|
+
import { MatRippleModule } from '@angular/material/core';
|
11
|
+
import { FormsModule, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
|
12
|
+
import { MAT_INPUT_VALUE_ACCESSOR } from '@angular/material/input';
|
13
|
+
|
14
|
+
/**
|
15
|
+
* A class that implements the `Iterable` interface to generate coordinates for clock labels.
|
16
|
+
*
|
17
|
+
* @template T - The type of the labels.
|
18
|
+
*/
|
19
|
+
class ClockIterable {
|
20
|
+
/**
|
21
|
+
* The labels to be positioned around the clock.
|
22
|
+
*/
|
23
|
+
labels;
|
24
|
+
/**
|
25
|
+
* The radius of the clock.
|
26
|
+
*/
|
27
|
+
radio;
|
28
|
+
/**
|
29
|
+
* The size of each label.
|
30
|
+
*/
|
31
|
+
labelSize;
|
32
|
+
/**
|
33
|
+
* The inset distance from the edge of the clock.
|
34
|
+
*/
|
35
|
+
inset;
|
36
|
+
/**
|
37
|
+
* The degrees between each label.
|
38
|
+
*/
|
39
|
+
degreesByNumber = 30;
|
40
|
+
/**
|
41
|
+
* Creates an instance of ClockIterable.
|
42
|
+
*
|
43
|
+
* @param labels - An array of labels to be positioned around the clock [12, 1, 2, 3...].
|
44
|
+
* @param labelSize - The size of each label.
|
45
|
+
* @param diameter - The diameter of the clock.
|
46
|
+
* @param inset - The inset distance from the edge of the clock (default is 0).
|
47
|
+
*/
|
48
|
+
constructor(labels, labelSize, diameter, inset = 0) {
|
49
|
+
this.labels = labels;
|
50
|
+
this.labelSize = labelSize / 2;
|
51
|
+
this.radio = (diameter / 2) - inset;
|
52
|
+
this.inset = inset;
|
53
|
+
}
|
54
|
+
/**
|
55
|
+
* Returns an iterator that yields the label and its x, y coordinates.
|
56
|
+
*
|
57
|
+
* @returns An iterator that yields a tuple containing the label and its x, y coordinates.
|
58
|
+
*/
|
59
|
+
*[Symbol.iterator]() {
|
60
|
+
for (let i = 0; i < this.labels.length; i++) {
|
61
|
+
const x = this.radio + (this.radio - this.labelSize) * Math.sin(i * this.degreesByNumber * Math.PI / 180);
|
62
|
+
const y = this.radio - (this.radio - this.labelSize) * Math.cos(i * this.degreesByNumber * Math.PI / 180);
|
63
|
+
yield [this.labels[i], this.inset + x - this.labelSize, this.inset + y - this.labelSize];
|
64
|
+
}
|
65
|
+
}
|
66
|
+
}
|
67
|
+
|
68
|
+
/**
|
69
|
+
* Converts a time string or number to the number of seconds since midnight.
|
70
|
+
* Supports various formats including:
|
71
|
+
* - Integers (e.g., "10", "1030", 10, 1030) representing hours in 24-hour format (up to 2359).
|
72
|
+
* - Space-separated numbers (e.g., "10 30") representing HH MM.
|
73
|
+
* - Colon-separated numbers (e.g., "10:30") representing HH:MM.
|
74
|
+
* - Colon-separated numbers with AM/PM (e.g., "10:30am", "10:30 am").
|
75
|
+
*
|
76
|
+
* @param input The input to convert (can be a string or a number).
|
77
|
+
* @returns The number of seconds since midnight, or null if the input is invalid.
|
78
|
+
*/
|
79
|
+
function timeToSeconds(input) {
|
80
|
+
const inputStr = typeof input === 'number' ? String(input) : input;
|
81
|
+
// Handle numeric cases (number or string numeric)
|
82
|
+
if (/^\d+$/.test(inputStr)) {
|
83
|
+
const num = parseInt(inputStr, 10);
|
84
|
+
if (num < 0 || num > 2359)
|
85
|
+
return null;
|
86
|
+
const len = inputStr.length;
|
87
|
+
let hour, minute;
|
88
|
+
if (len <= 2) {
|
89
|
+
hour = num;
|
90
|
+
minute = 0;
|
91
|
+
}
|
92
|
+
else if (len === 3) {
|
93
|
+
hour = parseInt(inputStr[0], 10);
|
94
|
+
minute = parseInt(inputStr.substring(1), 10);
|
95
|
+
}
|
96
|
+
else {
|
97
|
+
hour = parseInt(inputStr.substring(0, 2), 10);
|
98
|
+
minute = parseInt(inputStr.substring(2), 10);
|
99
|
+
}
|
100
|
+
return (hour <= 23 && minute <= 59) ? hour * 3600 + minute * 60 : null;
|
101
|
+
}
|
102
|
+
// Handle formats like "1pm", "1 pm", "10am", "10 AM"
|
103
|
+
const ampmMatch = inputStr.match(/^(\d{1,2})\s?([APap][Mm])$/i);
|
104
|
+
if (ampmMatch) {
|
105
|
+
let hour = parseInt(ampmMatch[1], 10);
|
106
|
+
const period = ampmMatch[2].toUpperCase();
|
107
|
+
if (hour < 1 || hour > 12)
|
108
|
+
return null;
|
109
|
+
if (period === 'PM') {
|
110
|
+
hour = hour === 12 ? 12 : hour + 12;
|
111
|
+
}
|
112
|
+
else {
|
113
|
+
hour = hour === 12 ? 0 : hour;
|
114
|
+
}
|
115
|
+
return hour * 3600; // No minutes, so default to 0 minutes
|
116
|
+
}
|
117
|
+
// Handle format with colon and AM/PM (e.g., "1:00pm", "1:00 PM")
|
118
|
+
const colonAmpmMatch = inputStr.match(/^(\d{1,2}):(\d{1,2})\s?([APap][Mm])$/i);
|
119
|
+
if (colonAmpmMatch) {
|
120
|
+
let hour = parseInt(colonAmpmMatch[1], 10);
|
121
|
+
const minute = parseInt(colonAmpmMatch[2], 10);
|
122
|
+
const period = colonAmpmMatch[3].toUpperCase();
|
123
|
+
if (hour < 1 || hour > 12 || minute > 59)
|
124
|
+
return null;
|
125
|
+
if (period === 'PM')
|
126
|
+
hour = hour === 12 ? 12 : hour + 12;
|
127
|
+
else
|
128
|
+
hour = hour === 12 ? 0 : hour;
|
129
|
+
return hour * 3600 + minute * 60;
|
130
|
+
}
|
131
|
+
// Handle cases with separators
|
132
|
+
const [hPart, mPart] = inputStr.split(/[\s:]/);
|
133
|
+
if (hPart && mPart && /^\d+$/.test(hPart) && /^\d+$/.test(mPart)) {
|
134
|
+
const hour = parseInt(hPart, 10);
|
135
|
+
const minute = parseInt(mPart, 10);
|
136
|
+
if (hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59) {
|
137
|
+
return hour * 3600 + minute * 60;
|
138
|
+
}
|
139
|
+
}
|
140
|
+
return null;
|
141
|
+
}
|
142
|
+
function secondsToDate(seconds) {
|
143
|
+
const initialDate = new Date(0, 0, 0, 0);
|
144
|
+
const date = new Date(initialDate.getTime() + seconds * 1000);
|
145
|
+
return date;
|
146
|
+
}
|
147
|
+
function detectTimeFormat(date) {
|
148
|
+
const localTime = date.toLocaleTimeString();
|
149
|
+
if (localTime.includes('AM') || localTime.includes('PM')) {
|
150
|
+
return '12h';
|
151
|
+
}
|
152
|
+
else {
|
153
|
+
return '24h';
|
154
|
+
}
|
155
|
+
}
|
156
|
+
function splitDate(date) {
|
157
|
+
const hours = date.getHours();
|
158
|
+
const minutes = date.getMinutes();
|
159
|
+
return { hours, minutes };
|
160
|
+
}
|
161
|
+
function amPmTransform(isAm, date) {
|
162
|
+
const newDate = new Date(date);
|
163
|
+
const tempHours = newDate.getHours();
|
164
|
+
if (isAm === 'AM' && tempHours >= 12) {
|
165
|
+
newDate.setHours(tempHours - 12);
|
166
|
+
}
|
167
|
+
else if (isAm === 'PM' && tempHours < 12) {
|
168
|
+
newDate.setHours(tempHours + 12);
|
169
|
+
}
|
170
|
+
return newDate;
|
171
|
+
}
|
172
|
+
function validateTimeValue(value, max) {
|
173
|
+
let numericValue = value.replace(/[^0-9]/g, '');
|
174
|
+
if (numericValue.length > 2) {
|
175
|
+
numericValue = numericValue.slice(0, 2);
|
176
|
+
}
|
177
|
+
if (numericValue === '') {
|
178
|
+
return '';
|
179
|
+
}
|
180
|
+
const num = parseInt(numericValue, 10);
|
181
|
+
return num > max ? max.toString() : numericValue;
|
182
|
+
}
|
183
|
+
function formatTimeValue(value, defaultValue, padLength = 2) {
|
184
|
+
if (value === '') {
|
185
|
+
return defaultValue;
|
186
|
+
}
|
187
|
+
const num = parseInt(value, 10);
|
188
|
+
if (num === 0) {
|
189
|
+
return defaultValue;
|
190
|
+
}
|
191
|
+
return value.padStart(padLength, '0');
|
192
|
+
}
|
193
|
+
/**
|
194
|
+
* Converts a time string or number to the number of seconds since midnight.
|
195
|
+
*
|
196
|
+
* Supported formats include:
|
197
|
+
* - Integers (e.g., "10", "1030", 10, 1030) in 24-hour format (up to 2359).
|
198
|
+
* - Space-separated numbers (e.g., "10 30") as HH MM.
|
199
|
+
* - Colon-separated numbers (e.g., "10:30" or "10:30:00") as HH:MM or HH:MM:SS.
|
200
|
+
* - Formats with regional AM/PM markers either at the beginning or at the end
|
201
|
+
* (e.g., "10:30am", "am 10:30", "午後3:25:00", "10:30 du matin").
|
202
|
+
*
|
203
|
+
* Additionally, this function normalizes Arabic digits (Unicode range \u0660-\u0669)
|
204
|
+
* to their Western counterparts before processing.
|
205
|
+
*
|
206
|
+
* @param input The input to convert (can be a string or number).
|
207
|
+
* @returns The number of seconds since midnight, or null if the input is invalid.
|
208
|
+
*/
|
209
|
+
function timeToSecondsi18n(input) {
|
210
|
+
// Convert input to string and trim whitespace.
|
211
|
+
let inputStr = typeof input === 'number' ? String(input) : input;
|
212
|
+
inputStr = inputStr.trim();
|
213
|
+
// Normalize Arabic digits to Western digits if found.
|
214
|
+
if (/[\u0660-\u0669]/.test(inputStr)) {
|
215
|
+
inputStr = inputStr.replace(/[\u0660-\u0669]/g, d => (d.charCodeAt(0) - 0x0660).toString());
|
216
|
+
}
|
217
|
+
// Extended list of AM and PM markers across various regions/languages.
|
218
|
+
const AM_MARKERS = [
|
219
|
+
'am', 'a.m.', 'a.m', // English
|
220
|
+
'午前', // Japanese
|
221
|
+
'上午', // Chinese
|
222
|
+
'오전', // Korean
|
223
|
+
'ص', 'صباح', 'صباحاً', // Arabic
|
224
|
+
'du matin', 'matin' // French
|
225
|
+
];
|
226
|
+
const PM_MARKERS = [
|
227
|
+
'pm', 'p.m.', 'p.m', // English
|
228
|
+
'午後', // Japanese
|
229
|
+
'下午', // Chinese
|
230
|
+
'오후', // Korean
|
231
|
+
'م', 'مساء', 'مساءً', // Arabic
|
232
|
+
"de l’après-midi", "de l'aprés-midi", "de l'après-midi",
|
233
|
+
'après-midi', 'du soir', 'soir' // French
|
234
|
+
];
|
235
|
+
let period = null;
|
236
|
+
// Helper functions to compare prefix/suffix ignoring case.
|
237
|
+
const startsWithIgnoreCase = (str, prefix) => str.substring(0, prefix.length).toLowerCase() === prefix.toLowerCase();
|
238
|
+
const endsWithIgnoreCase = (str, suffix) => str.substring(str.length - suffix.length).toLowerCase() === suffix.toLowerCase();
|
239
|
+
// Check if the period marker is at the beginning.
|
240
|
+
for (const marker of AM_MARKERS) {
|
241
|
+
if (startsWithIgnoreCase(inputStr, marker)) {
|
242
|
+
period = 'AM';
|
243
|
+
inputStr = inputStr.substring(marker.length).trim();
|
244
|
+
break;
|
245
|
+
}
|
246
|
+
}
|
247
|
+
if (!period) {
|
248
|
+
for (const marker of PM_MARKERS) {
|
249
|
+
if (startsWithIgnoreCase(inputStr, marker)) {
|
250
|
+
period = 'PM';
|
251
|
+
inputStr = inputStr.substring(marker.length).trim();
|
252
|
+
break;
|
253
|
+
}
|
254
|
+
}
|
255
|
+
}
|
256
|
+
// If not found at the beginning, check at the end.
|
257
|
+
if (!period) {
|
258
|
+
for (const marker of AM_MARKERS) {
|
259
|
+
if (endsWithIgnoreCase(inputStr, marker)) {
|
260
|
+
period = 'AM';
|
261
|
+
inputStr = inputStr.substring(0, inputStr.length - marker.length).trim();
|
262
|
+
break;
|
263
|
+
}
|
264
|
+
}
|
265
|
+
}
|
266
|
+
if (!period) {
|
267
|
+
for (const marker of PM_MARKERS) {
|
268
|
+
if (endsWithIgnoreCase(inputStr, marker)) {
|
269
|
+
period = 'PM';
|
270
|
+
inputStr = inputStr.substring(0, inputStr.length - marker.length).trim();
|
271
|
+
break;
|
272
|
+
}
|
273
|
+
}
|
274
|
+
}
|
275
|
+
// Split the numeric part of the time.
|
276
|
+
let timeParts = [];
|
277
|
+
if (inputStr.includes(':')) {
|
278
|
+
// Colon-separated format. Accepts HH:MM or HH:MM:SS.
|
279
|
+
const parts = inputStr.split(':');
|
280
|
+
if (parts.length < 2 || parts.length > 3)
|
281
|
+
return null;
|
282
|
+
for (const part of parts) {
|
283
|
+
const num = parseInt(part, 10);
|
284
|
+
if (isNaN(num))
|
285
|
+
return null;
|
286
|
+
timeParts.push(num);
|
287
|
+
}
|
288
|
+
}
|
289
|
+
else if (inputStr.includes(' ')) {
|
290
|
+
// Space-separated format (e.g., "10 30").
|
291
|
+
const parts = inputStr.split(/\s+/);
|
292
|
+
if (parts.length > 3)
|
293
|
+
return null;
|
294
|
+
for (const part of parts) {
|
295
|
+
const num = parseInt(part, 10);
|
296
|
+
if (isNaN(num))
|
297
|
+
return null;
|
298
|
+
timeParts.push(num);
|
299
|
+
}
|
300
|
+
}
|
301
|
+
else if (/^\d+$/.test(inputStr)) {
|
302
|
+
// Numeric case without separators: "10", "1030".
|
303
|
+
if (inputStr.length <= 2) {
|
304
|
+
timeParts = [parseInt(inputStr, 10)];
|
305
|
+
}
|
306
|
+
else if (inputStr.length === 3) {
|
307
|
+
timeParts = [
|
308
|
+
parseInt(inputStr.substring(0, 1), 10),
|
309
|
+
parseInt(inputStr.substring(1), 10),
|
310
|
+
];
|
311
|
+
}
|
312
|
+
else if (inputStr.length === 4) {
|
313
|
+
timeParts = [
|
314
|
+
parseInt(inputStr.substring(0, 2), 10),
|
315
|
+
parseInt(inputStr.substring(2), 10),
|
316
|
+
];
|
317
|
+
}
|
318
|
+
else {
|
319
|
+
// Unsupported numeric format with more than 4 digits.
|
320
|
+
return null;
|
321
|
+
}
|
322
|
+
}
|
323
|
+
else {
|
324
|
+
// Unrecognized format.
|
325
|
+
return null;
|
326
|
+
}
|
327
|
+
// Extract hour, minute, and second (defaulting to 0 if not provided).
|
328
|
+
let hour = timeParts[0];
|
329
|
+
let minute = timeParts.length >= 2 ? timeParts[1] : 0;
|
330
|
+
let second = timeParts.length === 3 ? timeParts[2] : 0;
|
331
|
+
// Validate minute and second ranges.
|
332
|
+
if (minute < 0 || minute > 59)
|
333
|
+
return null;
|
334
|
+
if (second < 0 || second > 59)
|
335
|
+
return null;
|
336
|
+
// Adjust hour based on the AM/PM marker.
|
337
|
+
if (period) {
|
338
|
+
if (hour < 1 || hour > 12)
|
339
|
+
return null;
|
340
|
+
if (period === 'PM') {
|
341
|
+
hour = hour === 12 ? 12 : hour + 12;
|
342
|
+
}
|
343
|
+
else {
|
344
|
+
hour = hour === 12 ? 0 : hour;
|
345
|
+
}
|
346
|
+
}
|
347
|
+
else {
|
348
|
+
// Without a period, assume 24-hour format.
|
349
|
+
if (hour < 0 || hour > 23)
|
350
|
+
return null;
|
351
|
+
}
|
352
|
+
return hour * 3600 + minute * 60 + second;
|
353
|
+
}
|
354
|
+
|
355
|
+
function cubicBezier(p1x, p1y, p2x, p2y, t) {
|
356
|
+
const cx = 3.0 * p1x;
|
357
|
+
const bx = 3.0 * (p2x - p1x) - cx;
|
358
|
+
const ax = 1.0 - cx - bx;
|
359
|
+
const cy = 3.0 * p1y;
|
360
|
+
const by = 3.0 * (p2y - p1y) - cy;
|
361
|
+
const ay = 1.0 - cy - by;
|
362
|
+
let x = t;
|
363
|
+
for (let i = 0; i < 4; i++) {
|
364
|
+
const t_0 = x * x * x * ax + x * x * bx + x * cx;
|
365
|
+
const t_1 = 3 * x * x * ax + 2 * x * bx + cx;
|
366
|
+
x = x - (t_0 - t) / t_1;
|
367
|
+
}
|
368
|
+
return x * x * x * ay + x * x * by + x * cy;
|
369
|
+
}
|
370
|
+
|
371
|
+
function normalizeEvent(event) {
|
372
|
+
if (event instanceof MouseEvent) {
|
373
|
+
return event;
|
374
|
+
}
|
375
|
+
else if (event instanceof TouchEvent) {
|
376
|
+
return event.touches[0] || event.changedTouches[0];
|
377
|
+
}
|
378
|
+
else if (event instanceof Touch) {
|
379
|
+
return {
|
380
|
+
clientX: event.clientX,
|
381
|
+
clientY: event.clientY,
|
382
|
+
pageX: event.pageX,
|
383
|
+
pageY: event.pageY,
|
384
|
+
screenX: event.screenX,
|
385
|
+
screenY: event.screenY,
|
386
|
+
target: event.target
|
387
|
+
};
|
388
|
+
}
|
389
|
+
return event;
|
390
|
+
}
|
391
|
+
function createPointerEvents(element) {
|
392
|
+
const pointerDown$ = fromEvent(element, 'pointerdown');
|
393
|
+
const pointerMove$ = fromEvent(document, 'pointermove');
|
394
|
+
const pointerUp$ = fromEvent(document, 'pointerup');
|
395
|
+
const pointerLeave$ = fromEvent(document, 'pointerleave');
|
396
|
+
const start$ = pointerDown$;
|
397
|
+
const move$ = pointerMove$;
|
398
|
+
const end$ = merge(pointerUp$, pointerLeave$);
|
399
|
+
return { start$, move$, end$ };
|
400
|
+
}
|
401
|
+
|
402
|
+
function snapAngle(angle, steps) {
|
403
|
+
// Calc the size of each step in the scale. 12 for hours, 60 for minutes
|
404
|
+
const stepSize = 360 / steps;
|
405
|
+
// Find the closest multiple of the step size. 12 for hours, 60 for minutes
|
406
|
+
const closestStep = Math.round(angle / stepSize);
|
407
|
+
// Calc the snapped angle
|
408
|
+
const snappedAngle = closestStep * stepSize;
|
409
|
+
return snappedAngle;
|
410
|
+
}
|
411
|
+
function hoursToAngle(hour) {
|
412
|
+
const angle = (hour - 3) * 30;
|
413
|
+
return (angle + 360) % 360;
|
414
|
+
}
|
415
|
+
function minutesToAngle(minute) {
|
416
|
+
let angle = (minute - 15) * 6;
|
417
|
+
angle = (angle + 360) % 360;
|
418
|
+
return angle;
|
419
|
+
}
|
420
|
+
function hours24ToAngle(hour) {
|
421
|
+
let angle;
|
422
|
+
if (hour >= 0 && hour <= 11) {
|
423
|
+
angle = (hour - 3) * 30; // Dial 1: 0 to 11
|
424
|
+
}
|
425
|
+
else {
|
426
|
+
angle = (hour - 15) * 30; // Dial 2: 12 to 23
|
427
|
+
}
|
428
|
+
angle = (angle + 360) % 360;
|
429
|
+
return angle;
|
430
|
+
}
|
431
|
+
function angleToHours(angle) {
|
432
|
+
// Normalize the angle to be within 0-360 range
|
433
|
+
angle = (angle + 360) % 360;
|
434
|
+
// Correct mapping: 0 degrees -> 3, 270 degrees -> 12
|
435
|
+
let hour = Math.floor((angle + 90) / 30) % 12;
|
436
|
+
// Handle the 0 hour case (midnight)
|
437
|
+
if (hour === 0) {
|
438
|
+
hour = 12;
|
439
|
+
}
|
440
|
+
return hour;
|
441
|
+
}
|
442
|
+
function angleToMinutes(angle) {
|
443
|
+
// Normalize the angle
|
444
|
+
angle = (angle + 360) % 360;
|
445
|
+
let minute = (angle / 6) + 15;
|
446
|
+
minute = (minute + 60) % 60; // Ensure positive value and wrap around 60
|
447
|
+
return Math.floor(minute);
|
448
|
+
}
|
449
|
+
function angleToHours24(angle, dial) {
|
450
|
+
angle = (angle + 360) % 360;
|
451
|
+
let hour = 0;
|
452
|
+
const baseValue = angle / 30;
|
453
|
+
if (dial === 1) {
|
454
|
+
hour = (Math.floor(baseValue + 3) % 12 + 12) % 12;
|
455
|
+
}
|
456
|
+
else if (dial === 2) {
|
457
|
+
hour = (Math.floor(baseValue + 3) % 12) + 12;
|
458
|
+
}
|
459
|
+
return hour;
|
460
|
+
}
|
461
|
+
|
462
|
+
const HOURS_LABEL = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
463
|
+
const HOURS_LABEL_24_P1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
464
|
+
const HOURS_LABEL_24_P2 = [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23];
|
465
|
+
const MINUTES_LABEL = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55];
|
466
|
+
|
467
|
+
class RkTimepickerDial {
|
468
|
+
currentTime = model.required({ alias: 'time' });
|
469
|
+
timeFormat = model('12h', { alias: 'format' });
|
470
|
+
selectedTime = model('hours', { alias: 'selected' });
|
471
|
+
period = model('AM');
|
472
|
+
value = computed(() => {
|
473
|
+
const date = this.currentTime();
|
474
|
+
return date ? splitDate(date) : { hours: 0, minutes: 0 };
|
475
|
+
});
|
476
|
+
format = computed(() => {
|
477
|
+
const fm = this.timeFormat();
|
478
|
+
return (fm && (fm === '24h' || fm === '12h')) ? fm : detectTimeFormat(new Date());
|
479
|
+
});
|
480
|
+
selected = computed(() => {
|
481
|
+
const selected = this.selectedTime();
|
482
|
+
return selected === 'hours' || selected === 'minutes' ? selected : 'hours';
|
483
|
+
});
|
484
|
+
_dial = viewChild.required('dial');
|
485
|
+
_clipedLabel = viewChild.required('dial2');
|
486
|
+
_dialSelector = viewChild.required('dialSelector');
|
487
|
+
_elRef = inject(ElementRef);
|
488
|
+
_renderer = inject(Renderer2);
|
489
|
+
_destroyRef = inject(DestroyRef);
|
490
|
+
_injector = inject(Injector);
|
491
|
+
_document = inject(DOCUMENT);
|
492
|
+
_currentDial = signal(1);
|
493
|
+
_currentDegree = signal(0);
|
494
|
+
_loading = signal(false);
|
495
|
+
_motion = 400;
|
496
|
+
_radius = 128;
|
497
|
+
_labelSize = 48;
|
498
|
+
constructor() {
|
499
|
+
afterNextRender(() => {
|
500
|
+
this._fillBySelected(this.selected(), this.format());
|
501
|
+
effect(() => {
|
502
|
+
const selected = this.selected();
|
503
|
+
const format = this.format();
|
504
|
+
this._fillBySelected(selected, format);
|
505
|
+
untracked(() => {
|
506
|
+
const value = this.value();
|
507
|
+
this.period.set(this.value().hours < 12 ? 'AM' : 'PM');
|
508
|
+
this._currentDial.set(format === '24h' && selected === 'hours' && value.hours >= 12 ? 2 : 1);
|
509
|
+
this._handleInputsChange(value, format, selected);
|
510
|
+
});
|
511
|
+
}, { injector: this._injector });
|
512
|
+
effect(() => {
|
513
|
+
const value = this.value();
|
514
|
+
untracked(() => {
|
515
|
+
if (this._loading())
|
516
|
+
return;
|
517
|
+
const format = this.format();
|
518
|
+
const selected = this.selected();
|
519
|
+
this.period.set(this.value().hours < 12 ? 'AM' : 'PM');
|
520
|
+
this._currentDial.set(format === '24h' && selected === 'hours' && value.hours >= 12 ? 2 : 1);
|
521
|
+
this._handleInputsChange(value, format, selected, true);
|
522
|
+
});
|
523
|
+
}, { injector: this._injector });
|
524
|
+
effect(() => {
|
525
|
+
const period = this.period();
|
526
|
+
untracked(() => {
|
527
|
+
const currentTime = this.currentTime();
|
528
|
+
if (period === null || currentTime === null)
|
529
|
+
return;
|
530
|
+
this.currentTime.set(amPmTransform(period, currentTime));
|
531
|
+
});
|
532
|
+
}, { injector: this._injector });
|
533
|
+
const { start$, move$, end$ } = createPointerEvents(this._elRef.nativeElement);
|
534
|
+
start$.pipe(tap((event) => this._onPointerEventInit(normalizeEvent(event))), switchMap(() => {
|
535
|
+
return move$.pipe(takeUntil(end$.pipe(tap((event) => this._onPointerEventStop(normalizeEvent(event))))));
|
536
|
+
}), takeUntilDestroyed(this._destroyRef)).subscribe(event => {
|
537
|
+
const normalized = normalizeEvent(event);
|
538
|
+
this._onPointerEventInit(normalized);
|
539
|
+
});
|
540
|
+
});
|
541
|
+
}
|
542
|
+
_handleInputsChange(value, format, selected, animate = true) {
|
543
|
+
if (selected === 'hours') {
|
544
|
+
const angle = format === '24h' ? hours24ToAngle(value.hours) : hoursToAngle(value.hours);
|
545
|
+
if (animate) {
|
546
|
+
this._rotateAnimation(this._currentDegree(), angle, this._motion, 2);
|
547
|
+
}
|
548
|
+
else {
|
549
|
+
this._moveByAngle(angle, 2);
|
550
|
+
}
|
551
|
+
this._currentDegree.set(angle);
|
552
|
+
}
|
553
|
+
else {
|
554
|
+
const angle = minutesToAngle(value.minutes);
|
555
|
+
if (animate) {
|
556
|
+
this._rotateAnimation(this._currentDegree(), angle, this._motion, 2);
|
557
|
+
}
|
558
|
+
else {
|
559
|
+
this._moveByAngle(angle, 2);
|
560
|
+
}
|
561
|
+
this._currentDegree.set(angle);
|
562
|
+
}
|
563
|
+
}
|
564
|
+
_onPointerEventInit(event) {
|
565
|
+
this._loading.set(true);
|
566
|
+
this._moveByTouchClick(event, this._dial().nativeElement);
|
567
|
+
}
|
568
|
+
async _onPointerEventStop(event) {
|
569
|
+
const currentDegree = this._currentDegree();
|
570
|
+
const snapDegrees = this.selected() === 'hours' ? snapAngle(currentDegree, 12) : snapAngle(currentDegree, 60);
|
571
|
+
await this._rotateAnimation(this._currentDegree(), snapDegrees, this._motion / 2, 2);
|
572
|
+
this._currentDegree.set(snapDegrees);
|
573
|
+
this._loading.set(false);
|
574
|
+
}
|
575
|
+
_fillDial(dial, labels, withClean, inset = 2) {
|
576
|
+
if (withClean) {
|
577
|
+
dial.innerHTML = '';
|
578
|
+
}
|
579
|
+
const clockIterable = new ClockIterable(labels, this._labelSize, this._radius * 2, inset);
|
580
|
+
for (const [label, x, y] of clockIterable) {
|
581
|
+
const numero = this._document.createElement('div');
|
582
|
+
numero.classList.add('rk-clock-dial-label');
|
583
|
+
numero.textContent = this.selected() === 'hours' ? label.toString() : label.toString().padStart(2, '0');
|
584
|
+
numero.style.position = 'absolute';
|
585
|
+
numero.style.left = x + 'px';
|
586
|
+
numero.style.top = y + 'px';
|
587
|
+
this._renderer.appendChild(dial, numero);
|
588
|
+
}
|
589
|
+
}
|
590
|
+
_fillBySelected(selected, format) {
|
591
|
+
switch (selected) {
|
592
|
+
case 'hours':
|
593
|
+
if (format === '12h') {
|
594
|
+
this._fillDial(this._dial().nativeElement, HOURS_LABEL, true);
|
595
|
+
this._fillDial(this._clipedLabel().nativeElement, HOURS_LABEL, true);
|
596
|
+
}
|
597
|
+
else if (format === '24h') {
|
598
|
+
this._fillDial(this._dial().nativeElement, HOURS_LABEL_24_P1, true);
|
599
|
+
this._fillDial(this._dial().nativeElement, HOURS_LABEL_24_P2, false, 38);
|
600
|
+
this._fillDial(this._clipedLabel().nativeElement, HOURS_LABEL_24_P1, true);
|
601
|
+
this._fillDial(this._clipedLabel().nativeElement, HOURS_LABEL_24_P2, false, 38);
|
602
|
+
}
|
603
|
+
break;
|
604
|
+
case 'minutes':
|
605
|
+
this._fillDial(this._dial().nativeElement, MINUTES_LABEL, true);
|
606
|
+
this._fillDial(this._clipedLabel().nativeElement, MINUTES_LABEL, true);
|
607
|
+
break;
|
608
|
+
default:
|
609
|
+
this._fillDial(this._dial().nativeElement, HOURS_LABEL, true);
|
610
|
+
this._fillDial(this._clipedLabel().nativeElement, HOURS_LABEL, true);
|
611
|
+
}
|
612
|
+
}
|
613
|
+
_moveByTouchClick(event, dial, inset = 2) {
|
614
|
+
const rect = dial.getBoundingClientRect();
|
615
|
+
// relative to the center of the clock
|
616
|
+
const x = event.clientX - rect.left - this._radius;
|
617
|
+
const y = event.clientY - rect.top - this._radius;
|
618
|
+
// Calc angle in degrees
|
619
|
+
let angle = Math.atan2(y, x) * (180 / Math.PI);
|
620
|
+
if (angle < 0) {
|
621
|
+
angle += 360; //make sure the angle is in the range [0, 360]
|
622
|
+
}
|
623
|
+
// Calc new position for the object (using radius as reference)
|
624
|
+
let radius = this._radius - (this._labelSize / 2) - inset;
|
625
|
+
//--------------------------------------------------------------------------------------------------------------//
|
626
|
+
// logic when format is 24h and two dial exist
|
627
|
+
const distanceFromCenter = Math.sqrt(x * x + y * y);
|
628
|
+
const MAX_RAD = radius - 20;
|
629
|
+
let selectorInset = 0;
|
630
|
+
if (distanceFromCenter < MAX_RAD && this.selected() === 'hours' && this.format() === '24h') {
|
631
|
+
radius -= 36;
|
632
|
+
this._currentDial.set(2);
|
633
|
+
selectorInset = 36;
|
634
|
+
}
|
635
|
+
else {
|
636
|
+
this._currentDial.set(1);
|
637
|
+
selectorInset = 0;
|
638
|
+
}
|
639
|
+
//----------------------------------------------------------------------------------------------------------------//
|
640
|
+
const moveX = this._radius + radius * Math.cos(angle * (Math.PI / 180));
|
641
|
+
const moveY = this._radius + radius * Math.sin(angle * (Math.PI / 180));
|
642
|
+
// Call the provided functions with the calculated values
|
643
|
+
this._moveTrack(moveX, moveY);
|
644
|
+
this._moveSelector(angle, selectorInset);
|
645
|
+
this._currentDegree.set(angle);
|
646
|
+
//update time
|
647
|
+
const snapDegrees = this.selected() === 'hours' ? snapAngle(angle, 12) : snapAngle(angle, 60);
|
648
|
+
const currentDate = this.currentTime();
|
649
|
+
const date = currentDate ? new Date(currentDate) : new Date();
|
650
|
+
if (this.selected() === 'hours') {
|
651
|
+
let hours = this.format() === '24h' ? angleToHours24(snapDegrees, this._currentDial()) : angleToHours(snapDegrees);
|
652
|
+
if (this.format() === '24h') {
|
653
|
+
this.period.set(hours < 12 ? 'AM' : 'PM');
|
654
|
+
}
|
655
|
+
if (this.format() !== '24h' && this.period() === 'PM') {
|
656
|
+
hours = hours < 12 ? hours + 12 : hours;
|
657
|
+
}
|
658
|
+
if (this.period() === 'AM' && hours >= 12 && this.format() === '12h') {
|
659
|
+
hours = hours - 12;
|
660
|
+
}
|
661
|
+
date.setHours(hours);
|
662
|
+
}
|
663
|
+
else {
|
664
|
+
date.setMinutes(angleToMinutes(snapDegrees));
|
665
|
+
}
|
666
|
+
this.currentTime.set(date);
|
667
|
+
}
|
668
|
+
_moveByAngle(angle, inset = 2) {
|
669
|
+
// Ensure angle is within 0-360 range
|
670
|
+
angle = angle % 360;
|
671
|
+
if (angle < 0) {
|
672
|
+
angle += 360;
|
673
|
+
}
|
674
|
+
// Calc new position for the object (using radius as reference)
|
675
|
+
let radius = this._radius - (this._labelSize / 2) - inset;
|
676
|
+
// logic when format is 24h and two dial exist
|
677
|
+
let selectorInset = 0;
|
678
|
+
if (this.selected() === 'hours' && this.format() === '24h' && this._currentDial() === 2) {
|
679
|
+
radius -= 36;
|
680
|
+
selectorInset = 36;
|
681
|
+
}
|
682
|
+
const moveX = this._radius + radius * Math.cos(angle * (Math.PI / 180));
|
683
|
+
const moveY = this._radius + radius * Math.sin(angle * (Math.PI / 180));
|
684
|
+
// Call the provided functions with the calculated values
|
685
|
+
this._moveTrack(moveX, moveY);
|
686
|
+
this._moveSelector(angle, selectorInset);
|
687
|
+
}
|
688
|
+
_moveTrack(x, y) {
|
689
|
+
this._renderer.setStyle(this._clipedLabel().nativeElement, 'clip-path', `circle(24px at ${x}px ${y}px)`);
|
690
|
+
}
|
691
|
+
_moveSelector(degrees, inset = 0) {
|
692
|
+
this._renderer.setStyle(this._dialSelector().nativeElement, 'width', 104 - inset + 'px');
|
693
|
+
this._renderer.setStyle(this._dialSelector().nativeElement, 'transform', `rotate(${degrees}deg)`);
|
694
|
+
}
|
695
|
+
async _rotateAnimation(currentDegree, degree, animationTime, inset = 2) {
|
696
|
+
return new Promise((resolve) => {
|
697
|
+
// Calculate the shortest angle difference between the current and target degrees.
|
698
|
+
let angleDiff = degree - currentDegree;
|
699
|
+
// Adjust the angle difference to be within the range of -180 to 180 degrees.
|
700
|
+
if (angleDiff > 180) {
|
701
|
+
angleDiff -= 360;
|
702
|
+
}
|
703
|
+
else if (angleDiff < -180) {
|
704
|
+
angleDiff += 360;
|
705
|
+
}
|
706
|
+
// Store the start time of the animation.
|
707
|
+
let startTime = null;
|
708
|
+
const animate = (timestamp) => {
|
709
|
+
// Initialize the start time if it hasn't been set yet.
|
710
|
+
if (!startTime)
|
711
|
+
startTime = timestamp;
|
712
|
+
// Calculate the progress of the animation (0 to 1).
|
713
|
+
let progress = Math.min((timestamp - startTime) / animationTime, 1);
|
714
|
+
// Apply a cubic Bezier easing function to the progress. This makes the animation smoother.
|
715
|
+
const easedProgress = cubicBezier(0.05, 0.7, 0.1, 1.0, progress);
|
716
|
+
// Calculate the current angle based on the eased progress.
|
717
|
+
const currentAngle = currentDegree + angleDiff * easedProgress;
|
718
|
+
// Update the position of the dial and selector based on the current angle.
|
719
|
+
this._moveByAngle(currentAngle, inset);
|
720
|
+
// Continue the animation if it's not finished.
|
721
|
+
if (progress < 1) {
|
722
|
+
requestAnimationFrame(animate);
|
723
|
+
}
|
724
|
+
else {
|
725
|
+
resolve(); // Resolve the promise when the animation is complete.
|
726
|
+
}
|
727
|
+
};
|
728
|
+
requestAnimationFrame(animate);
|
729
|
+
});
|
730
|
+
}
|
731
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: RkTimepickerDial, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
732
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "19.1.6", type: RkTimepickerDial, isStandalone: true, selector: "rk-timepicker-dial", inputs: { currentTime: { classPropertyName: "currentTime", publicName: "time", isSignal: true, isRequired: true, transformFunction: null }, timeFormat: { classPropertyName: "timeFormat", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, selectedTime: { classPropertyName: "selectedTime", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, period: { classPropertyName: "period", publicName: "period", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { currentTime: "timeChange", timeFormat: "formatChange", selectedTime: "selectedChange", period: "periodChange" }, host: { classAttribute: "rk-timepicker-dial" }, viewQueries: [{ propertyName: "_dial", first: true, predicate: ["dial"], descendants: true, isSignal: true }, { propertyName: "_clipedLabel", first: true, predicate: ["dial2"], descendants: true, isSignal: true }, { propertyName: "_dialSelector", first: true, predicate: ["dialSelector"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
733
|
+
<div class="rk-timepicker-dial-container">
|
734
|
+
<div #dial class="rk-label-container rk-dial-size">
|
735
|
+
</div>
|
736
|
+
<div #dial2 class="rk-cliped-label rk-dial-size">
|
737
|
+
</div>
|
738
|
+
<div #dialSelector class="rk-dial-selector"></div>
|
739
|
+
<div class="rk-pivot-point"></div>
|
740
|
+
</div>
|
741
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
742
|
+
}
|
743
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: RkTimepickerDial, decorators: [{
|
744
|
+
type: Component,
|
745
|
+
args: [{ selector: 'rk-timepicker-dial', host: {
|
746
|
+
'class': 'rk-timepicker-dial',
|
747
|
+
}, imports: [], template: `
|
748
|
+
<div class="rk-timepicker-dial-container">
|
749
|
+
<div #dial class="rk-label-container rk-dial-size">
|
750
|
+
</div>
|
751
|
+
<div #dial2 class="rk-cliped-label rk-dial-size">
|
752
|
+
</div>
|
753
|
+
<div #dialSelector class="rk-dial-selector"></div>
|
754
|
+
<div class="rk-pivot-point"></div>
|
755
|
+
</div>
|
756
|
+
`, standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None }]
|
757
|
+
}], ctorParameters: () => [] });
|
758
|
+
|
759
|
+
class RkTimepickerInputLabel {
|
760
|
+
currentTime = model.required({ alias: 'time' });
|
761
|
+
selected = model('hours');
|
762
|
+
period = model('AM');
|
763
|
+
format = model('12h');
|
764
|
+
editable = model(false);
|
765
|
+
hours = signal('');
|
766
|
+
minutes = signal('');
|
767
|
+
inputSupportLabels = input(['Hour', 'Minute'], { alias: 'inputLabels' });
|
768
|
+
inputHours = viewChild('inputH');
|
769
|
+
inputMinutes = viewChild('inputM');
|
770
|
+
value = computed(() => {
|
771
|
+
const date = this.currentTime();
|
772
|
+
const format = this.format();
|
773
|
+
if (date) {
|
774
|
+
const { hours, minutes } = splitDate(date);
|
775
|
+
if (format === '12h') {
|
776
|
+
return { hours: hours > 12 ? hours - 12 : hours === 0 ? 12 : hours, minutes };
|
777
|
+
}
|
778
|
+
return { hours, minutes };
|
779
|
+
}
|
780
|
+
return { hours: 0, minutes: 0 };
|
781
|
+
});
|
782
|
+
constructor() {
|
783
|
+
effect(() => {
|
784
|
+
const editable = this.editable();
|
785
|
+
const selected = this.selected();
|
786
|
+
if (editable) {
|
787
|
+
if (selected === 'hours') {
|
788
|
+
this.inputHours()?.nativeElement.focus();
|
789
|
+
}
|
790
|
+
else if (selected === 'minutes') {
|
791
|
+
this.inputMinutes()?.nativeElement.focus();
|
792
|
+
}
|
793
|
+
}
|
794
|
+
});
|
795
|
+
effect(() => {
|
796
|
+
const value = this.value();
|
797
|
+
untracked(() => {
|
798
|
+
this.onHoursInput(value.hours.toString().padStart(2, '0'));
|
799
|
+
this.onMinutesInput(value.minutes.toString().padStart(2, '0'));
|
800
|
+
});
|
801
|
+
});
|
802
|
+
}
|
803
|
+
onHoursInput(event) {
|
804
|
+
const input = this.inputHours()?.nativeElement;
|
805
|
+
const value = typeof event === 'string' ? event : event.target?.value;
|
806
|
+
const max = this.format() === '24h' ? 23 : 12;
|
807
|
+
const validatedValue = validateTimeValue(value, max);
|
808
|
+
if (input && input.value !== validatedValue) {
|
809
|
+
input.value = validatedValue;
|
810
|
+
}
|
811
|
+
this.hours.set(validatedValue);
|
812
|
+
}
|
813
|
+
onMinutesInput(event) {
|
814
|
+
const input = this.inputMinutes()?.nativeElement;
|
815
|
+
const value = typeof event === 'string' ? event : event.target?.value;
|
816
|
+
const validatedValue = validateTimeValue(value, 59);
|
817
|
+
if (input && input.value !== validatedValue) {
|
818
|
+
input.value = validatedValue;
|
819
|
+
}
|
820
|
+
this.minutes.set(validatedValue);
|
821
|
+
}
|
822
|
+
onHoursBlur() {
|
823
|
+
const defaultHour = this.format() === '24h' ? '00' : '01';
|
824
|
+
const formattedValue = formatTimeValue(this.hours(), defaultHour);
|
825
|
+
this.hours.set(formattedValue);
|
826
|
+
this.updateModel();
|
827
|
+
}
|
828
|
+
onMinutesBlur() {
|
829
|
+
const formattedValue = formatTimeValue(this.minutes(), '00');
|
830
|
+
this.minutes.set(formattedValue);
|
831
|
+
this.updateModel();
|
832
|
+
}
|
833
|
+
updateModel() {
|
834
|
+
const tempDate = this.currentTime();
|
835
|
+
if (tempDate) {
|
836
|
+
const newDate = new Date(tempDate);
|
837
|
+
let hoursN = Number(this.hours());
|
838
|
+
const minutesN = Number(this.minutes());
|
839
|
+
if (this.format() === '12h' && this.period() === 'PM') {
|
840
|
+
hoursN = hoursN < 12 ? hoursN + 12 : hoursN;
|
841
|
+
}
|
842
|
+
newDate.setHours(hoursN);
|
843
|
+
newDate.setMinutes(minutesN);
|
844
|
+
this.currentTime.set(newDate);
|
845
|
+
}
|
846
|
+
}
|
847
|
+
selectedChange(selected) {
|
848
|
+
if (this.selected() === selected) {
|
849
|
+
return;
|
850
|
+
}
|
851
|
+
this.selected.set(selected);
|
852
|
+
}
|
853
|
+
periodSelector(period) {
|
854
|
+
if (this.period() === period) {
|
855
|
+
return;
|
856
|
+
}
|
857
|
+
this.period.set(period);
|
858
|
+
}
|
859
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: RkTimepickerInputLabel, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
860
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.1.6", type: RkTimepickerInputLabel, isStandalone: true, selector: "rk-timepicker-input-label", inputs: { currentTime: { classPropertyName: "currentTime", publicName: "time", isSignal: true, isRequired: true, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, period: { classPropertyName: "period", publicName: "period", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, inputSupportLabels: { classPropertyName: "inputSupportLabels", publicName: "inputLabels", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { currentTime: "timeChange", selected: "selectedChange", period: "periodChange", format: "formatChange", editable: "editableChange" }, host: { classAttribute: "rk-time-input-field" }, viewQueries: [{ propertyName: "inputHours", first: true, predicate: ["inputH"], descendants: true, isSignal: true }, { propertyName: "inputMinutes", first: true, predicate: ["inputM"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
861
|
+
<div class="rk-time-input-label-container">
|
862
|
+
<div class="rk-time-selector-container" matRipple [ngClass]="{'rk-time-selector-container-selected': selected() === 'hours'}" (click)="selectedChange('hours')">
|
863
|
+
@if (!editable()) {
|
864
|
+
{{value().hours.toString().padStart(2, '0')}}
|
865
|
+
} @else {
|
866
|
+
<input type="text" maxlength="2" #inputH [value]="hours()" (focus)="selectedChange('hours')" (input)="onHoursInput($event)" (blur)="onHoursBlur()" name="hours">
|
867
|
+
}
|
868
|
+
</div>
|
869
|
+
@if (editable()) {
|
870
|
+
<span class="rk-time-input-support-label">{{inputSupportLabels()[0]}}</span>
|
871
|
+
}
|
872
|
+
</div>
|
873
|
+
<div class="rk-time-divider">:</div>
|
874
|
+
<div class="rk-time-input-label-container">
|
875
|
+
<div class="rk-time-selector-container" matRipple [ngClass]="{'rk-time-selector-container-selected': selected() === 'minutes'}" (click)="selectedChange('minutes')">
|
876
|
+
@if (!editable()) {
|
877
|
+
{{value().minutes.toString().padStart(2, '0')}}
|
878
|
+
} @else {
|
879
|
+
<input type="text" maxlength="2" #inputM [value]="minutes()" (focus)="selectedChange('minutes')" (input)="onMinutesInput($event)" (blur)="onMinutesBlur()" name="minutes">
|
880
|
+
}
|
881
|
+
</div>
|
882
|
+
@if (editable()) {
|
883
|
+
<span class="rk-time-input-support-label">{{inputSupportLabels()[1]}}</span>
|
884
|
+
}
|
885
|
+
</div>
|
886
|
+
@if (format() === '12h') {
|
887
|
+
<div class="rk-period-selector-container">
|
888
|
+
<div class="rk-period-selector" matRipple [ngClass]="{'rk-period-selector-selected': period() === 'AM'}" (click)="periodSelector('AM')">AM</div>
|
889
|
+
<div class="rk-period-selector" matRipple [ngClass]="{'rk-period-selector-selected': period() === 'PM'}" (click)="periodSelector('PM')">PM</div>
|
890
|
+
</div>
|
891
|
+
}
|
892
|
+
`, isInline: true, styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: MatRippleModule }, { kind: "directive", type: i1.MatRipple, selector: "[mat-ripple], [matRipple]", inputs: ["matRippleColor", "matRippleUnbounded", "matRippleCentered", "matRippleRadius", "matRippleAnimation", "matRippleDisabled", "matRippleTrigger"], exportAs: ["matRipple"] }, { kind: "ngmodule", type: FormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
893
|
+
}
|
894
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: RkTimepickerInputLabel, decorators: [{
|
895
|
+
type: Component,
|
896
|
+
args: [{ selector: 'rk-timepicker-input-label', imports: [NgClass, MatRippleModule, FormsModule], host: {
|
897
|
+
'class': 'rk-time-input-field',
|
898
|
+
}, template: `
|
899
|
+
<div class="rk-time-input-label-container">
|
900
|
+
<div class="rk-time-selector-container" matRipple [ngClass]="{'rk-time-selector-container-selected': selected() === 'hours'}" (click)="selectedChange('hours')">
|
901
|
+
@if (!editable()) {
|
902
|
+
{{value().hours.toString().padStart(2, '0')}}
|
903
|
+
} @else {
|
904
|
+
<input type="text" maxlength="2" #inputH [value]="hours()" (focus)="selectedChange('hours')" (input)="onHoursInput($event)" (blur)="onHoursBlur()" name="hours">
|
905
|
+
}
|
906
|
+
</div>
|
907
|
+
@if (editable()) {
|
908
|
+
<span class="rk-time-input-support-label">{{inputSupportLabels()[0]}}</span>
|
909
|
+
}
|
910
|
+
</div>
|
911
|
+
<div class="rk-time-divider">:</div>
|
912
|
+
<div class="rk-time-input-label-container">
|
913
|
+
<div class="rk-time-selector-container" matRipple [ngClass]="{'rk-time-selector-container-selected': selected() === 'minutes'}" (click)="selectedChange('minutes')">
|
914
|
+
@if (!editable()) {
|
915
|
+
{{value().minutes.toString().padStart(2, '0')}}
|
916
|
+
} @else {
|
917
|
+
<input type="text" maxlength="2" #inputM [value]="minutes()" (focus)="selectedChange('minutes')" (input)="onMinutesInput($event)" (blur)="onMinutesBlur()" name="minutes">
|
918
|
+
}
|
919
|
+
</div>
|
920
|
+
@if (editable()) {
|
921
|
+
<span class="rk-time-input-support-label">{{inputSupportLabels()[1]}}</span>
|
922
|
+
}
|
923
|
+
</div>
|
924
|
+
@if (format() === '12h') {
|
925
|
+
<div class="rk-period-selector-container">
|
926
|
+
<div class="rk-period-selector" matRipple [ngClass]="{'rk-period-selector-selected': period() === 'AM'}" (click)="periodSelector('AM')">AM</div>
|
927
|
+
<div class="rk-period-selector" matRipple [ngClass]="{'rk-period-selector-selected': period() === 'PM'}" (click)="periodSelector('PM')">PM</div>
|
928
|
+
</div>
|
929
|
+
}
|
930
|
+
`, standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None }]
|
931
|
+
}], ctorParameters: () => [] });
|
932
|
+
|
933
|
+
class RkTimepicker {
|
934
|
+
selected = model('hours');
|
935
|
+
format = model('12h');
|
936
|
+
period = model('AM');
|
937
|
+
editable = model(false);
|
938
|
+
orientation = model('portrait');
|
939
|
+
_inputSupportLabels = input(['Hour', 'Minute'], { alias: 'inputLabels' });
|
940
|
+
_headline = input(['Enter time', 'Select time'], { alias: 'headline' });
|
941
|
+
_actions = input(['Cancel', 'OK'], { alias: 'actions' });
|
942
|
+
/** Whether the timepicker is currently disabled. */
|
943
|
+
disabled = computed(() => !!this._input()?.disabled());
|
944
|
+
defaultTime = input(null, { alias: 'time' });
|
945
|
+
load = signal(false);
|
946
|
+
isLandscape = computed(() => {
|
947
|
+
return this.orientation() === 'landscape';
|
948
|
+
});
|
949
|
+
headline = computed(() => {
|
950
|
+
const editable = this.editable();
|
951
|
+
const labels = this._headline();
|
952
|
+
return editable ? labels[0] : labels[1];
|
953
|
+
});
|
954
|
+
_panelTemplate = viewChild.required('panelTemplate');
|
955
|
+
_dialogRef = signal(null);
|
956
|
+
_input = signal(null);
|
957
|
+
_isOpen = signal(false);
|
958
|
+
currentTime = signal(null);
|
959
|
+
animated = signal(false);
|
960
|
+
/** Emits when the timepicker is opened. */
|
961
|
+
opened = output();
|
962
|
+
/** Emits when the timepicker is closed. */
|
963
|
+
closed = output();
|
964
|
+
selectedTime = output();
|
965
|
+
_dialog = inject(Dialog);
|
966
|
+
constructor() {
|
967
|
+
effect(() => {
|
968
|
+
const value = this.defaultTime();
|
969
|
+
if (!value) {
|
970
|
+
this.currentTime.set(new Date());
|
971
|
+
}
|
972
|
+
else {
|
973
|
+
this.currentTime.set(value);
|
974
|
+
}
|
975
|
+
});
|
976
|
+
effect(() => {
|
977
|
+
const editable = this.editable();
|
978
|
+
untracked(() => {
|
979
|
+
this.animated.set(true);
|
980
|
+
});
|
981
|
+
});
|
982
|
+
afterRender(() => {
|
983
|
+
this.load.set(true);
|
984
|
+
});
|
985
|
+
}
|
986
|
+
/** Opens the timepicker. */
|
987
|
+
open() {
|
988
|
+
const input = this._input();
|
989
|
+
if (!input) {
|
990
|
+
return;
|
991
|
+
}
|
992
|
+
if (this._isOpen()) {
|
993
|
+
return;
|
994
|
+
}
|
995
|
+
this.currentTime.set(input.value());
|
996
|
+
this.animated.set(false);
|
997
|
+
this._isOpen.set(true);
|
998
|
+
this.opened.emit();
|
999
|
+
const dialogRef = this._dialog.open(this._panelTemplate(), {
|
1000
|
+
width: '328px',
|
1001
|
+
});
|
1002
|
+
this._dialogRef.set(dialogRef);
|
1003
|
+
dialogRef.closed.subscribe(result => {
|
1004
|
+
if (result) {
|
1005
|
+
this._input()?.value.set(result);
|
1006
|
+
this.selectedTime.emit(result);
|
1007
|
+
input.simulateEnter();
|
1008
|
+
}
|
1009
|
+
this._isOpen.set(false);
|
1010
|
+
this.closed.emit();
|
1011
|
+
input.focus();
|
1012
|
+
});
|
1013
|
+
}
|
1014
|
+
/** Registers an input with the timepicker. */
|
1015
|
+
registerInput(input) {
|
1016
|
+
const currentInput = this._input();
|
1017
|
+
if (currentInput && input !== currentInput) {
|
1018
|
+
console.warn('RkTimepicker can only be registered with one input at a time');
|
1019
|
+
}
|
1020
|
+
this._input.set(input);
|
1021
|
+
}
|
1022
|
+
_handleKeydown(event) {
|
1023
|
+
const keyCode = event.key;
|
1024
|
+
if (keyCode === 'Enter') {
|
1025
|
+
event.preventDefault();
|
1026
|
+
this.onConfirm();
|
1027
|
+
}
|
1028
|
+
else if (keyCode === 'Escape') {
|
1029
|
+
event.preventDefault();
|
1030
|
+
this.onCancel();
|
1031
|
+
}
|
1032
|
+
}
|
1033
|
+
changeMode() {
|
1034
|
+
this.editable.set(!this.editable());
|
1035
|
+
}
|
1036
|
+
onConfirm() {
|
1037
|
+
this._dialogRef()?.close(this.currentTime());
|
1038
|
+
}
|
1039
|
+
onCancel() {
|
1040
|
+
this._dialogRef()?.close();
|
1041
|
+
}
|
1042
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: RkTimepicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
1043
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.1.6", type: RkTimepicker, isStandalone: true, selector: "rk-timepicker", inputs: { selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, period: { classPropertyName: "period", publicName: "period", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, _inputSupportLabels: { classPropertyName: "_inputSupportLabels", publicName: "inputLabels", isSignal: true, isRequired: false, transformFunction: null }, _headline: { classPropertyName: "_headline", publicName: "headline", isSignal: true, isRequired: false, transformFunction: null }, _actions: { classPropertyName: "_actions", publicName: "actions", isSignal: true, isRequired: false, transformFunction: null }, defaultTime: { classPropertyName: "defaultTime", publicName: "time", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selected: "selectedChange", format: "formatChange", period: "periodChange", editable: "editableChange", orientation: "orientationChange", opened: "opened", closed: "closed", selectedTime: "selectedTime" }, host: { listeners: { "keydown": "_handleKeydown($event)" }, properties: { "class.rk-timepicker-landscape": "isLandscape()", "class.rk-timepicker-editable": "editable()" } }, viewQueries: [{ propertyName: "_panelTemplate", first: true, predicate: ["panelTemplate"], descendants: true, isSignal: true }], exportAs: ["rkTimepicker"], ngImport: i0, template: `
|
1044
|
+
<ng-template #panelTemplate>
|
1045
|
+
<div class="rk-timepicker rk-timepicker-panel">
|
1046
|
+
<div class="rk-timepicker-container" [ngClass]="{ 'rk-timepicker-container-hide': !load() }">
|
1047
|
+
<span class="rk-timepicker-headline">{{ headline() }}</span>
|
1048
|
+
<rk-timepicker-input-label [(time)]="currentTime" [inputLabels]="_inputSupportLabels()" [(editable)]="editable" [(format)]="format" [(period)]="period" [(selected)]="selected"></rk-timepicker-input-label>
|
1049
|
+
<rk-timepicker-dial [ngClass]="{ 'rk-dial-closed': editable(), 'rk-dial-animated': animated()}" [(time)]="currentTime" [(format)]="format" [(period)]="period" [(selected)]="selected"></rk-timepicker-dial>
|
1050
|
+
<div class="rk-timepicker-footer">
|
1051
|
+
<button class="rk-timepicker-mode-button" (click)="changeMode()" mat-icon-button>
|
1052
|
+
@if (editable()) {
|
1053
|
+
<mat-icon>schedule</mat-icon>
|
1054
|
+
} @else {
|
1055
|
+
<mat-icon>keyboard</mat-icon>
|
1056
|
+
}
|
1057
|
+
</button>
|
1058
|
+
<div class="rk-timepicker-actions">
|
1059
|
+
<button mat-button (click)="onCancel()">{{_actions()[0]}}</button>
|
1060
|
+
<button mat-button (click)="onConfirm()">{{_actions()[1]}}</button>
|
1061
|
+
</div>
|
1062
|
+
</div>
|
1063
|
+
</div>
|
1064
|
+
</div>
|
1065
|
+
</ng-template>
|
1066
|
+
`, isInline: true, styles: [""], dependencies: [{ kind: "component", type: RkTimepickerInputLabel, selector: "rk-timepicker-input-label", inputs: ["time", "selected", "period", "format", "editable", "inputLabels"], outputs: ["timeChange", "selectedChange", "periodChange", "formatChange", "editableChange"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: DialogModule }, { kind: "component", type: RkTimepickerDial, selector: "rk-timepicker-dial", inputs: ["time", "format", "selected", "period"], outputs: ["timeChange", "formatChange", "selectedChange", "periodChange"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
1067
|
+
}
|
1068
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: RkTimepicker, decorators: [{
|
1069
|
+
type: Component,
|
1070
|
+
args: [{ selector: 'rk-timepicker', exportAs: 'rkTimepicker', imports: [RkTimepickerInputLabel, NgClass, DialogModule, RkTimepickerDial, MatButton, MatIconButton, MatIcon], host: {
|
1071
|
+
'[class.rk-timepicker-landscape]': 'isLandscape()',
|
1072
|
+
'[class.rk-timepicker-editable]': 'editable()',
|
1073
|
+
'(keydown)': '_handleKeydown($event)',
|
1074
|
+
}, template: `
|
1075
|
+
<ng-template #panelTemplate>
|
1076
|
+
<div class="rk-timepicker rk-timepicker-panel">
|
1077
|
+
<div class="rk-timepicker-container" [ngClass]="{ 'rk-timepicker-container-hide': !load() }">
|
1078
|
+
<span class="rk-timepicker-headline">{{ headline() }}</span>
|
1079
|
+
<rk-timepicker-input-label [(time)]="currentTime" [inputLabels]="_inputSupportLabels()" [(editable)]="editable" [(format)]="format" [(period)]="period" [(selected)]="selected"></rk-timepicker-input-label>
|
1080
|
+
<rk-timepicker-dial [ngClass]="{ 'rk-dial-closed': editable(), 'rk-dial-animated': animated()}" [(time)]="currentTime" [(format)]="format" [(period)]="period" [(selected)]="selected"></rk-timepicker-dial>
|
1081
|
+
<div class="rk-timepicker-footer">
|
1082
|
+
<button class="rk-timepicker-mode-button" (click)="changeMode()" mat-icon-button>
|
1083
|
+
@if (editable()) {
|
1084
|
+
<mat-icon>schedule</mat-icon>
|
1085
|
+
} @else {
|
1086
|
+
<mat-icon>keyboard</mat-icon>
|
1087
|
+
}
|
1088
|
+
</button>
|
1089
|
+
<div class="rk-timepicker-actions">
|
1090
|
+
<button mat-button (click)="onCancel()">{{_actions()[0]}}</button>
|
1091
|
+
<button mat-button (click)="onConfirm()">{{_actions()[1]}}</button>
|
1092
|
+
</div>
|
1093
|
+
</div>
|
1094
|
+
</div>
|
1095
|
+
</div>
|
1096
|
+
</ng-template>
|
1097
|
+
`, standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None }]
|
1098
|
+
}], ctorParameters: () => [] });
|
1099
|
+
|
1100
|
+
class RkTimepickerInput {
|
1101
|
+
_elementRef = inject(ElementRef);
|
1102
|
+
_renderer = inject(Renderer2);
|
1103
|
+
/** Necessary for ControlValueAccessor implementation */
|
1104
|
+
_onChange;
|
1105
|
+
_onTouched;
|
1106
|
+
_accessorDisabled = signal(false);
|
1107
|
+
_validatorOnChange;
|
1108
|
+
/** Current value of the input. */
|
1109
|
+
value = model(null);
|
1110
|
+
_tempDateSafe = signal(null);
|
1111
|
+
/** Timepicker that the input is associated with. */
|
1112
|
+
timepicker = input.required({
|
1113
|
+
alias: 'rkTimepicker',
|
1114
|
+
});
|
1115
|
+
/** Whether the input is disabled. */
|
1116
|
+
disabled = computed(() => this.disabledInput() || this._accessorDisabled());
|
1117
|
+
/**
|
1118
|
+
* Whether the input should be disabled through the template.
|
1119
|
+
*/
|
1120
|
+
disabledInput = input(false, {
|
1121
|
+
transform: booleanAttribute,
|
1122
|
+
alias: 'disabled',
|
1123
|
+
});
|
1124
|
+
constructor() {
|
1125
|
+
this._registerTimepicker();
|
1126
|
+
}
|
1127
|
+
_updateModelValue() {
|
1128
|
+
const value = this.value();
|
1129
|
+
if (!value) {
|
1130
|
+
this._validatorOnChange?.();
|
1131
|
+
return;
|
1132
|
+
}
|
1133
|
+
this._onChange?.(value);
|
1134
|
+
}
|
1135
|
+
_validateTimeOrNull(value) {
|
1136
|
+
if (value === null || value instanceof Date) {
|
1137
|
+
this.value.set(value);
|
1138
|
+
return;
|
1139
|
+
}
|
1140
|
+
const validTime = timeToSecondsi18n(value);
|
1141
|
+
const time = validTime ? secondsToDate(validTime) : null;
|
1142
|
+
if (!time) {
|
1143
|
+
this.value.set(null);
|
1144
|
+
}
|
1145
|
+
else {
|
1146
|
+
const newDate = new Date(this._tempDateSafe() ?? new Date());
|
1147
|
+
newDate.setHours(time.getHours(), time.getMinutes());
|
1148
|
+
this.value.set(newDate);
|
1149
|
+
}
|
1150
|
+
}
|
1151
|
+
_updateInputValue() {
|
1152
|
+
if (this.value() !== null) {
|
1153
|
+
this._renderer.setProperty(this._elementRef.nativeElement, 'value', this.value()?.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }));
|
1154
|
+
}
|
1155
|
+
}
|
1156
|
+
/** Handles the `input` event. */
|
1157
|
+
_handleInput(value) {
|
1158
|
+
this._validateTimeOrNull(value);
|
1159
|
+
this._validatorOnChange?.();
|
1160
|
+
}
|
1161
|
+
/** Handles the `keydown` event. */
|
1162
|
+
_handleKeydown(event) {
|
1163
|
+
if (['Enter', 'Escape', 'Tab'].includes(event.code)) {
|
1164
|
+
this._updateModelValue();
|
1165
|
+
this._updateInputValue();
|
1166
|
+
}
|
1167
|
+
}
|
1168
|
+
simulateEnter() {
|
1169
|
+
this._handleKeydown({ code: 'Enter' });
|
1170
|
+
}
|
1171
|
+
/** Handles the `blur` event. */
|
1172
|
+
_handleBlur() {
|
1173
|
+
this._onTouched?.();
|
1174
|
+
this._updateModelValue();
|
1175
|
+
this._updateInputValue();
|
1176
|
+
}
|
1177
|
+
/** Focuses the input. */
|
1178
|
+
focus() {
|
1179
|
+
this._elementRef.nativeElement.focus();
|
1180
|
+
}
|
1181
|
+
/** Implemented as a part of ControlValueAccessor. */
|
1182
|
+
writeValue(value) {
|
1183
|
+
if (value instanceof Date) {
|
1184
|
+
this._tempDateSafe.set(value);
|
1185
|
+
this.value.set(value);
|
1186
|
+
this._updateInputValue();
|
1187
|
+
}
|
1188
|
+
else {
|
1189
|
+
this._validateTimeOrNull(value);
|
1190
|
+
this._updateInputValue();
|
1191
|
+
}
|
1192
|
+
}
|
1193
|
+
/** Implemented as a part of ControlValueAccessor. */
|
1194
|
+
registerOnChange(fn) {
|
1195
|
+
this._onChange = fn;
|
1196
|
+
}
|
1197
|
+
/** Implemented as a part of ControlValueAccessor. */
|
1198
|
+
registerOnTouched(fn) {
|
1199
|
+
this._onTouched = fn;
|
1200
|
+
}
|
1201
|
+
/** Implemented as a part of ControlValueAccessor. */
|
1202
|
+
setDisabledState(isDisabled) {
|
1203
|
+
this._accessorDisabled.set(isDisabled);
|
1204
|
+
}
|
1205
|
+
// implemented as a part of Validator
|
1206
|
+
validate(control) {
|
1207
|
+
if (control.value == null || this.value() == null) {
|
1208
|
+
return { invalidTimeFormat: true };
|
1209
|
+
}
|
1210
|
+
return null;
|
1211
|
+
}
|
1212
|
+
registerOnValidatorChange(fn) {
|
1213
|
+
this._validatorOnChange = fn;
|
1214
|
+
}
|
1215
|
+
/** Sets up the logic that registers the input with the timepicker. */
|
1216
|
+
_registerTimepicker() {
|
1217
|
+
effect(() => {
|
1218
|
+
const timepicker = this.timepicker();
|
1219
|
+
timepicker.registerInput(this);
|
1220
|
+
timepicker.closed.subscribe(() => this._onTouched?.());
|
1221
|
+
});
|
1222
|
+
}
|
1223
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: RkTimepickerInput, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
1224
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.1.6", type: RkTimepickerInput, isStandalone: true, selector: "input[rkTimepicker]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, timepicker: { classPropertyName: "timepicker", publicName: "rkTimepicker", isSignal: true, isRequired: true, transformFunction: null }, disabledInput: { classPropertyName: "disabledInput", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { attributes: { "type": "text" }, listeners: { "input": "_handleInput($event.target.value)", "blur": "_handleBlur()", "keydown": "_handleKeydown($event)" }, properties: { "disabled": "disabled()" }, classAttribute: "rk-timepicker-input" }, providers: [
|
1225
|
+
{
|
1226
|
+
provide: NG_VALUE_ACCESSOR,
|
1227
|
+
useExisting: RkTimepickerInput,
|
1228
|
+
multi: true,
|
1229
|
+
},
|
1230
|
+
{
|
1231
|
+
provide: NG_VALIDATORS,
|
1232
|
+
useExisting: RkTimepickerInput,
|
1233
|
+
multi: true,
|
1234
|
+
},
|
1235
|
+
{
|
1236
|
+
provide: MAT_INPUT_VALUE_ACCESSOR,
|
1237
|
+
useExisting: RkTimepickerInput,
|
1238
|
+
},
|
1239
|
+
], exportAs: ["rkTimepickerInput"], ngImport: i0 });
|
1240
|
+
}
|
1241
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: RkTimepickerInput, decorators: [{
|
1242
|
+
type: Directive,
|
1243
|
+
args: [{
|
1244
|
+
selector: 'input[rkTimepicker]',
|
1245
|
+
exportAs: 'rkTimepickerInput',
|
1246
|
+
host: {
|
1247
|
+
'class': 'rk-timepicker-input',
|
1248
|
+
'type': 'text',
|
1249
|
+
'(input)': '_handleInput($event.target.value)',
|
1250
|
+
'(blur)': '_handleBlur()',
|
1251
|
+
'(keydown)': '_handleKeydown($event)',
|
1252
|
+
'[disabled]': 'disabled()',
|
1253
|
+
},
|
1254
|
+
providers: [
|
1255
|
+
{
|
1256
|
+
provide: NG_VALUE_ACCESSOR,
|
1257
|
+
useExisting: RkTimepickerInput,
|
1258
|
+
multi: true,
|
1259
|
+
},
|
1260
|
+
{
|
1261
|
+
provide: NG_VALIDATORS,
|
1262
|
+
useExisting: RkTimepickerInput,
|
1263
|
+
multi: true,
|
1264
|
+
},
|
1265
|
+
{
|
1266
|
+
provide: MAT_INPUT_VALUE_ACCESSOR,
|
1267
|
+
useExisting: RkTimepickerInput,
|
1268
|
+
},
|
1269
|
+
]
|
1270
|
+
}]
|
1271
|
+
}], ctorParameters: () => [] });
|
1272
|
+
|
1273
|
+
class RkTimepickerToggle {
|
1274
|
+
timepicker = input.required({
|
1275
|
+
alias: 'for',
|
1276
|
+
});
|
1277
|
+
/** Whether the toggle button is disabled. */
|
1278
|
+
disabled = input(false, {
|
1279
|
+
transform: booleanAttribute,
|
1280
|
+
alias: 'disabled',
|
1281
|
+
});
|
1282
|
+
_isDisabled = computed(() => {
|
1283
|
+
const timepicker = this.timepicker();
|
1284
|
+
return this.disabled() || timepicker.disabled();
|
1285
|
+
});
|
1286
|
+
/** Opens the connected timepicker. */
|
1287
|
+
_open(event) {
|
1288
|
+
if (this.timepicker() && !this._isDisabled()) {
|
1289
|
+
this.timepicker().open();
|
1290
|
+
event.stopPropagation();
|
1291
|
+
}
|
1292
|
+
}
|
1293
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: RkTimepickerToggle, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
1294
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.1.6", type: RkTimepickerToggle, isStandalone: true, selector: "rk-timepicker-toggle", inputs: { timepicker: { classPropertyName: "timepicker", publicName: "for", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
1295
|
+
<button mat-icon-button type="button" [disabled]="_isDisabled()" (click)="_open($event)">
|
1296
|
+
<ng-content select="[rkTimepickerToggleIcon]">
|
1297
|
+
<svg
|
1298
|
+
class="rk-timepicker-toggle-default-icon"
|
1299
|
+
height="24px"
|
1300
|
+
width="24px"
|
1301
|
+
viewBox="0 -960 960 960"
|
1302
|
+
fill="currentColor"
|
1303
|
+
focusable="false"
|
1304
|
+
aria-hidden="true">
|
1305
|
+
<path d="m612-292 56-56-148-148v-184h-80v216l172 172ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-400Zm0 320q133 0 226.5-93.5T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 133 93.5 226.5T480-160Z"/>
|
1306
|
+
</svg>
|
1307
|
+
</ng-content>
|
1308
|
+
</button>
|
1309
|
+
`, isInline: true, styles: [""], dependencies: [{ kind: "component", type: MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }] });
|
1310
|
+
}
|
1311
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: RkTimepickerToggle, decorators: [{
|
1312
|
+
type: Component,
|
1313
|
+
args: [{ selector: 'rk-timepicker-toggle', imports: [MatIconButton], template: `
|
1314
|
+
<button mat-icon-button type="button" [disabled]="_isDisabled()" (click)="_open($event)">
|
1315
|
+
<ng-content select="[rkTimepickerToggleIcon]">
|
1316
|
+
<svg
|
1317
|
+
class="rk-timepicker-toggle-default-icon"
|
1318
|
+
height="24px"
|
1319
|
+
width="24px"
|
1320
|
+
viewBox="0 -960 960 960"
|
1321
|
+
fill="currentColor"
|
1322
|
+
focusable="false"
|
1323
|
+
aria-hidden="true">
|
1324
|
+
<path d="m612-292 56-56-148-148v-184h-80v216l172 172ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-400Zm0 320q133 0 226.5-93.5T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 133 93.5 226.5T480-160Z"/>
|
1325
|
+
</svg>
|
1326
|
+
</ng-content>
|
1327
|
+
</button>
|
1328
|
+
` }]
|
1329
|
+
}] });
|
1330
|
+
|
1331
|
+
class RkClock {
|
1332
|
+
value = model(null, { alias: 'time' });
|
1333
|
+
selected = model('hours');
|
1334
|
+
format = model('24h');
|
1335
|
+
period = model('AM');
|
1336
|
+
editable = model(false);
|
1337
|
+
orientation = model('portrait');
|
1338
|
+
inputSupportLabels = input(['Hour', 'Minute'], { alias: 'inputLabels' });
|
1339
|
+
_headline = input(['Enter time', 'Select time'], { alias: 'headline' });
|
1340
|
+
load = signal(false);
|
1341
|
+
isLandscape = computed(() => {
|
1342
|
+
return this.orientation() === 'landscape';
|
1343
|
+
});
|
1344
|
+
headline = computed(() => {
|
1345
|
+
const editable = this.editable();
|
1346
|
+
const labels = this._headline();
|
1347
|
+
return editable ? labels[0] : labels[1];
|
1348
|
+
});
|
1349
|
+
constructor() {
|
1350
|
+
effect(() => {
|
1351
|
+
const value = this.value();
|
1352
|
+
if (!value) {
|
1353
|
+
this.value.set(new Date());
|
1354
|
+
}
|
1355
|
+
;
|
1356
|
+
});
|
1357
|
+
afterRender(() => {
|
1358
|
+
this.load.set(true);
|
1359
|
+
});
|
1360
|
+
}
|
1361
|
+
changeMode() {
|
1362
|
+
this.editable.set(!this.editable());
|
1363
|
+
}
|
1364
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: RkClock, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
1365
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.1.6", type: RkClock, isStandalone: true, selector: "rk-clock", inputs: { value: { classPropertyName: "value", publicName: "time", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, period: { classPropertyName: "period", publicName: "period", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, inputSupportLabels: { classPropertyName: "inputSupportLabels", publicName: "inputLabels", isSignal: true, isRequired: false, transformFunction: null }, _headline: { classPropertyName: "_headline", publicName: "headline", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "timeChange", selected: "selectedChange", format: "formatChange", period: "periodChange", editable: "editableChange", orientation: "orientationChange" }, host: { properties: { "class.rk-timepicker-landscape": "isLandscape()", "class.rk-timepicker-editable": "editable()" }, classAttribute: "rk-timepicker" }, exportAs: ["rkClock"], ngImport: i0, template: `
|
1366
|
+
<div class="rk-timepicker-container" [ngClass]="{ 'rk-timepicker-container-hide': !load() }">
|
1367
|
+
<span class="rk-timepicker-headline">{{ headline() }}</span>
|
1368
|
+
<rk-timepicker-input-label [(time)]="value" [inputLabels]="inputSupportLabels()" [(editable)]="editable" [(format)]="format" [(period)]="period" [(selected)]="selected"></rk-timepicker-input-label>
|
1369
|
+
<rk-timepicker-dial [ngClass]="{ 'rk-dial-closed': editable()}" [(time)]="value" [(format)]="format" [(period)]="period" [(selected)]="selected"></rk-timepicker-dial>
|
1370
|
+
<div class="rk-timepicker-footer">
|
1371
|
+
<button class="rk-timepicker-mode-button" (click)="changeMode()" mat-icon-button>
|
1372
|
+
@if (editable()) {
|
1373
|
+
<mat-icon>schedule</mat-icon>
|
1374
|
+
} @else {
|
1375
|
+
<mat-icon>keyboard</mat-icon>
|
1376
|
+
}
|
1377
|
+
</button>
|
1378
|
+
</div>
|
1379
|
+
</div>
|
1380
|
+
`, isInline: true, styles: [""], dependencies: [{ kind: "component", type: RkTimepickerInputLabel, selector: "rk-timepicker-input-label", inputs: ["time", "selected", "period", "format", "editable", "inputLabels"], outputs: ["timeChange", "selectedChange", "periodChange", "formatChange", "editableChange"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: RkTimepickerDial, selector: "rk-timepicker-dial", inputs: ["time", "format", "selected", "period"], outputs: ["timeChange", "formatChange", "selectedChange", "periodChange"] }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
1381
|
+
}
|
1382
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: RkClock, decorators: [{
|
1383
|
+
type: Component,
|
1384
|
+
args: [{ selector: 'rk-clock', exportAs: 'rkClock', imports: [RkTimepickerInputLabel, NgClass, RkTimepickerDial, MatButton, MatIconButton, MatIcon], host: {
|
1385
|
+
'class': 'rk-timepicker',
|
1386
|
+
'[class.rk-timepicker-landscape]': 'isLandscape()',
|
1387
|
+
'[class.rk-timepicker-editable]': 'editable()',
|
1388
|
+
}, template: `
|
1389
|
+
<div class="rk-timepicker-container" [ngClass]="{ 'rk-timepicker-container-hide': !load() }">
|
1390
|
+
<span class="rk-timepicker-headline">{{ headline() }}</span>
|
1391
|
+
<rk-timepicker-input-label [(time)]="value" [inputLabels]="inputSupportLabels()" [(editable)]="editable" [(format)]="format" [(period)]="period" [(selected)]="selected"></rk-timepicker-input-label>
|
1392
|
+
<rk-timepicker-dial [ngClass]="{ 'rk-dial-closed': editable()}" [(time)]="value" [(format)]="format" [(period)]="period" [(selected)]="selected"></rk-timepicker-dial>
|
1393
|
+
<div class="rk-timepicker-footer">
|
1394
|
+
<button class="rk-timepicker-mode-button" (click)="changeMode()" mat-icon-button>
|
1395
|
+
@if (editable()) {
|
1396
|
+
<mat-icon>schedule</mat-icon>
|
1397
|
+
} @else {
|
1398
|
+
<mat-icon>keyboard</mat-icon>
|
1399
|
+
}
|
1400
|
+
</button>
|
1401
|
+
</div>
|
1402
|
+
</div>
|
1403
|
+
`, standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None }]
|
1404
|
+
}], ctorParameters: () => [] });
|
1405
|
+
|
1406
|
+
/*
|
1407
|
+
* Public API Surface of timepicker
|
1408
|
+
*/
|
1409
|
+
|
1410
|
+
/**
|
1411
|
+
* Generated bundle index. Do not edit.
|
1412
|
+
*/
|
1413
|
+
|
1414
|
+
export { HOURS_LABEL, HOURS_LABEL_24_P1, HOURS_LABEL_24_P2, MINUTES_LABEL, RkClock, RkTimepicker, RkTimepickerInput, RkTimepickerToggle, amPmTransform, angleToHours, angleToHours24, angleToMinutes, createPointerEvents, detectTimeFormat, formatTimeValue, hours24ToAngle, hoursToAngle, minutesToAngle, normalizeEvent, secondsToDate, snapAngle, splitDate, timeToSeconds, timeToSecondsi18n, validateTimeValue };
|
1415
|
+
//# sourceMappingURL=redkhat-timepicker.mjs.map
|