UNPKG

@redkhat/timepicker

Version:

This project contains a series of time selection components. The timepicker allows users to select a time from a clock-like interface. The timepicker is built using **Angular 19+** and **Angular Material 19+**.

1 lines 98.8 kB
{"version":3,"file":"redkhat-timepicker.mjs","sources":["../../../../src/timepicker/src/lib/utils/clock-iterable.ts","../../../../src/timepicker/src/lib/utils/time.ts","../../../../src/timepicker/src/lib/utils/animation.ts","../../../../src/timepicker/src/lib/utils/events.ts","../../../../src/timepicker/src/lib/utils/angle.ts","../../../../src/timepicker/src/lib/utils/constants.ts","../../../../src/timepicker/src/lib/timepicker-dial.ts","../../../../src/timepicker/src/lib/timepicker-input-label.ts","../../../../src/timepicker/src/lib/timepicker.ts","../../../../src/timepicker/src/lib/timepicker-input.ts","../../../../src/timepicker/src/lib/timepicker-toggle.ts","../../../../src/timepicker/src/lib/clock.ts","../../../../src/timepicker/src/public-api.ts","../../../../src/timepicker/src/redkhat-timepicker.ts"],"sourcesContent":["\n/**\n * A class that implements the `Iterable` interface to generate coordinates for clock labels.\n * \n * @template T - The type of the labels.\n */\nexport class ClockIterable<T> implements Iterable<[T, number, number]> {\n /**\n * The labels to be positioned around the clock.\n */\n private labels: T[];\n\n /**\n * The radius of the clock.\n */\n private radio: number;\n\n /**\n * The size of each label.\n */\n private labelSize: number;\n\n /**\n * The inset distance from the edge of the clock.\n */\n private inset: number;\n\n /**\n * The degrees between each label.\n */\n private readonly degreesByNumber = 30;\n\n /**\n * Creates an instance of ClockIterable.\n * \n * @param labels - An array of labels to be positioned around the clock [12, 1, 2, 3...].\n * @param labelSize - The size of each label.\n * @param diameter - The diameter of the clock.\n * @param inset - The inset distance from the edge of the clock (default is 0).\n */\n constructor(labels: T[], labelSize: number, diameter: number, inset: number = 0) {\n this.labels = labels;\n this.labelSize = labelSize / 2;\n this.radio = (diameter / 2) - inset;\n this.inset = inset;\n }\n\n /**\n * Returns an iterator that yields the label and its x, y coordinates.\n * \n * @returns An iterator that yields a tuple containing the label and its x, y coordinates.\n */\n *[Symbol.iterator](): Iterator<[T, number, number]> {\n for (let i = 0; i < this.labels.length; i++) {\n const x = this.radio + (this.radio - this.labelSize) * Math.sin(i * this.degreesByNumber * Math.PI / 180);\n const y = this.radio - (this.radio - this.labelSize) * Math.cos(i * this.degreesByNumber * Math.PI / 180);\n yield [this.labels[i], this.inset + x - this.labelSize, this.inset + y - this.labelSize];\n }\n }\n}\n","export function secondsToDate(seconds: number): Date {\n const initialDate = new Date(0, 0, 0, 0);\n const date = new Date(initialDate.getTime() + seconds * 1000);\n return date;\n}\n\nexport function detectTimeFormat(date: Date): string {\n const localTime = date.toLocaleTimeString();\n if (localTime.includes('AM') || localTime.includes('PM')) {\n return '12h';\n } else {\n return '24h';\n }\n}\n\nexport function splitDate(date: Date): SimpleTime {\n const hours = date.getHours();\n const minutes = date.getMinutes();\n return { hours, minutes };\n}\n\nexport function amPmTransform(isAm: string, date: Date) : Date {\n const newDate = new Date(date);\n const tempHours = newDate.getHours();\n if (isAm === 'AM' && tempHours >= 12) {\n newDate.setHours(tempHours - 12);\n } else if (isAm === 'PM' && tempHours < 12) {\n newDate.setHours(tempHours + 12);\n }\n return newDate;\n}\n\nexport function validateTimeValue(value: string, max: number): string {\n let numericValue = value.replace(/[^0-9]/g, '');\n\n if (numericValue.length > 2) {\n numericValue = numericValue.slice(0, 2);\n }\n\n if (numericValue === '') {\n return '';\n }\n const num = parseInt(numericValue, 10);\n return num > max ? max.toString() : numericValue;\n}\n\nexport function formatTimeValue(value: string, defaultValue: string, padLength: number = 2): string {\n if (value === '') {\n return defaultValue;\n }\n const num = parseInt(value, 10);\n if (num === 0) {\n return defaultValue;\n }\n return value.padStart(padLength, '0');\n}\n\nexport interface SimpleTime {\n hours: number;\n minutes: number;\n}\n\n\n/**\n * Converts a time string or number to the number of seconds since midnight.\n *\n * Supported formats include:\n * - Integers (e.g., \"10\", \"1030\", 10, 1030) in 24-hour format (up to 2359).\n * - Space-separated numbers (e.g., \"10 30\") as HH MM.\n * - Colon-separated numbers (e.g., \"10:30\" or \"10:30:00\") as HH:MM or HH:MM:SS.\n * - Formats with regional AM/PM markers either at the beginning or at the end\n * (e.g., \"10:30am\", \"am 10:30\", \"午後3:25:00\", \"10:30 du matin\").\n *\n * Additionally, this function normalizes Arabic digits (Unicode range \\u0660-\\u0669)\n * to their Western counterparts before processing.\n *\n * @param input The input to convert (can be a string or number).\n * @returns The number of seconds since midnight, or null if the input is invalid.\n */\nexport function timeToSecondsi18n(input: string | number): number | null {\n // Convert input to string and trim whitespace.\n let inputStr = typeof input === 'number' ? String(input) : input;\n inputStr = inputStr.trim();\n\n // Normalize Arabic digits to Western digits if found.\n if (/[\\u0660-\\u0669]/.test(inputStr)) {\n inputStr = inputStr.replace(/[\\u0660-\\u0669]/g, d => (d.charCodeAt(0) - 0x0660).toString());\n }\n // Extended list of AM and PM markers across various regions/languages.\n const AM_MARKERS = [\n 'am', 'a.m.', 'a.m', // English\n '午前', // Japanese\n '上午', // Chinese\n '오전', // Korean\n 'ص', 'صباح', 'صباحاً', // Arabic\n 'du matin', 'matin' // French\n ];\n const PM_MARKERS = [\n 'pm', 'p.m.', 'p.m', // English\n '午後', // Japanese\n '下午', // Chinese\n '오후', // Korean\n 'م', 'مساء', 'مساءً', // Arabic\n \"de l’après-midi\", \"de l'aprés-midi\", \"de l'après-midi\",\n 'après-midi', 'du soir', 'soir' // French\n ];\n\n let period: 'AM' | 'PM' | null = null;\n\n // Helper functions to compare prefix/suffix ignoring case.\n const startsWithIgnoreCase = (str: string, prefix: string) =>\n str.substring(0, prefix.length).toLowerCase() === prefix.toLowerCase();\n const endsWithIgnoreCase = (str: string, suffix: string) =>\n str.substring(str.length - suffix.length).toLowerCase() === suffix.toLowerCase();\n\n // Check if the period marker is at the beginning.\n for (const marker of AM_MARKERS) {\n if (startsWithIgnoreCase(inputStr, marker)) {\n period = 'AM';\n inputStr = inputStr.substring(marker.length).trim();\n break;\n }\n }\n if (!period) {\n for (const marker of PM_MARKERS) {\n if (startsWithIgnoreCase(inputStr, marker)) {\n period = 'PM';\n inputStr = inputStr.substring(marker.length).trim();\n break;\n }\n }\n }\n // If not found at the beginning, check at the end.\n if (!period) {\n for (const marker of AM_MARKERS) {\n if (endsWithIgnoreCase(inputStr, marker)) {\n period = 'AM';\n inputStr = inputStr.substring(0, inputStr.length - marker.length).trim();\n break;\n }\n }\n }\n if (!period) {\n for (const marker of PM_MARKERS) {\n if (endsWithIgnoreCase(inputStr, marker)) {\n period = 'PM';\n inputStr = inputStr.substring(0, inputStr.length - marker.length).trim();\n break;\n }\n }\n }\n\n // Split the numeric part of the time.\n let timeParts: number[] = [];\n\n if (inputStr.includes(':')) {\n // Colon-separated format. Accepts HH:MM or HH:MM:SS.\n const parts = inputStr.split(':');\n if (parts.length < 2 || parts.length > 3) return null;\n for (const part of parts) {\n const num = parseInt(part, 10);\n if (isNaN(num)) return null;\n timeParts.push(num);\n }\n } else if (inputStr.includes(' ')) {\n // Space-separated format (e.g., \"10 30\").\n const parts = inputStr.split(/\\s+/);\n if (parts.length > 3) return null;\n for (const part of parts) {\n const num = parseInt(part, 10);\n if (isNaN(num)) return null;\n timeParts.push(num);\n }\n } else if (/^\\d+$/.test(inputStr)) {\n // Numeric case without separators: \"10\", \"1030\".\n if (inputStr.length <= 2) {\n timeParts = [parseInt(inputStr, 10)];\n } else if (inputStr.length === 3) {\n timeParts = [\n parseInt(inputStr.substring(0, 1), 10),\n parseInt(inputStr.substring(1), 10),\n ];\n } else if (inputStr.length === 4) {\n timeParts = [\n parseInt(inputStr.substring(0, 2), 10),\n parseInt(inputStr.substring(2), 10),\n ];\n } else {\n // Unsupported numeric format with more than 4 digits.\n return null;\n }\n } else {\n // Unrecognized format.\n return null;\n }\n\n // Extract hour, minute, and second (defaulting to 0 if not provided).\n let hour = timeParts[0];\n let minute = timeParts.length >= 2 ? timeParts[1] : 0;\n let second = timeParts.length === 3 ? timeParts[2] : 0;\n\n // Validate minute and second ranges.\n if (minute < 0 || minute > 59) return null;\n if (second < 0 || second > 59) return null;\n\n // Adjust hour based on the AM/PM marker.\n if (period) {\n if (hour < 1 || hour > 12) return null;\n if (period === 'PM') {\n hour = hour === 12 ? 12 : hour + 12;\n } else {\n hour = hour === 12 ? 0 : hour;\n }\n } else {\n // Without a period, assume 24-hour format.\n if (hour < 0 || hour > 23) return null;\n }\n\n return hour * 3600 + minute * 60 + second;\n}\n","export function cubicBezier(p1x: number, p1y: number, p2x: number, p2y: number, t: number): number {\n const cx = 3.0 * p1x;\n const bx = 3.0 * (p2x - p1x) - cx;\n const ax = 1.0 - cx - bx;\n const cy = 3.0 * p1y;\n const by = 3.0 * (p2y - p1y) - cy;\n const ay = 1.0 - cy - by;\n\n let x = t;\n for (let i = 0; i < 4; i++) {\n const t_0 = x * x * x * ax + x * x * bx + x * cx;\n const t_1 = 3 * x * x * ax + 2 * x * bx + cx;\n x = x - (t_0 - t) / t_1;\n }\n return x * x * x * ay + x * x * by + x * cy;\n}\n","import { fromEvent, merge, Observable, tap } from \"rxjs\";\n\nexport interface MouseTouchEvent {\n clientX: number;\n clientY: number;\n pageX: number;\n pageY: number;\n screenX: number;\n screenY: number;\n target: EventTarget | null;\n}\n\n export function normalizeEvent(event: MouseEvent | TouchEvent | Touch): MouseTouchEvent {\n if (event instanceof MouseEvent) {\n return event;\n } else if (event instanceof TouchEvent) {\n return event.touches[0] || event.changedTouches[0];\n } else if (event instanceof Touch) {\n return {\n clientX: event.clientX,\n clientY: event.clientY,\n pageX: event.pageX,\n pageY: event.pageY,\n screenX: event.screenX,\n screenY: event.screenY,\n target: event.target\n };\n }\n return event;\n}\n\n export function createPointerEvents(element: HTMLElement | Element): {\n start$: Observable<PointerEvent>;\n move$: Observable<PointerEvent>;\n end$: Observable<PointerEvent>;\n } {\n const pointerDown$ = fromEvent<PointerEvent>(element, 'pointerdown');\n const pointerMove$ = fromEvent<PointerEvent>(document, 'pointermove');\n const pointerUp$ = fromEvent<PointerEvent>(document, 'pointerup');\n const pointerLeave$ = fromEvent<PointerEvent>(document, 'pointerleave');\n\n const start$ = pointerDown$;\n const move$ = pointerMove$;\n const end$ = merge(pointerUp$, pointerLeave$);\n\n return { start$, move$, end$ };\n}\n","export function snapAngle(angle: number, steps: number): number {\n // Calc the size of each step in the scale. 12 for hours, 60 for minutes\n const stepSize = 360 / steps;\n // Find the closest multiple of the step size. 12 for hours, 60 for minutes\n const closestStep = Math.round(angle / stepSize);\n // Calc the snapped angle\n const snappedAngle = closestStep * stepSize;\n\n return snappedAngle;\n}\n\nexport function hoursToAngle(hour: number): number {\n const angle = (hour - 3) * 30;\n return (angle + 360) % 360;\n}\n\nexport function minutesToAngle(minute: number): number {\n let angle = (minute - 15) * 6;\n angle = (angle + 360) % 360;\n\n return angle;\n}\n\nexport function hours24ToAngle(hour: number): number {\n let angle: number;\n\n if (hour >= 0 && hour <= 11) {\n angle = (hour - 3) * 30; // Dial 1: 0 to 11\n } else {\n angle = (hour - 15) * 30; // Dial 2: 12 to 23\n }\n\n angle = (angle + 360) % 360;\n\n return angle;\n}\n\nexport function angleToHours(angle: number): number {\n // Normalize the angle to be within 0-360 range\n angle = (angle + 360) % 360;\n // Correct mapping: 0 degrees -> 3, 270 degrees -> 12\n let hour = Math.floor((angle + 90) / 30) % 12;\n // Handle the 0 hour case (midnight)\n if (hour === 0) {\n hour = 12;\n }\n\n return hour;\n}\n\nexport function angleToMinutes(angle: number): number {\n // Normalize the angle\n angle = (angle + 360) % 360;\n\n let minute = (angle / 6) + 15;\n\n minute = (minute + 60) % 60; // Ensure positive value and wrap around 60\n\n return Math.floor(minute);\n}\n\nexport function angleToHours24(angle: number, dial: number): number {\n angle = (angle + 360) % 360;\n\n let hour: number = 0;\n const baseValue = angle / 30;\n\n if (dial === 1) { \n hour = (Math.floor(baseValue + 3) % 12 + 12) % 12;\n } else if (dial === 2) { \n hour = (Math.floor(baseValue + 3) % 12) + 12;\n }\n \n return hour;\n}","export const HOURS_LABEL = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];\nexport const HOURS_LABEL_24_P1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];\nexport const HOURS_LABEL_24_P2 = [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23];\nexport const MINUTES_LABEL = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55];\n","import { Component, ChangeDetectionStrategy, ViewEncapsulation, ModelSignal, model, viewChild, ElementRef, Renderer2, DestroyRef, inject, computed, afterNextRender, effect, Injector, WritableSignal, signal, untracked } from '@angular/core';\nimport { DOCUMENT } from '@angular/common';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { switchMap, takeUntil, tap } from 'rxjs';\n\nimport { ClockIterable } from './utils/clock-iterable';\nimport { amPmTransform, detectTimeFormat, SimpleTime, splitDate } from './utils/time';\nimport { cubicBezier } from './utils/animation';\nimport { normalizeEvent, createPointerEvents, MouseTouchEvent } from './utils/events';\nimport { snapAngle, hoursToAngle, angleToHours, hours24ToAngle, minutesToAngle, angleToHours24, angleToMinutes } from './utils/angle';\nimport { HOURS_LABEL, HOURS_LABEL_24_P1, HOURS_LABEL_24_P2, MINUTES_LABEL } from './utils/constants';\n\n@Component({\n selector: 'rk-timepicker-dial',\n host: {\n 'class': 'rk-timepicker-dial',\n },\n imports: [],\n template: `\t\n <div class=\"rk-timepicker-dial-container\">\n <div #dial class=\"rk-label-container rk-dial-size\">\n </div>\n <div #dial2 class=\"rk-cliped-label rk-dial-size\">\n </div>\n <div #dialSelector class=\"rk-dial-selector\"></div>\n <div class=\"rk-pivot-point\"></div>\n </div>\n `,\n styles: [],\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.None\n})\nexport class RkTimepickerDial {\n currentTime: ModelSignal<Date | null> = model.required<Date | null>({alias: 'time'});\n timeFormat: ModelSignal<string | null> = model<string | null>('12h', {alias: 'format'});\n selectedTime: ModelSignal<string | null> = model<string | null>('hours', {alias: 'selected'});\n period: ModelSignal<string> = model<string>('AM');\n\n readonly value = computed(() => {\n const date = this.currentTime();\n return date ? splitDate(date) : { hours: 0, minutes: 0 };\n });\n readonly format = computed(() => {\n const fm = this.timeFormat();\n return (fm && (fm === '24h' || fm === '12h')) ? fm : detectTimeFormat(new Date());\n });\n readonly selected = computed(() => {\n const selected = this.selectedTime();\n return selected === 'hours' || selected === 'minutes' ? selected : 'hours';\n });\n\n private _dial = viewChild.required<ElementRef>('dial');\n private _clipedLabel = viewChild.required<ElementRef>('dial2');\n private _dialSelector = viewChild.required<ElementRef>('dialSelector');\n\n private _elRef: ElementRef = inject(ElementRef);\n private _renderer: Renderer2 = inject(Renderer2);\n private _destroyRef: DestroyRef = inject(DestroyRef);\n private _injector = inject(Injector);\n private _document = inject(DOCUMENT);\n\n private _currentDial = signal(1);\n private _currentDegree: WritableSignal<number> = signal(0);\n private _loading = signal(false);\n private readonly _motion = 250;\n private readonly _radius = 128;\n private readonly _labelSize = 48;\n \n constructor() {\n afterNextRender(() => {\n this._fillBySelected(this.selected(), this.format());\n\n effect(() => {\n const selected = this.selected();\n const format = this.format();\n this._fillBySelected(selected, format);\n untracked(() => {\n const value = this.value();\n this.period.set(this.value().hours < 12 ? 'AM' : 'PM');\n this._currentDial.set(format === '24h' && selected === 'hours' && value.hours >= 12 ? 2 : 1);\n this._handleInputsChange(value, format, selected);\n });\n\n }, { injector: this._injector});\n\n effect(() => {\n const value = this.value();\n untracked(() => {\n if(this._loading()) return;\n const format = this.format();\n const selected = this.selected();\n this.period.set(this.value().hours < 12 ? 'AM' : 'PM');\n this._currentDial.set(format === '24h' && selected === 'hours' && value.hours >= 12 ? 2 : 1);\n this._handleInputsChange(value, format, selected, true);\n })\n }, { injector: this._injector});\n\n effect(() => {\n const period = this.period();\n untracked(() => {\n const currentTime = this.currentTime();\n if(period === null || currentTime === null) return;\n this.currentTime.set(amPmTransform(period, currentTime));\n })\n }, { injector: this._injector});\n\n const { start$, move$, end$ } = createPointerEvents(this._elRef.nativeElement);\n start$.pipe(\n tap((event) => this._onPointerEventInit(normalizeEvent(event))),\n switchMap(() => {\n return move$.pipe(\n takeUntil(end$.pipe(tap((event) => this._onPointerEventStop(normalizeEvent(event)))))\n );\n }),\n takeUntilDestroyed(this._destroyRef)\n ).subscribe(event => {\n const normalized = normalizeEvent(event);\n this._onPointerEventInit(normalized);\n })\n });\n }\n\n private _handleInputsChange(value: SimpleTime, format: string, selected: string, animate = true) {\n if(selected === 'hours') {\n const angle = format === '24h' ? hours24ToAngle(value.hours) : hoursToAngle(value.hours);\n if(animate) {\n this._rotateAnimation(this._currentDegree(), angle, this._motion, 2);\n } else {\n this._moveByAngle(angle, 2);\n }\n this._currentDegree.set(angle);\n } else {\n const angle = minutesToAngle(value.minutes);\n if(animate) {\n this._rotateAnimation(this._currentDegree(), angle, this._motion, 2);\n } else {\n this._moveByAngle(angle, 2);\n }\n this._currentDegree.set(angle);\n }\n }\n\n private _onPointerEventInit(event: MouseTouchEvent) {\n this._loading.set(true);\n this._moveByTouchClick(event, this._dial().nativeElement);\n }\n\n private async _onPointerEventStop(event: MouseTouchEvent) {\n const currentDegree = this._currentDegree();\n const snapDegrees = this.selected() === 'hours' ? snapAngle(currentDegree, 12) : snapAngle(currentDegree, 60);\n\n await this._rotateAnimation(this._currentDegree(), snapDegrees, this._motion / 2, 2);\n this._currentDegree.set(snapDegrees);\n this._loading.set(false);\n if(this.selected() === 'hours') {\n this.selectedTime.set('minutes');\n }\n }\n\n private _fillDial(dial: HTMLElement, labels: number[], withClean: boolean, inset: number = 2) {\n if (withClean) {\n dial.innerHTML = '';\n }\n const clockIterable = new ClockIterable(labels, this._labelSize, this._radius * 2, inset);\n for (const [label, x, y] of clockIterable) {\n const numero = this._document.createElement('div');\n numero.classList.add('rk-clock-dial-label')\n numero.textContent = this.selected() === 'hours' ? label.toString() : label.toString().padStart(2, '0');\n numero.style.position = 'absolute';\n numero.style.left = x + 'px';\n numero.style.top = y + 'px';\n this._renderer.appendChild(dial, numero);\n }\n }\n\n private _fillBySelected(selected: string, format: string) {\n switch (selected) {\n case 'hours':\n if (format === '12h') {\n this._fillDial(this._dial().nativeElement, HOURS_LABEL, true);\n this._fillDial(this._clipedLabel().nativeElement, HOURS_LABEL, true);\n } else if (format === '24h') {\n this._fillDial(this._dial().nativeElement, HOURS_LABEL_24_P1, true);\n this._fillDial(this._dial().nativeElement, HOURS_LABEL_24_P2, false, 38);\n this._fillDial(this._clipedLabel().nativeElement, HOURS_LABEL_24_P1, true);\n this._fillDial(this._clipedLabel().nativeElement, HOURS_LABEL_24_P2, false, 38);\n }\n break;\n case 'minutes':\n this._fillDial(this._dial().nativeElement, MINUTES_LABEL, true);\n this._fillDial(this._clipedLabel().nativeElement, MINUTES_LABEL, true);\n break;\n default:\n this._fillDial(this._dial().nativeElement, HOURS_LABEL, true);\n this._fillDial(this._clipedLabel().nativeElement, HOURS_LABEL, true);\n }\n }\n\n private _moveByTouchClick(event: MouseTouchEvent, dial: HTMLElement, inset: number = 2) {\n const rect = dial.getBoundingClientRect();\n // relative to the center of the clock\n const x = event.clientX - rect.left - this._radius;\n const y = event.clientY - rect.top - this._radius;\n\n // Calc angle in degrees\n let angle = Math.atan2(y, x) * (180 / Math.PI);\n if (angle < 0) {\n angle += 360; //make sure the angle is in the range [0, 360]\n }\n \n // Calc new position for the object (using radius as reference)\n let radius = this._radius - (this._labelSize / 2) - inset;\n\n//--------------------------------------------------------------------------------------------------------------//\n // logic when format is 24h and two dial exist\n const distanceFromCenter = Math.sqrt(x * x + y * y);\n const MAX_RAD = radius - 20;\n let selectorInset = 0;\n \n if (distanceFromCenter < MAX_RAD && this.selected() === 'hours' && this.format() === '24h') {\n radius -= 36;\n this._currentDial.set(2);\n selectorInset = 36;\n } else {\n this._currentDial.set(1);\n selectorInset = 0;\n }\n \n//----------------------------------------------------------------------------------------------------------------//\n\n const moveX = this._radius + radius * Math.cos(angle * (Math.PI / 180));\n const moveY = this._radius + radius * Math.sin(angle * (Math.PI / 180));\n // Call the provided functions with the calculated values\n this._moveTrack(moveX, moveY);\n this._moveSelector(angle, selectorInset);\n this._currentDegree.set(angle);\n\n //update time\n const snapDegrees = this.selected() === 'hours' ? snapAngle(angle, 12) : snapAngle(angle, 60);\n const currentDate = this.currentTime();\n const date = currentDate ? new Date(currentDate) : new Date();\n \n if(this.selected() === 'hours') {\n let hours = this.format() === '24h' ? angleToHours24(snapDegrees, this._currentDial()) : angleToHours(snapDegrees);\n if(this.format() === '24h') {\n this.period.set(hours < 12 ? 'AM' : 'PM');\n }\n if (this.format() !== '24h' && this.period() === 'PM') {\n hours = hours < 12 ? hours + 12 : hours; \n }\n if(this.period() === 'AM' && hours >= 12 && this.format() === '12h') {\n hours = hours - 12;\n }\n date.setHours(hours);\n } else {\n date.setMinutes(angleToMinutes(snapDegrees));\n }\n \n this.currentTime.set(date);\n }\n\n private _moveByAngle(angle: number, inset: number = 2) {\n // Ensure angle is within 0-360 range\n angle = angle % 360;\n if (angle < 0) {\n angle += 360;\n }\n // Calc new position for the object (using radius as reference)\n let radius = this._radius - (this._labelSize / 2) - inset;\n\n // logic when format is 24h and two dial exist\n let selectorInset = 0;\n if (this.selected() === 'hours' && this.format() === '24h' && this._currentDial() === 2) {\n radius -= 36;\n selectorInset = 36;\n }\n\n const moveX = this._radius + radius * Math.cos(angle * (Math.PI / 180));\n const moveY = this._radius + radius * Math.sin(angle * (Math.PI / 180));\n\n // Call the provided functions with the calculated values\n this._moveTrack(moveX, moveY);\n this._moveSelector(angle, selectorInset);\n }\n\n private _moveTrack(x: number, y: number) {\n this._renderer.setStyle(this._clipedLabel().nativeElement, 'clip-path', `circle(24px at ${x}px ${y}px)`);\n }\n\n private _moveSelector(degrees: number, inset: number = 0) {\n this._renderer.setStyle(this._dialSelector().nativeElement, 'width', 104 - inset + 'px');\n this._renderer.setStyle(this._dialSelector().nativeElement, 'transform', `rotate(${degrees}deg)`);\n }\n\n private async _rotateAnimation(currentDegree: number, degree: number, animationTime: number, inset: number = 2): Promise<void> {\n return new Promise<void>((resolve) => {\n // Calculate the shortest angle difference between the current and target degrees.\n let angleDiff = degree - currentDegree;\n\n // Adjust the angle difference to be within the range of -180 to 180 degrees.\n if (angleDiff > 180) {\n angleDiff -= 360;\n } else if (angleDiff < -180) {\n angleDiff += 360;\n }\n\n // Store the start time of the animation.\n let startTime: number | null = null;\n\n const animate = (timestamp: number) => {\n // Initialize the start time if it hasn't been set yet.\n if (!startTime) startTime = timestamp;\n\n // Calculate the progress of the animation (0 to 1).\n let progress = Math.min((timestamp - startTime) / animationTime, 1);\n\n // Apply a cubic Bezier easing function to the progress. This makes the animation smoother.\n const easedProgress = cubicBezier(0.05, 0.7, 0.1, 1.0, progress);\n\n // Calculate the current angle based on the eased progress.\n const currentAngle = currentDegree + angleDiff * easedProgress;\n\n // Update the position of the dial and selector based on the current angle.\n this._moveByAngle(currentAngle, inset);\n\n // Continue the animation if it's not finished.\n if (progress < 1) {\n requestAnimationFrame(animate);\n } else {\n resolve(); // Resolve the promise when the animation is complete.\n }\n };\n requestAnimationFrame(animate);\n });\n }\n}","import { ChangeDetectionStrategy, Component, computed, effect, ElementRef, input, model, ModelSignal, signal, untracked, viewChild, ViewEncapsulation } from '@angular/core';\nimport { NgClass } from '@angular/common';\nimport { MatRippleModule } from '@angular/material/core';\nimport { formatTimeValue, splitDate, validateTimeValue } from './utils';\nimport { FormsModule } from '@angular/forms';\n\n@Component({\n selector: 'rk-timepicker-input-label',\n imports: [NgClass, MatRippleModule, FormsModule],\n host: {\n 'class': 'rk-time-input-field',\n },\n template: `\n <div class=\"rk-time-input-label-container\">\n <div class=\"rk-time-selector-container\" matRipple [ngClass]=\"{'rk-time-selector-container-selected': selected() === 'hours'}\" (click)=\"selectedChange('hours')\">\n @if (!editable()) {\n {{value().hours.toString().padStart(2, '0')}}\n } @else {\n <input type=\"number\" maxlength=\"2\" #inputH [value]=\"hours()\" (focus)=\"selectedChange('hours')\" (input)=\"onHoursInput($event)\" (keydown)=\"handleKeydown($event, 'hours')\" (blur)=\"onHoursBlur()\" name=\"hours\">\n }\n </div>\n @if (editable()) {\n <span class=\"rk-time-input-support-label\">{{inputSupportLabels()[0]}}</span>\n }\n </div>\n <div class=\"rk-time-divider\">:</div>\n <div class=\"rk-time-input-label-container\">\n <div class=\"rk-time-selector-container\" matRipple [ngClass]=\"{'rk-time-selector-container-selected': selected() === 'minutes'}\" (click)=\"selectedChange('minutes')\">\n @if (!editable()) {\n {{value().minutes.toString().padStart(2, '0')}}\n } @else {\n <input type=\"number\" maxlength=\"2\" #inputM [value]=\"minutes()\" (focus)=\"selectedChange('minutes')\" (input)=\"onMinutesInput($event)\" (keydown)=\"handleKeydown($event, 'minutes')\" (blur)=\"onMinutesBlur()\" name=\"minutes\">\n }\n </div>\n @if (editable()) {\n <span class=\"rk-time-input-support-label\">{{inputSupportLabels()[1]}}</span>\n }\n </div>\n @if (format() === '12h') {\n <div class=\"rk-period-selector-container\">\n <div class=\"rk-period-selector\" matRipple [ngClass]=\"{'rk-period-selector-selected': period() === 'AM'}\" (click)=\"periodSelector('AM')\">AM</div>\n <div class=\"rk-period-selector\" matRipple [ngClass]=\"{'rk-period-selector-selected': period() === 'PM'}\" (click)=\"periodSelector('PM')\">PM</div>\n </div>\n }\n `,\n styles: ``,\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.None\n})\nexport class RkTimepickerInputLabel {\n currentTime: ModelSignal<Date | null> = model.required<Date | null>({alias: 'time'});\n selected = model('hours');\n period = model('AM');\n format = model('12h');\n editable = model(false);\n\n hours = signal<string>('');\n minutes = signal<string>('');\n\n inputSupportLabels = input<string[]>(['Hour', 'Minute'], {alias: 'inputLabels'});\n inputHours = viewChild<ElementRef<HTMLInputElement>>('inputH');\n inputMinutes = viewChild<ElementRef<HTMLInputElement>>('inputM');\n\n readonly value = computed(() => {\n const date = this.currentTime();\n const format = this.format();\n if (date) {\n const { hours, minutes } = splitDate(date);\n if (format === '12h') {\n return { hours: hours > 12 ? hours - 12 : hours === 0 ? 12 : hours, minutes };\n }\n return { hours, minutes };\n }\n return { hours: 0, minutes: 0 };\n });\n\n constructor() {\n effect(() => {\n const editable = this.editable();\n const selected = this.selected();\n if (editable) {\n if (selected=== 'hours') {\n this.inputHours()?.nativeElement.focus();\n } else if (selected === 'minutes') {\n this.inputMinutes()?.nativeElement.focus();\n }\n }\n });\n\n effect(() => {\n const value = this.value();\n untracked(() => {\n this.onHoursInput(value.hours.toString().padStart(2, '0'));\n this.onMinutesInput(value.minutes.toString().padStart(2, '0'));\n })\n })\n }\n\n onHoursInput(event: Event|string): void {\n const input = this.inputHours()?.nativeElement;\n const value = typeof event === 'string' ? event : (<HTMLInputElement>event.target)?.value;\n const max = this.format() === '24h' ? 23 : 12;\n const validatedValue = validateTimeValue(value, max);\n\n if (input &&input.value !== validatedValue) {\n input.value = validatedValue;\n }\n this.hours.set(validatedValue);\n\n }\n \n onMinutesInput(event: Event|string): void {\n const input = this.inputMinutes()?.nativeElement;\n const value = typeof event === 'string' ? event : (<HTMLInputElement>event.target)?.value;\n const validatedValue = validateTimeValue(value, 59);\n\n if (input &&input.value !== validatedValue) {\n input.value = validatedValue;\n }\n\n this.minutes.set(validatedValue);\n }\n\n onHoursBlur(): void {\n const defaultHour = this.format() === '24h' ? '00' : '01';\n const formattedValue = formatTimeValue(this.hours(), defaultHour);\n this.hours.set(formattedValue);\n this.updateModel();\n }\n\n onMinutesBlur(): void {\n const formattedValue = formatTimeValue(this.minutes(), '00');\n this.minutes.set(formattedValue);\n this.updateModel();\n }\n\n updateModel() {\n const tempDate = this.currentTime();\n if(tempDate) {\n const newDate = new Date(tempDate);\n let hoursN = Number(this.hours());\n const minutesN = Number(this.minutes());\n if (this.format() === '12h' && this.period() === 'PM') {\n hoursN = hoursN < 12 ? hoursN + 12 : hoursN;\n }\n newDate.setHours(hoursN);\n newDate.setMinutes(minutesN);\n this.currentTime.set(newDate);\n }\n }\n handleKeydown(event: KeyboardEvent, selected: string) {\n if (['Enter', 'Escape', 'Tab'].includes(event.code)) {\n if(selected === 'hours') {\n this.onHoursBlur();\n this.selected.set('minutes');\n } else {\n this.onMinutesBlur();\n }\n }\n }\n\n selectedChange(selected: string) {\n if (this.selected() === selected) {\n return;\n }\n this.selected.set(selected);\n }\n\n periodSelector(period: string) {\n if (this.period() === period) {\n return;\n }\n this.period.set(period);\n }\n}","import { Component, ChangeDetectionStrategy, ViewEncapsulation, model, input, computed, effect, signal, output, afterRender, InputSignal, viewChild, TemplateRef, OutputEmitterRef, inject, untracked, Signal } from '@angular/core';\nimport { NgClass } from '@angular/common';\nimport { MatButton, MatIconButton } from '@angular/material/button';\nimport { MatIcon } from '@angular/material/icon';\nimport { Dialog, DialogModule, DialogRef } from '@angular/cdk/dialog';\nimport { RkTimepickerDial } from './timepicker-dial';\nimport { RkTimepickerInputLabel } from './timepicker-input-label';\nimport { RkTimepickerInput } from './timepicker-input';\n\n@Component({\n selector: 'rk-timepicker',\n exportAs: 'rkTimepicker',\n imports: [RkTimepickerInputLabel, NgClass, DialogModule, RkTimepickerDial, MatButton, MatIconButton, MatIcon],\n host: {\n '[class.rk-timepicker-landscape]': 'isLandscape()',\n '[class.rk-timepicker-editable]': 'editable()',\n '(keydown)': '_handleKeydown($event)',\n },\n template: `\n <ng-template #panelTemplate>\n <div class=\"rk-timepicker rk-timepicker-panel\">\n <div class=\"rk-timepicker-container\" [ngClass]=\"{ 'rk-timepicker-container-hide': !load() }\">\n <span class=\"rk-timepicker-headline\">{{ headline() }}</span>\n <rk-timepicker-input-label [(time)]=\"currentTime\" [inputLabels]=\"_inputSupportLabels()\" [(editable)]=\"editable\" [(format)]=\"format\" [(period)]=\"period\" [(selected)]=\"selected\"></rk-timepicker-input-label>\n <rk-timepicker-dial [ngClass]=\"{ 'rk-dial-closed': editable(), 'rk-dial-animated': animated()}\" [(time)]=\"currentTime\" [(format)]=\"format\" [(period)]=\"period\" [(selected)]=\"selected\"></rk-timepicker-dial>\n <div class=\"rk-timepicker-footer\">\n <button class=\"rk-timepicker-mode-button\" (click)=\"changeMode()\" mat-icon-button>\n @if (editable()) {\n <mat-icon>schedule</mat-icon>\n } @else {\n <mat-icon>keyboard</mat-icon> \n }\n </button>\n <div class=\"rk-timepicker-actions\">\n <button mat-button (click)=\"onCancel()\">{{_actions()[0]}}</button>\n <button mat-button (click)=\"onConfirm()\">{{_actions()[1]}}</button>\n </div>\n </div>\n </div>\n </div>\n </ng-template>\n `,\n styles: ``,\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.None,\n})\nexport class RkTimepicker {\n selected = model<string>('hours');\n format = model('12h');\n period = model<string>('AM');\n editable = model(false);\n orientation = model<string>('portrait');\n \n _inputSupportLabels = input<string[]>(['Hour', 'Minute'], {alias: 'inputLabels'});\n _headline = input<string[]>(['Enter time', 'Select time'], {alias: 'headline'});\n _actions = input<string[]>(['Cancel', 'OK'], {alias: 'actions'});\n /** Whether the timepicker is currently disabled. */\n readonly disabled: Signal<boolean> = computed(() => !!this._input()?.disabled());\n \n readonly defaultTime: InputSignal<Date | null> = input<Date | null>(null,{alias: 'time'});\n readonly load = signal(false);\n \n readonly isLandscape = computed(() => {\n return this.orientation() === 'landscape';\n });\n \n readonly headline = computed(() => {\n const editable = this.editable();\n const labels = this._headline();\n return editable ? labels[0] : labels[1];\n });\n \n \n protected _panelTemplate = viewChild.required<TemplateRef<unknown>>('panelTemplate');\n private _dialogRef = signal<DialogRef<Date | null> | null>(null);\n \n private _input = signal<RkTimepickerInput | null>(null);\n private _isOpen = signal(false);\n\n currentTime = signal<Date | null>(null);\n animated = signal(false);\n \n /** Emits when the timepicker is opened. */\n readonly opened: OutputEmitterRef<void> = output();\n /** Emits when the timepicker is closed. */\n readonly closed: OutputEmitterRef<void> = output();\n\n readonly selectedTime: OutputEmitterRef<Date> = output();\n \n private _dialog = inject(Dialog);\n\n constructor() {\n effect(() => {\n const value = this.defaultTime();\n if(!value) {\n this.currentTime.set(new Date());\n } else {\n this.currentTime.set(value);\n }\n });\n\n effect(() => {\n const editable = this.editable();\n untracked(() => {\n this.animated.set(true);\n })\n })\n\n afterRender(() => {\n this.load.set(true);\n });\n }\n\n /** Opens the timepicker. */\n open(): void {\n const input = this._input();\n\n if (!input) {\n return;\n }\n\n if (this._isOpen()) {\n return;\n }\n\n this.currentTime.set(input.value());\n this.animated.set(false);\n this._isOpen.set(true);\n this.opened.emit();\n \n const dialogRef = this._dialog.open<Date | null>(this._panelTemplate(), {\n width: '328px',\n });\n\n this._dialogRef.set(dialogRef);\n\n dialogRef.closed.subscribe(result => {\n if(result) {\n this._input()?.value.set(result);\n this.selectedTime.emit(result);\n input.simulateEnter();\n }\n this._isOpen.set(false);\n this.closed.emit();\n this.selected.set('hours');\n input.focus();\n });\n }\n\n /** Registers an input with the timepicker. */\n registerInput(input: RkTimepickerInput): void {\n const currentInput = this._input();\n\n if (currentInput && input !== currentInput ) {\n console.warn('RkTimepicker can only be registered with one input at a time');\n }\n\n this._input.set(input);\n }\n\n private _handleKeydown(event: KeyboardEvent): void {\n const keyCode = event.key;\n if (keyCode === 'Enter') {\n event.preventDefault();\n this.onConfirm();\n } else if (keyCode === 'Escape') {\n event.preventDefault();\n this.onCancel();\n }\n }\n\n\n changeMode() {\n this.editable.set(!this.editable());\n }\n\n \n onConfirm() {\n this._dialogRef()?.close(this.currentTime());\n }\n\n onCancel() {\n this._dialogRef()?.close();\n } \n}\n","import { booleanAttribute, computed, Directive, effect, ElementRef, inject, input, InputSignal, InputSignalWithTransform, model, ModelSignal, Renderer2, Signal, signal } from '@angular/core';\nimport { AbstractControl, ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms';\nimport { MAT_INPUT_VALUE_ACCESSOR } from '@angular/material/input';\nimport { RkTimepicker } from './timepicker';\nimport { secondsToDate, timeToSecondsi18n as timeToSeconds } from './utils';\n\n@Directive({\n selector: 'input[rkTimepicker]',\n exportAs: 'rkTimepickerInput',\n host: {\n 'class': 'rk-timepicker-input',\n 'type': 'text',\n '(input)': '_handleInput($event.target.value)',\n '(blur)': '_handleBlur()',\n '(keydown)': '_handleKeydown($event)',\n '[disabled]': 'disabled()',\n },\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: RkTimepickerInput,\n multi: true,\n },\n {\n provide: NG_VALIDATORS,\n useExisting: RkTimepickerInput,\n multi: true,\n },\n {\n provide: MAT_INPUT_VALUE_ACCESSOR,\n useExisting: RkTimepickerInput,\n },\n ]\n})\nexport class RkTimepickerInput implements ControlValueAccessor, Validator {\n private _elementRef = inject<ElementRef<HTMLInputElement>>(ElementRef);\n private _renderer = inject(Renderer2);\n \n /** Necessary for ControlValueAccessor implementation */\n private _onChange: ((value: any) => void) | undefined;\n private _onTouched: (() => void) | undefined;\n private _accessorDisabled = signal(false);\n private _validatorOnChange: (() => void) | undefined;\n\n /** Current value of the input. */\n readonly value: ModelSignal<Date | null> = model<Date | null>(null);\n private _tempDateSafe = signal<Date | null>(null);\n \n /** Timepicker that the input is associated with. */\n readonly timepicker: InputSignal<RkTimepicker> = input.required<RkTimepicker>({\n alias: 'rkTimepicker',\n });\n /** Whether the input is disabled. */\n readonly disabled: Signal<boolean> = computed(\n () => this.disabledInput() || this._accessorDisabled(),\n );\n /**\n * Whether the input should be disabled through the template.\n */\n readonly disabledInput: InputSignalWithTransform<boolean, unknown> = input(false, {\n transform: booleanAttribute,\n alias: 'disabled',\n });\n \n constructor() {\n this._registerTimepicker();\n }\n\n private _updateModelValue() {\n const value = this.value();\n if(!value) {\n this._validatorOnChange?.();\n return\n }\n this._onChange?.(value);\n }\n \n private _validateTimeOrNull(value: string | Date | null) {\n if (value === null || value instanceof Date) {\n this.value.set(value);\n return;\n }\n const validTime = timeToSeconds(value);\n const time = validTime ? secondsToDate(validTime) : null;\n if(!time) {\n this.value.set(null);\n } else {\n const newDate = new Date(this._tempDateSafe() ?? new Date());\n newDate.setHours(time.getHours(), time.getMinutes());\n this.value.set(newDate);\n }\n }\n \n private _updateInputValue() {\n if (this.value() !== null) {\n this._renderer.setProperty(this._elementRef.nativeElement, 'value', this.value()?.toLocaleTimeString([],{ hour: \"2-digit\", minute: \"2-digit\" }));\n }\n }\n\n /** Handles the `input` event. */\n protected _handleInput(value: string) {\n this._validateTimeOrNull(value);\n this._validatorOnChange?.();\n }\n\n /** Handles the `keydown` event. */\n protected _handleKeydown(event: KeyboardEvent) {\n if (['Enter', 'Escape', 'Tab'].includes(event.code)) {\n this._updateModelValue();\n this._updateInputValue();\n }\n }\n\n simulateEnter() {\n this._handleKeydown({ code: 'Enter' } as KeyboardEvent);\n }\n\n /** Handles the `blur` event. */\n protected _handleBlur() {\n this._onTouched?.();\n this._updateModelValue();\n this._updateInputValue();\n }\n\n /** Focuses the input. */\n focus(): void {\n this._elementRef.nativeElement.focus();\n }\n\n /** Implemented as a part of ControlValueAccessor. */\n writeValue(value: any): void {\n if(value instanceof Date) {\n this._tempDateSafe.set(value);\n this.value.set(value);\n this._updateInputValue();\n } else {\n this._validateTimeOrNull(value);\n this._updateInputValue();\n }\n }\n /** Implemented as a part of ControlValueAccessor. */\n registerOnChange(fn: (value: any) => void): void {\n this._onChange = fn;\n }\n /** Implemented as a part of ControlValueAccessor. */\n registerOnTouched(fn: () => void): void {\n this._onTouched = fn;\n }\n /** Implemented as a part of ControlValueAccessor. */\n setDisabledState(isDisabled: any): void {\n this._accessorDisabled.set(isDisabled);\n }\n\n // implemented as a part of Validator\n validate(control: AbstractControl): ValidationErrors | null {\n if (control.value == null || this.value() == null) {\n return { invalidTimeFormat: true };\n }\n return null;\n }\n\n registerOnValidatorChange(fn: () => void): void {\n this._validatorOnChange = fn;\n }\n\n /** Sets up the logic that registers the input with the timepicker. */\n private _registerTimepicker(): void {\n effect(() => {\n const timepicker = this.timepicker();\n timepicker.registerInput(this);\n timepicker.closed.subscribe(() => this._onTouched?.());\n });\n }\n}\n","import { booleanAttribute, Component, computed, input, InputSignal, InputSignalWithTransform } from '@angular/core';\nimport { MatIconButton } from '@angular/material/button';\nimport { RkTimepicker } from './timepicker';\n\n@Component({\n selector: 'rk-timepicker-toggle',\n imports: [MatIconButton],\n template: `\n <button mat-icon-button type=\"button\" [disabled]=\"_isDisabled()\" (click)=\"_open($event)\">\n <ng-content select=\"[rkTimepickerToggleIcon]\">\n <svg\n class=\"rk-timepicker-toggle-default-icon\"\n height=\"24px\"\n width=\"24px\"\n viewBox=\"0 -960 960 960\"\n fill=\"currentColor\"\n focusable=\"false\"\n aria-hidden=\"true\">\n <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\"/>\n </svg>\n </ng-content>\n </button>\n `,\n styles: ``\n})\nexport class RkTimepickerToggle {\n readonly timepicker: InputSignal<RkTimepicker> = input.required<RkTimepicker>({\n alias: 'for',\n });\n\n /** Whether the toggle button is disabled. */\n readonly disabled: InputSignalWithTransform<boolean, unknown> = input(false, {\n transform: booleanAttribute,\n alias: 'disabled',\n });\n\n protected _isDisabled = computed(() => {\n const timepicker = this.timepicker();\n return this.disabled() || timepicker.disabled();\n });\n\n /** Opens the connected timepicker. */\n protected _open(event: Event): void {\n if (this.timepicker() && !this._isDisabled()) {\n this.timepicker().open();\n event.stopPropagation();\n }\n }\n}","import { Component, ChangeDetectionStrategy, ViewEncapsulation, model, input, computed, effect, signal, afterRender } from '@angular/core';\nimport { NgClass } from '@angular/common';\nimport { MatButton, MatIconButton } from '@angular/material/button';\nimport { MatIcon } from '@angular/material/icon';\nimport { RkTimepickerDial } from './timepicker-dial';\nimport { RkTimepickerInputLabel } from './timepicker-input-label';\n\n@Component({\n selector: 'rk-clock',\n exportAs: 'rkClock',\n imports: [RkTimepickerInputLabel, NgClass, RkTimepickerDial, MatButton, MatIconButton, MatIcon],\n host: {\n 'class': 'rk-timepicker',\n '[class.rk-timepicker-landscape]': 'isLandscape()',\n '[class.rk-timepicker-editable]': 'editable()',\n },\n template: `\n <div class=\"rk-timepicker-container\" [ngClass]=\"{ 'rk-timepicker-container-hide': !load() }\">\n <span class=\"rk-timepicker-headline\">{{ headline() }}</span>\n <rk-timepicker-input-label [(time)]=\"value\" [inputLabels]=\"inputSupportLabels()\" [(editable)]=\"editable\" [(format)]=\"format\" [(period)]=\"period\" [(selected)]=\"selected\"></rk-timepicker-input-label>\n <rk-timepicker-dial [ngClass]=\"{ 'rk-dial-closed': editable()}\" [(time)]=\"value\" [(format)]=\"format\" [(period)]=\"period\" [(selected)]=\"selected\"></rk-timepicker-dial>\n <div class=\"rk-timepicker-footer\">\n <button class=\"rk-timepicker-mode-button\" (click)=\"changeMode()\" mat-icon-button>\n @if (editable()) {\n <mat-icon>schedule</mat-icon>\n } @else {\n <mat-icon>keyboard</mat-icon> \n }\n </button>\n </div>\n </div>\n `,\n styles: ``,\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.None,\n})\nexport class RkClock {\n value= model<Date | null>(null, {alias: 'time'});\n selected = model<string>('hours');\