mirror of
https://github.com/GeorgeSG/lovelace-time-picker-card.git
synced 2026-09-06 02:18:44 +00:00
Add hour formats. Fix and refactor style variables
This commit is contained in:
@@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
TemplateResult,
|
||||||
|
} from 'lit-element';
|
||||||
|
import { ClassInfo, classMap } from 'lit-html/directives/class-map';
|
||||||
|
import { Period } from '../types';
|
||||||
|
|
||||||
|
@customElement('time-period')
|
||||||
|
export class TimePeriodComponent extends LitElement {
|
||||||
|
static readonly EVENT_TOGGLE = 'toggle';
|
||||||
|
|
||||||
|
@property() private period!: Period;
|
||||||
|
|
||||||
|
render(): TemplateResult {
|
||||||
|
return html` <div class="time-period-selector">
|
||||||
|
<div class=${classMap(this.amClass)} @click=${this.onTimePeriodChange}>
|
||||||
|
AM<mwc-ripple></mwc-ripple>
|
||||||
|
</div>
|
||||||
|
<div class=${classMap(this.pmClass)} @click=${this.onTimePeriodChange}>
|
||||||
|
PM<mwc-ripple></mwc-ripple>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
onTimePeriodChange(): void {
|
||||||
|
const event = new CustomEvent(TimePeriodComponent.EVENT_TOGGLE);
|
||||||
|
this.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get amClass(): ClassInfo {
|
||||||
|
return { 'time-period': true, active: this.period === Period.AM };
|
||||||
|
}
|
||||||
|
|
||||||
|
private get pmClass(): ClassInfo {
|
||||||
|
return { 'time-period': true, active: this.period === Period.PM };
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return css`
|
||||||
|
.time-period-selector {
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-period {
|
||||||
|
width: 30px;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--tpc-elements-background-color);
|
||||||
|
color: var(--tpc-text-color, #fff);
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-period.active {
|
||||||
|
background: var(--tpc-accent-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ import { Direction } from '../types';
|
|||||||
|
|
||||||
@customElement('time-unit')
|
@customElement('time-unit')
|
||||||
export class TimeUnitComponent extends LitElement {
|
export class TimeUnitComponent extends LitElement {
|
||||||
|
static readonly EVENT_UPDATE = 'update';
|
||||||
|
|
||||||
@property() private unit!: TimeUnit;
|
@property() private unit!: TimeUnit;
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
@@ -23,7 +25,7 @@ export class TimeUnitComponent extends LitElement {
|
|||||||
type="number"
|
type="number"
|
||||||
placeholder="MM"
|
placeholder="MM"
|
||||||
min="0"
|
min="0"
|
||||||
max="60"
|
max=${this.unit.maxValue}
|
||||||
.value="${this.unit.toString()}"
|
.value="${this.unit.toString()}"
|
||||||
@change=${this.onInputChange}
|
@change=${this.onInputChange}
|
||||||
/>
|
/>
|
||||||
@@ -43,7 +45,7 @@ export class TimeUnitComponent extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private emitUpdate(): void {
|
private emitUpdate(): void {
|
||||||
const event = new CustomEvent('update');
|
const event = new CustomEvent(TimeUnitComponent.EVENT_UPDATE);
|
||||||
this.dispatchEvent(event);
|
this.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,15 +72,16 @@ export class TimeUnitComponent extends LitElement {
|
|||||||
padding: 8px;
|
padding: 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
color: var(--tpc-icon-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-input {
|
.time-input {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
padding: 8px 8px 6px;
|
padding: 8px 8px 6px;
|
||||||
background: var(--time-picker-card-background-color);
|
background: var(--tpc-elements-background-color);
|
||||||
border: 0;
|
border: 0;
|
||||||
border-bottom: 2px solid var(--time-picker-card-background-color);
|
border-bottom: 2px solid var(--tpc-elements-background-color);
|
||||||
color: var(--text-color, #fff);
|
color: var(--tpc-text-color, #fff);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
-moz-appearance: textfield;
|
-moz-appearance: textfield;
|
||||||
|
|||||||
@@ -2,7 +2,3 @@ import * as pkg from '../package.json';
|
|||||||
|
|
||||||
export const CARD_VERSION = pkg.version;
|
export const CARD_VERSION = pkg.version;
|
||||||
export const CARD_SIZE = 3;
|
export const CARD_SIZE = 3;
|
||||||
|
|
||||||
export const STYLE_VARIABLES = {
|
|
||||||
'--time-picker-card-background-color': 'rgb(37, 47, 68)',
|
|
||||||
};
|
|
||||||
|
|||||||
+23
-1
@@ -1,10 +1,32 @@
|
|||||||
import { TimeUnit } from './time-unit';
|
import { TimeUnit } from './time-unit';
|
||||||
|
import { HourMode } from '../types';
|
||||||
|
|
||||||
export class Hour extends TimeUnit {
|
export class Hour extends TimeUnit {
|
||||||
private static readonly DEFAULT_STEP = 1;
|
private static readonly DEFAULT_STEP = 1;
|
||||||
private static readonly MAX = 24;
|
private static readonly MAX = 24;
|
||||||
|
|
||||||
constructor(value: number, step = Hour.DEFAULT_STEP) {
|
constructor(value: number, step = Hour.DEFAULT_STEP, private hourMode: HourMode) {
|
||||||
super(value, step, Hour.MAX);
|
super(value, step, Hour.MAX);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get maxValue(): number {
|
||||||
|
return this.hourMode || Hour.MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePeriod(): void {
|
||||||
|
this.setValue(this.value + 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
const value = this.hourMode === 12 ? (this.value + 12) % 12 : this.value;
|
||||||
|
|
||||||
|
return value < 10 ? `0${value}` : value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected isValidString(valueStr: string): boolean {
|
||||||
|
const value = parseInt(valueStr);
|
||||||
|
const limit = this.hourMode || this._limit;
|
||||||
|
|
||||||
|
return !isNaN(value) && value >= 0 && value <= limit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,16 @@ import { TimeUnit } from './time-unit';
|
|||||||
|
|
||||||
export class Minute extends TimeUnit {
|
export class Minute extends TimeUnit {
|
||||||
private static readonly DEFAULT_STEP = 5;
|
private static readonly DEFAULT_STEP = 5;
|
||||||
private static readonly MAX = 50;
|
private static readonly MAX = 60;
|
||||||
|
|
||||||
|
maxValue = Minute.MAX;
|
||||||
|
|
||||||
constructor(value: number, step = Minute.DEFAULT_STEP) {
|
constructor(value: number, step = Minute.DEFAULT_STEP) {
|
||||||
super(value, step, Minute.MAX);
|
super(value, step, Minute.MAX);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected isValidString(valueStr: string): boolean {
|
||||||
|
const value = parseInt(valueStr);
|
||||||
|
return !isNaN(value) && value >= 0 && value <= this._limit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-7
@@ -1,7 +1,23 @@
|
|||||||
import { Direction } from '../types';
|
import { Direction } from '../types';
|
||||||
|
|
||||||
export abstract class TimeUnit {
|
export abstract class TimeUnit {
|
||||||
constructor(private _value: number, private _step: number, private _limit: number) {}
|
/**
|
||||||
|
* Return true if the valueStr can be set as a value of this instance.
|
||||||
|
*/
|
||||||
|
protected abstract isValidString(valueStr: string): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The max allowed value for this instance. Used for UI validation.
|
||||||
|
*/
|
||||||
|
abstract maxValue: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance of a TimeUnit
|
||||||
|
* @param _value current value
|
||||||
|
* @param _step how much to increase / decrease the value when step-changing
|
||||||
|
* @param _limit value upper limit
|
||||||
|
*/
|
||||||
|
constructor(private _value: number, protected _step: number, protected _limit: number) {}
|
||||||
|
|
||||||
get value(): number {
|
get value(): number {
|
||||||
return this._value;
|
return this._value;
|
||||||
@@ -29,12 +45,7 @@ export abstract class TimeUnit {
|
|||||||
return this.value < 10 ? `0${this.value}` : this.value.toString();
|
return this.value < 10 ? `0${this.value}` : this.value.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private isValidString(valueStr: string): boolean {
|
protected setValue(newValue: number): void {
|
||||||
const value = parseInt(valueStr);
|
|
||||||
return !isNaN(value) && value >= 0 && value <= this._limit;
|
|
||||||
}
|
|
||||||
|
|
||||||
private setValue(newValue: number): void {
|
|
||||||
if (newValue >= this._limit || newValue < 0) {
|
if (newValue >= this._limit || newValue < 0) {
|
||||||
newValue = (newValue + this._limit) % this._limit;
|
newValue = (newValue + this._limit) % this._limit;
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-14
@@ -9,12 +9,13 @@ import {
|
|||||||
property,
|
property,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from 'lit-element';
|
} from 'lit-element';
|
||||||
|
import './components/time-period.component';
|
||||||
import './components/time-unit.component';
|
import './components/time-unit.component';
|
||||||
import { CARD_SIZE, CARD_VERSION, STYLE_VARIABLES } from './const';
|
import { CARD_SIZE, CARD_VERSION } from './const';
|
||||||
import { Hour } from './models/hour';
|
import { Hour } from './models/hour';
|
||||||
import { Minute } from './models/minute';
|
import { Minute } from './models/minute';
|
||||||
import { Partial } from './partials';
|
import { Partial } from './partials';
|
||||||
import { TimePickerCardConfig } from './types';
|
import { Period, TimePickerCardConfig } from './types';
|
||||||
|
|
||||||
console.info(
|
console.info(
|
||||||
`%c TIME-PICKER-CARD \n%c Version ${CARD_VERSION} `,
|
`%c TIME-PICKER-CARD \n%c Version ${CARD_VERSION} `,
|
||||||
@@ -28,14 +29,7 @@ export class TimePickerCard extends LitElement {
|
|||||||
@property() private config!: TimePickerCardConfig;
|
@property() private config!: TimePickerCardConfig;
|
||||||
@property() private hour!: Hour;
|
@property() private hour!: Hour;
|
||||||
@property() private minute!: Minute;
|
@property() private minute!: Minute;
|
||||||
|
@property() private period!: Period;
|
||||||
connectedCallback(): void {
|
|
||||||
super.connectedCallback();
|
|
||||||
|
|
||||||
Object.entries(STYLE_VARIABLES).forEach(([variable, value]) =>
|
|
||||||
this.style.setProperty(variable, value)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private get entity(): HassEntity | undefined {
|
private get entity(): HassEntity | undefined {
|
||||||
return this.hass.states[this.config.entity];
|
return this.hass.states[this.config.entity];
|
||||||
@@ -49,6 +43,10 @@ export class TimePickerCard extends LitElement {
|
|||||||
return this.config.name || this.entity?.attributes.friendly_name;
|
return this.config.name || this.entity?.attributes.friendly_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get shouldShowPeriod(): boolean {
|
||||||
|
return this.config.hour_mode === 12;
|
||||||
|
}
|
||||||
|
|
||||||
render(): TemplateResult | null {
|
render(): TemplateResult | null {
|
||||||
if (!this.entity) {
|
if (!this.entity) {
|
||||||
return Partial.error('Entity not found', this.config);
|
return Partial.error('Entity not found', this.config);
|
||||||
@@ -66,16 +64,24 @@ export class TimePickerCard extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { hour, minute } = this.entity!.attributes;
|
const { hour, minute } = this.entity!.attributes;
|
||||||
this.hour = new Hour(hour, this.config.hour_step);
|
this.hour = new Hour(hour, this.config.hour_step, this.config.hour_mode);
|
||||||
this.minute = new Minute(minute, this.config.minute_step);
|
this.minute = new Minute(minute, this.config.minute_step);
|
||||||
|
this.period = this.hour.value >= 12 ? Period.PM : Period.AM;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-card class="time-picker-ha-card">
|
<ha-card>
|
||||||
${this.shouldShowName ? Partial.header(this.name!) : ''}
|
${this.shouldShowName ? Partial.header(this.name!) : ''}
|
||||||
<div class="time-picker-content">
|
<div class="time-picker-content">
|
||||||
<time-unit .unit=${this.hour} @update=${this.callHassService}></time-unit>
|
<time-unit .unit=${this.hour} @update=${this.callHassService}></time-unit>
|
||||||
<div class="time-separator">:</div>
|
<div class="time-separator">:</div>
|
||||||
<time-unit .unit=${this.minute} @update=${this.callHassService}></time-unit>
|
<time-unit .unit=${this.minute} @update=${this.callHassService}></time-unit>
|
||||||
|
|
||||||
|
${this.shouldShowPeriod
|
||||||
|
? html`<time-period
|
||||||
|
.period=${this.period}
|
||||||
|
@toggle=${this.onPeriodToggle}
|
||||||
|
></time-period>`
|
||||||
|
: ''}
|
||||||
</div>
|
</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
@@ -90,6 +96,10 @@ export class TimePickerCard extends LitElement {
|
|||||||
throw new Error('You must set an entity');
|
throw new Error('You must set an entity');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.hour_mode && config.hour_mode !== 12 && config.hour_mode !== 24) {
|
||||||
|
throw new Error('Invalid hour_mode: select either 12 or 24');
|
||||||
|
}
|
||||||
|
|
||||||
this.config = config;
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +107,11 @@ export class TimePickerCard extends LitElement {
|
|||||||
return CARD_SIZE;
|
return CARD_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onPeriodToggle(): void {
|
||||||
|
this.hour.togglePeriod();
|
||||||
|
this.callHassService();
|
||||||
|
}
|
||||||
|
|
||||||
private callHassService(): Promise<void> {
|
private callHassService(): Promise<void> {
|
||||||
if (!this.hass) {
|
if (!this.hass) {
|
||||||
throw new Error('Unable to update datetime');
|
throw new Error('Unable to update datetime');
|
||||||
@@ -112,12 +127,21 @@ export class TimePickerCard extends LitElement {
|
|||||||
|
|
||||||
static get styles(): CSSResult {
|
static get styles(): CSSResult {
|
||||||
return css`
|
return css`
|
||||||
.time-picker-ha-card {
|
:host {
|
||||||
|
--tpc-elements-background-color: var(
|
||||||
|
--time-picker-elements-background-color,
|
||||||
|
var(--dark-primary-color)
|
||||||
|
);
|
||||||
|
|
||||||
|
--tpc-icon-color: var(--time-picker-icon-color, var(--primary-text-color));
|
||||||
|
--tpc-text-color: var(--time-picker-text-color, #fff);
|
||||||
|
--tpc-accent-color: var(--time-picker-accent-color, var(--accent-color));
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-picker-header {
|
.time-picker-header {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background-color: var(--time-picker-card-background-color);
|
color: var(--tpc-text-color, #fff);
|
||||||
|
background-color: var(--tpc-elements-background-color);
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { LovelaceCardConfig } from 'custom-card-helpers';
|
|||||||
export interface TimePickerCardConfig extends LovelaceCardConfig {
|
export interface TimePickerCardConfig extends LovelaceCardConfig {
|
||||||
entity: string;
|
entity: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
hour_mode?: HourMode;
|
||||||
hour_step?: number;
|
hour_step?: number;
|
||||||
minute_step?: number;
|
minute_step?: number;
|
||||||
hide?: TimePickerHideConfig;
|
hide?: TimePickerHideConfig;
|
||||||
@@ -16,3 +17,10 @@ export enum Direction {
|
|||||||
UP = 'up',
|
UP = 'up',
|
||||||
DOWN = 'down',
|
DOWN = 'down',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum Period {
|
||||||
|
AM = 'am',
|
||||||
|
PM = 'pm',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HourMode = 12 | 24 | undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user