Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8d87e9afb | ||
|
|
e56dc41dcc | ||
|
|
4b5fc79630 | ||
|
|
6113ec9415 | ||
|
|
f306d95748 | ||
|
|
fe91b79320 | ||
|
|
0b1b87ed6e | ||
|
|
81a0f1a95a | ||
|
|
526a52f630 | ||
|
|
72d82d7c0a | ||
|
|
21f66a9066 | ||
|
|
fe93d385e9 | ||
|
|
05e0952624 | ||
|
|
3c10a9ab44 | ||
|
|
9a2a57af5c | ||
|
|
74949a3df2 | ||
|
|
224ee3b043 | ||
|
|
2e604a66d6 | ||
|
|
b7ad71d979 | ||
|
|
79418592c9 | ||
|
|
0297c9aba4 | ||
|
|
588cead35d | ||
|
|
04462e096c | ||
|
|
03312aa5f7 | ||
|
|
4488b303d2 | ||
|
|
491e571aab | ||
|
|
ae7dfb7668 | ||
|
|
cc22b482b4 | ||
|
|
812e7f3016 | ||
|
|
20cba88ef2 | ||
|
|
5b6c9d0af0 | ||
|
|
993fda7d34 | ||
|
|
df9de8bd00 | ||
|
|
a7b98e31a4 | ||
|
|
a92bb7915c | ||
|
|
6258b55518 | ||
|
|
b4663377a4 | ||
|
|
1dc6152986 | ||
|
|
8e11f71b48 | ||
|
|
c646fc9c9e | ||
|
|
2791d8109c | ||
|
|
14cc1a2c10 | ||
|
|
76a52f0c9f | ||
|
|
ce22ceb808 | ||
|
|
45d3364959 | ||
|
|
6ac3d62823 | ||
|
|
f86aada0e3 | ||
|
|
e806a915a5 |
@@ -13,7 +13,8 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/camelcase": 0,
|
"@typescript-eslint/camelcase": 0,
|
||||||
"@typescript-eslint/no-non-null-assertion": 0,
|
"@typescript-eslint/no-non-null-assertion": 0,
|
||||||
"@typescript-eslint/explicit-function-return-type": [1, { "allowExpressions": true }]
|
"@typescript-eslint/explicit-function-return-type": [1, { "allowExpressions": true }],
|
||||||
|
"@typescript-eslint/no-namespace": 0
|
||||||
},
|
},
|
||||||
"ignorePatterns": ["dist/", "node_modules/"]
|
"ignorePatterns": ["dist/", "node_modules/", "*.js"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ on:
|
|||||||
- master
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
npm install
|
npm install
|
||||||
npm run lint
|
|
||||||
npm run build
|
npm run build
|
||||||
|
- name: Test
|
||||||
|
run: npm run test
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"files.exclude": {
|
||||||
|
"dist/**": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +1,33 @@
|
|||||||
# Time Picker Card by [@georgesg](https://www.github.com/georgesg)
|
# Time Picker Card
|
||||||
|
|
||||||
[![hacs_badge][hacs-shield]][hacs]
|
[![HACS][hacs-shield]][hacs-link]
|
||||||
[![GitHub Release][releases-shield]][releases]
|
[![Downloads][downloads-shield]][downloads-link]
|
||||||

|
[![GitHub Release][releases-shield]][releases-link]
|
||||||
[![Project Maintenance][maintenance-shield]][maintenance]
|
[![CI][ci-shield]][ci-link]
|
||||||
[![GitHub Activity][commits-shield]][commits]
|
[![Project Maintenance][maintenance-shield]][maintenance-link]
|
||||||
[![License][license-shield]][license]
|
[![License][license-shield]][license-link]
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This is a Time Picker card for [Home Assistant](https://www.home-assistant.io/)'s [Lovelace UI](https://www.home-assistant.io/lovelace).
|
This is a Time Picker Card for [Home Assistant](https://www.home-assistant.io/)'s [Lovelace UI](https://www.home-assistant.io/lovelace).
|
||||||
|
|
||||||
Requires an [Input Datetime](https://www.home-assistant.io/integrations/input_datetime/) that has time (`has_time: true`).
|
Requires an [Input Datetime](https://www.home-assistant.io/integrations/input_datetime/) that has time (`has_time: true`).
|
||||||
|
|
||||||
## Screenshots
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Install using [HACS](https://hacs.xyz) or follow this [guide](https://github.com/thomasloven/hass-config/wiki/Lovelace-Plugins)
|
### HACS
|
||||||
|
|
||||||
|
Install using [HACS](https://hacs.xyz) and add the following to your config:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
resources:
|
||||||
|
- url: /hacsfiles/lovelace-time-picker-card/time-picker-card.js
|
||||||
|
type: module
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual
|
||||||
|
|
||||||
|
Download time-picker-card.js from the [latest realease](https://github.com/GeorgeSG/lovelace-time-picker-card/releases/latest) and place it in your `config/www` folder. Add the following to your config:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
resources:
|
resources:
|
||||||
@@ -33,17 +37,65 @@ resources:
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
### Visual Editor
|
||||||
|
|
||||||
|
Time Picker Card supports Lovelace's Visual Editor. Click the + button to add a card and search for time picker.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Default config - card name shown, 24 hour mode
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
type: 'custom:time-picker-card'
|
type: 'custom:time-picker-card'
|
||||||
entity: input_datetime.alarm_time
|
entity: input_datetime.alarm_time
|
||||||
name: 'Choose a time'
|
|
||||||
hour_mode: 24
|
|
||||||
hour_step: 1
|
|
||||||
minute_step: 5
|
|
||||||
hide:
|
|
||||||
name: false
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Custom config - hidden card name, 12 hour mode
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: 'custom:time-picker-card'
|
||||||
|
entity: input_datetime.alarm_time
|
||||||
|
hour_mode: 12
|
||||||
|
hide:
|
||||||
|
name: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom config - hidden card name, 12 hour mode with a "single" hour mode picker
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: 'custom:time-picker-card'
|
||||||
|
entity: input_datetime.alarm_time
|
||||||
|
hour_mode: 12
|
||||||
|
layout:
|
||||||
|
hour_mode: single
|
||||||
|
hide:
|
||||||
|
name: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom config - card name inside card and controls aligned right
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: 'custom:time-picker-card'
|
||||||
|
entity: input_datetime.alarm_time
|
||||||
|
layout:
|
||||||
|
name: inside
|
||||||
|
align_controls: right
|
||||||
|
```
|
||||||
|
|
||||||
|
### With a custom lovelace theme
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
| Name | Type | Requirement | Description | Default |
|
| Name | Type | Requirement | Description | Default |
|
||||||
@@ -51,43 +103,64 @@ hide:
|
|||||||
| type | string | **Required** | `custom:time-picker-card` | |
|
| type | string | **Required** | `custom:time-picker-card` | |
|
||||||
| entity | string | **Required** | [Input Datetime](https://www.home-assistant.io/integrations/input_datetime/) entity with `has_time: true` | |
|
| entity | string | **Required** | [Input Datetime](https://www.home-assistant.io/integrations/input_datetime/) entity with `has_time: true` | |
|
||||||
| name | string | **Optional** | Card name | Entity's `friendly_name` |
|
| name | string | **Optional** | Card name | Entity's `friendly_name` |
|
||||||
|
| link_values | boolean | **Optional** | If enabled, will change hour when minutes overflow. E.g. will go from 11:55 to 12:00, instead of 11:00 | `false` |
|
||||||
| hour_mode | `12` or `24` | **Optional** | Hour format. If `12`, card will show AM/PM picker | `24` |
|
| hour_mode | `12` or `24` | **Optional** | Hour format. If `12`, card will show AM/PM picker | `24` |
|
||||||
| hour_step | number | **Optional** | Hour change when clicking arrows | `1` |
|
| hour_step | number | **Optional** | Hour change when clicking arrows | `1` |
|
||||||
| minute_step | number | **Optional** | Minute change when clicking arrows | `5` |
|
| minute_step | number | **Optional** | Minute change when clicking arrows | `5` |
|
||||||
|
| layout | object | **Optional** | Card Layout configuration | `none` |
|
||||||
| hide | object | **Optional** | Hide object | `none` |
|
| hide | object | **Optional** | Hide object | `none` |
|
||||||
|
|
||||||
## Hide Object
|
### Layout Object
|
||||||
|
|
||||||
| Name | Type | Requirement | Description | Default |
|
| Name | Value | Requirement | Description | Default |
|
||||||
| ---- | ------- | ------------ | ------------------- | ------- |
|
| -------------- | ------------------------- | ------------ | -------------------------------------------------------------------------------------------------- | -------- |
|
||||||
| name | boolean | **Optional** | Hides the card name | `false` |
|
| hour_mode | `single`, `double` | **Optional** | Whether to show both AM/PM or just the current mode. In `single` mode, tap the value to change it. | `double` |
|
||||||
|
| align_controls | `left`, `center`, `right` | **Optional** | Horizontal alignment of the controls | `center` |
|
||||||
|
| name | `header`, `inside` | **Optional** | Whether to show the name as a header or inside the card | `header` |
|
||||||
|
| embedded | boolean | **Optional** | Render with embedded style - disables padding, box shadow, and card header | `false` |
|
||||||
|
|
||||||
## Theme Variables
|
### Hide Object
|
||||||
|
|
||||||
| Name | Default | Description |
|
| Name | Type | Requirement | Description | Default |
|
||||||
| ------------------------------- | --------------------------- | ------------------------------------------- |
|
| ---- | ------- | ------------ | ------------------- | ------- |
|
||||||
| --tpc-elements-background-color | `var(--dark-primary-color)` | Background colro for header and ui elements |
|
| name | boolean | **Optional** | Hides the card name | `false` |
|
||||||
| --tpc-icon-color | `var(--primary-text-color)` | Arrow color |
|
| seconds | boolean | **Optional** | Hides seconds input | `true` |
|
||||||
| --tpc-text-color | `var(--primary-text-color)` | Text color |
|
|
||||||
| --tpc-accent-color | `var(--accent-color)` | AM / PM active color |
|
### Theme Variables
|
||||||
|
|
||||||
|
Time Picker Card will automatically pick up colors from your lovelace theme, but if you want to customize some of them,
|
||||||
|
you can use the following variables in your theme's config file:
|
||||||
|
|
||||||
|
| Name | Default | Description |
|
||||||
|
| ------------------------------------- | ------------------------------ | -------------------------------------- |
|
||||||
|
| time-picker-elements-background-color | `var(--primary-color)` | Background color for header and inputs |
|
||||||
|
| time-picker-icon-color | `var(--primary-text-color)` | Arrow color |
|
||||||
|
| time-picker-text-color | `white` | Text color |
|
||||||
|
| time-picker-accent-color | `var(--primary-color)` | AM / PM active color |
|
||||||
|
| time-picker-off-color | `var(--disabled-text-color)` | AM / PM inactive color |
|
||||||
|
| time-picker-border-radius | `var(--ha-card-border-radius)` | Border radius of the card |
|
||||||
|
|
||||||
## Meta
|
## Meta
|
||||||
|
|
||||||
**Georgi Gardev**
|
**Georgi Gardev**
|
||||||
|
|
||||||
- [gar.dev](https://gar.dev)
|
- [gar.dev](https://gar.dev)
|
||||||
- [![GitHub][github-icon]](https://github.com/GeorgeSG/) [GeorgeSG](https://github.com/GeorgeSG/)
|
- [![GitHub][github-icon]][github-link] [GeorgeSG][github-link]
|
||||||
- [![Twitter][twitter-icon]](https://twitter.com/georgesg92) [@georgesg92](https://twitter.com/georgesg92)
|
- [![Twitter][twitter-icon]][twitter-link] [@georgesg92][twitter-link]
|
||||||
|
|
||||||
[commits-shield]: https://img.shields.io/github/commit-activity/y/GeorgeSG/lovelace-time-picker-card?style=flat-square
|
[hacs-shield]: https://img.shields.io/badge/HACS-Default-brightgreen.svg
|
||||||
[commits]: https://github.com/GeorgeSG/lovelace-time-picker-card/commits/master
|
[hacs-link]: https://github.com/custom-components/hacs
|
||||||
[license-shield]: https://img.shields.io/github/license/GeorgeSG/lovelace-time-picker-card?style=flat-square
|
[downloads-shield]: https://img.shields.io/github/downloads/GeorgeSG/lovelace-time-picker-card/latest/total?color=brightgreen&logo=github
|
||||||
[license]: https://github.com/GeorgeSG/lovelace-time-picker-card/blob/master/LICENSE
|
[downloads-link]: https://github.com/GeorgeSG/lovelace-time-picker-card/releases
|
||||||
[maintenance-shield]: https://img.shields.io/maintenance/yes/2020.svg?style=flat-square
|
[releases-shield]: https://img.shields.io/github/release/GeorgeSG/lovelace-time-picker-card.svg
|
||||||
[maintenance]: https://github.com/GeorgeSG/lovelace-time-picker-card
|
[releases-link]: https://github.com/GeorgeSG/lovelace-time-picker-card/releases
|
||||||
[releases-shield]: https://img.shields.io/github/release/GeorgeSG/lovelace-time-picker-card.svg?style=flat-square
|
[ci-shield]: https://img.shields.io/github/workflow/status/GeorgeSG/lovelace-time-picker-card/CI?label=CI&logo=github&
|
||||||
[releases]: https://github.com/GeorgeSG/lovelace-time-picker-card/releases
|
[ci-link]: https://github.com/GeorgeSG/lovelace-time-picker-card/actions?query=workflow%3ACI
|
||||||
[hacs-shield]: https://img.shields.io/badge/HACS-Custom-orange.svg?style=flat-square
|
[maintenance-shield]: https://img.shields.io/maintenance/yes/2020.svg
|
||||||
[hacs]: https://github.com/custom-components/hacs
|
[maintenance-link]: https://github.com/GeorgeSG/lovelace-time-picker-card
|
||||||
[twitter-icon]: http://i.imgur.com/wWzX9uB.png
|
[license-shield]: https://img.shields.io/github/license/GeorgeSG/lovelace-time-picker-card?color=brightgreen
|
||||||
|
[license-link]: https://github.com/GeorgeSG/lovelace-time-picker-card/blob/master/LICENSE
|
||||||
[github-icon]: http://i.imgur.com/9I6NRUm.png
|
[github-icon]: http://i.imgur.com/9I6NRUm.png
|
||||||
|
[github-link]: https://github.com/GeorgeSG/
|
||||||
|
[twitter-icon]: http://i.imgur.com/wWzX9uB.png
|
||||||
|
[twitter-link]: https://twitter.com/georgesg92
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 45 KiB |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "time-picker-card",
|
"name": "time-picker-card",
|
||||||
"version": "0.0.1",
|
"version": "1.1.0",
|
||||||
"author": "Georgi Gardev <georgi@gardev.com>",
|
"author": "Georgi Gardev <georgi@gardev.com>",
|
||||||
"description": "Lovelace time-picker-card",
|
"description": "Lovelace time-picker-card",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -19,31 +19,42 @@
|
|||||||
"build": "npm run lint && npm run rollup",
|
"build": "npm run lint && npm run rollup",
|
||||||
"lint": "eslint src/*.ts",
|
"lint": "eslint src/*.ts",
|
||||||
"rollup": "rollup -c",
|
"rollup": "rollup -c",
|
||||||
|
"test": "./node_modules/karma/bin/karma start ./test/karma.conf.js",
|
||||||
"release-patch": "npm version patch && git push origin master --tags",
|
"release-patch": "npm version patch && git push origin master --tags",
|
||||||
"release-minor": "npm version minor && git push origin master --tags",
|
"release-minor": "npm version minor && git push origin master --tags",
|
||||||
"release-major": "npm version major && git push origin master --tags"
|
"release-major": "npm version major && git push origin master --tags"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"custom-card-helpers": "^1.5.0",
|
"custom-card-helpers": "^1.6.6",
|
||||||
"home-assistant-js-websocket": "^5.1.0",
|
"home-assistant-js-websocket": "^5.7.0",
|
||||||
"lit-element": "^2.3.1",
|
"lit-element": "^2.4.0",
|
||||||
"lit-html": "^1.2.1"
|
"lit-html": "^1.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.9.0",
|
"@babel/core": "^7.12.7",
|
||||||
"@rollup/plugin-json": "^4.0.3",
|
"@open-wc/testing": "^2.5.32",
|
||||||
"@rollup/plugin-node-resolve": "^7.1.1",
|
"@rollup/plugin-commonjs": "^11.1.0",
|
||||||
"@rollup/plugin-typescript": "^4.0.0",
|
"@rollup/plugin-json": "^4.1.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.30.0",
|
"@rollup/plugin-node-resolve": "^7.1.3",
|
||||||
"@typescript-eslint/parser": "^2.30.0",
|
"@rollup/plugin-typescript": "^4.1.2",
|
||||||
|
"@types/chai": "^4.2.14",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^2.34.0",
|
||||||
|
"@typescript-eslint/parser": "^2.34.0",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0",
|
||||||
"eslint-config-prettier": "^6.11.0",
|
"eslint-config-prettier": "^6.15.0",
|
||||||
"eslint-plugin-prettier": "^3.1.3",
|
"eslint-plugin-prettier": "^3.1.4",
|
||||||
"prettier": "^2.0.5",
|
"karma": "^5.2.3",
|
||||||
|
"karma-chai": "^0.1.0",
|
||||||
|
"karma-chrome-launcher": "^3.1.0",
|
||||||
|
"karma-mocha": "^2.0.1",
|
||||||
|
"karma-rollup-preprocessor": "^7.0.5",
|
||||||
|
"mocha": "^7.2.0",
|
||||||
|
"prettier": "^2.2.0",
|
||||||
"rollup": "^1.32.1",
|
"rollup": "^1.32.1",
|
||||||
"rollup-plugin-babel": "^4.4.0",
|
"rollup-plugin-babel": "^4.4.0",
|
||||||
"rollup-plugin-serve": "^1.0.1",
|
"rollup-plugin-serve": "^1.1.0",
|
||||||
"rollup-plugin-terser": "^5.3.0",
|
"rollup-plugin-terser": "^5.3.1",
|
||||||
"typescript": "^3.8.3"
|
"sinon": "^9.2.1",
|
||||||
|
"typescript": "^3.9.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,22 +8,30 @@ import {
|
|||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from 'lit-element';
|
} from 'lit-element';
|
||||||
import { ClassInfo, classMap } from 'lit-html/directives/class-map';
|
import { ClassInfo, classMap } from 'lit-html/directives/class-map';
|
||||||
import { Period } from '../types';
|
import { Layout, Period } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a Time Period selector with an input for a value and two arrows for step-chaning
|
||||||
|
* the value up and down.
|
||||||
|
*/
|
||||||
@customElement('time-period')
|
@customElement('time-period')
|
||||||
export class TimePeriodComponent extends LitElement {
|
export class TimePeriodComponent extends LitElement {
|
||||||
static readonly EVENT_TOGGLE = 'toggle';
|
static readonly EVENT_TOGGLE = 'toggle';
|
||||||
|
|
||||||
@property() private period!: Period;
|
@property() private period!: Period;
|
||||||
|
@property() private mode!: Layout.HourMode;
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html` <div class="time-period-selector">
|
return html`<div class="time-period-selector">
|
||||||
<div class=${classMap(this.amClass)} @click=${this.onTimePeriodChange}>
|
${this.mode === 'single' ? this.renderSingle() : this.renderDouble()}
|
||||||
AM<mwc-ripple></mwc-ripple>
|
|
||||||
</div>
|
|
||||||
<div class=${classMap(this.pmClass)} @click=${this.onTimePeriodChange}>
|
|
||||||
PM<mwc-ripple></mwc-ripple>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,12 +40,19 @@ export class TimePeriodComponent extends LitElement {
|
|||||||
this.dispatchEvent(event);
|
this.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get amClass(): ClassInfo {
|
private renderSingle(): TemplateResult {
|
||||||
return { 'time-period': true, active: this.period === Period.AM };
|
return html`<div class="time-period active" @click=${this.onTimePeriodChange}>
|
||||||
|
${this.period}<mwc-ripple></mwc-ripple>
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get pmClass(): ClassInfo {
|
private renderDouble(): TemplateResult {
|
||||||
return { 'time-period': true, active: this.period === Period.PM };
|
return html`<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>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResult {
|
static get styles(): CSSResult {
|
||||||
@@ -49,8 +64,8 @@ export class TimePeriodComponent extends LitElement {
|
|||||||
.time-period {
|
.time-period {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
background: var(--tpc-elements-background-color);
|
background: var(--tpc-off-color);
|
||||||
color: var(--tpc-text-color, #fff);
|
color: var(--tpc-text-color);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -10,9 +10,13 @@ import {
|
|||||||
import { TimeUnit } from '../models/time-unit';
|
import { TimeUnit } from '../models/time-unit';
|
||||||
import { Direction } from '../types';
|
import { Direction } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders an AM/PM picker.
|
||||||
|
*/
|
||||||
@customElement('time-unit')
|
@customElement('time-unit')
|
||||||
export class TimeUnitComponent extends LitElement {
|
export class TimeUnitComponent extends LitElement {
|
||||||
static readonly EVENT_UPDATE = 'update';
|
static readonly EVENT_UPDATE = 'update';
|
||||||
|
static readonly EVENT_STEP_CHANGE = 'stepChange';
|
||||||
|
|
||||||
@property() private unit!: TimeUnit;
|
@property() private unit!: TimeUnit;
|
||||||
|
|
||||||
@@ -24,7 +28,7 @@ export class TimeUnitComponent extends LitElement {
|
|||||||
class="time-input"
|
class="time-input"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="MM"
|
placeholder="MM"
|
||||||
min="0"
|
min=${this.unit.minValue}
|
||||||
max=${this.unit.maxValue}
|
max=${this.unit.maxValue}
|
||||||
.value="${this.unit.toString()}"
|
.value="${this.unit.toString()}"
|
||||||
@change=${this.onInputChange}
|
@change=${this.onInputChange}
|
||||||
@@ -36,23 +40,19 @@ export class TimeUnitComponent extends LitElement {
|
|||||||
|
|
||||||
onInputChange({ target: { value } }: { target: HTMLInputElement }): void {
|
onInputChange({ target: { value } }: { target: HTMLInputElement }): void {
|
||||||
this.unit.setStringValue(value);
|
this.unit.setStringValue(value);
|
||||||
this.emitUpdate();
|
const event = new CustomEvent(TimeUnitComponent.EVENT_UPDATE);
|
||||||
|
this.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
onStepChangerClick(direction: Direction): void {
|
onStepChangerClick(direction: Direction): void {
|
||||||
this.unit.stepUpdate(direction);
|
const event = new CustomEvent(TimeUnitComponent.EVENT_STEP_CHANGE, { detail: { direction } });
|
||||||
this.emitUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
private emitUpdate(): void {
|
|
||||||
const event = new CustomEvent(TimeUnitComponent.EVENT_UPDATE);
|
|
||||||
this.dispatchEvent(event);
|
this.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderStepChanger(direction: Direction): TemplateResult {
|
private renderStepChanger(direction: Direction): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="time-picker-icon" @click=${() => this.onStepChangerClick(direction)}>
|
<div class="time-picker-icon" @click=${() => this.onStepChangerClick(direction)}>
|
||||||
<ha-icon .icon="mdi:arrow-${direction}"></ha-icon>
|
<ha-icon .icon="hass:chevron-${direction}"></ha-icon>
|
||||||
<mwc-ripple id="ripple"></mwc-ripple>
|
<mwc-ripple id="ripple"></mwc-ripple>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,4 +1,15 @@
|
|||||||
import * as pkg from '../package.json';
|
import * as pkg from '../package.json';
|
||||||
|
import { Layout } from './types';
|
||||||
|
|
||||||
export const CARD_VERSION = pkg.version;
|
export const CARD_VERSION = pkg.version;
|
||||||
export const CARD_SIZE = 3;
|
export const CARD_SIZE = 3;
|
||||||
|
|
||||||
|
export const ENTITY_DOMAIN = 'input_datetime';
|
||||||
|
|
||||||
|
// Config defaults
|
||||||
|
export const DEFAULT_HOUR_STEP = 1;
|
||||||
|
export const DEFAULT_MINUTE_STEP = 5;
|
||||||
|
export const DEFAULT_SECOND_STEP = 5;
|
||||||
|
export const DEFAULT_LAYOUT_HOUR_MODE = 'double';
|
||||||
|
export const DEFAULT_LAYOUT_ALIGN_CONTROLS = Layout.AlignControls.CENTER;
|
||||||
|
export const DEFAULT_LAYOUT_NAME = Layout.Name.HEADER;
|
||||||
|
|||||||
@@ -0,0 +1,282 @@
|
|||||||
|
import { computeDomain, HomeAssistant, LovelaceCardEditor } from 'custom-card-helpers';
|
||||||
|
import { HassEntity } from 'home-assistant-js-websocket';
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
TemplateResult,
|
||||||
|
} from 'lit-element';
|
||||||
|
import {
|
||||||
|
DEFAULT_HOUR_STEP,
|
||||||
|
DEFAULT_MINUTE_STEP,
|
||||||
|
ENTITY_DOMAIN,
|
||||||
|
DEFAULT_LAYOUT_ALIGN_CONTROLS,
|
||||||
|
DEFAULT_LAYOUT_NAME,
|
||||||
|
DEFAULT_SECOND_STEP,
|
||||||
|
} from './const';
|
||||||
|
import { TimePickerCardConfig, Layout, HourMode } from './types';
|
||||||
|
|
||||||
|
@customElement('time-picker-card-editor')
|
||||||
|
export class TimePickerCardEditor extends LitElement implements LovelaceCardEditor {
|
||||||
|
private static readonly NUMBER_PROPERTIES = [
|
||||||
|
'hour_step',
|
||||||
|
'minute_step',
|
||||||
|
'second_step',
|
||||||
|
'hour_mode',
|
||||||
|
];
|
||||||
|
private static readonly CONFIG_CHANGED_EVENT = 'config-changed';
|
||||||
|
|
||||||
|
@property({ type: Object }) hass!: HomeAssistant;
|
||||||
|
@property() private config!: TimePickerCardConfig;
|
||||||
|
|
||||||
|
private get entity(): HassEntity | undefined {
|
||||||
|
return this.hass.states[this.config.entity];
|
||||||
|
}
|
||||||
|
|
||||||
|
private get datetimeEntities(): Array<string> {
|
||||||
|
return Object.keys(this.hass.states).filter(
|
||||||
|
(entityId) =>
|
||||||
|
computeDomain(entityId) === ENTITY_DOMAIN &&
|
||||||
|
this.hass.states[entityId].attributes.has_time === true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): TemplateResult {
|
||||||
|
return html`<div class="card-config">
|
||||||
|
<paper-dropdown-menu
|
||||||
|
style="width: 100%"
|
||||||
|
label="Entity (Required)"
|
||||||
|
.configValue=${'entity'}
|
||||||
|
@value-changed=${this.onValueChange}
|
||||||
|
>
|
||||||
|
<paper-listbox
|
||||||
|
slot="dropdown-content"
|
||||||
|
.selected=${this.datetimeEntities.indexOf(this.config.entity)}
|
||||||
|
>
|
||||||
|
${this.datetimeEntities.map((entity) => html`<paper-item>${entity}</paper-item>`)}
|
||||||
|
</paper-listbox>
|
||||||
|
</paper-dropdown-menu>
|
||||||
|
<paper-input
|
||||||
|
label="Name (Optional)"
|
||||||
|
.configValue=${'name'}
|
||||||
|
.value=${this.config.name}
|
||||||
|
.placeholder=${this.entity?.attributes.friendly_name}
|
||||||
|
@value-changed=${this.onValueChange}
|
||||||
|
></paper-input>
|
||||||
|
<div class="side-by-side">
|
||||||
|
<div>
|
||||||
|
<ha-switch
|
||||||
|
style="margin-left: 10px"
|
||||||
|
.checked="${!Boolean(this.config.hide?.name)}"
|
||||||
|
@change="${this.onHideNameChange}"
|
||||||
|
></ha-switch>
|
||||||
|
Show name?
|
||||||
|
</div>
|
||||||
|
<paper-dropdown-menu
|
||||||
|
style="width: 100%"
|
||||||
|
label="Name Position (Optional)"
|
||||||
|
@value-changed=${this.onLayoutNameChange}
|
||||||
|
>
|
||||||
|
<paper-listbox
|
||||||
|
slot="dropdown-content"
|
||||||
|
.selected=${Object.values(Layout.Name).indexOf(
|
||||||
|
this.config.layout?.name ?? DEFAULT_LAYOUT_NAME
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
${Object.values(Layout.Name).map((name) => html`<paper-item>${name}</paper-item>`)}
|
||||||
|
</paper-listbox>
|
||||||
|
</paper-dropdown-menu>
|
||||||
|
</div>
|
||||||
|
<div class="side-by-side">
|
||||||
|
<paper-input
|
||||||
|
type="number"
|
||||||
|
label="Hour Step (Optional)"
|
||||||
|
.configValue=${'hour_step'}
|
||||||
|
.value=${this.config.hour_step || DEFAULT_HOUR_STEP}
|
||||||
|
@value-changed=${this.onValueChange}
|
||||||
|
></paper-input>
|
||||||
|
<paper-input
|
||||||
|
type="number"
|
||||||
|
label="Minute Step (Optional)"
|
||||||
|
.configValue=${'minute_step'}
|
||||||
|
.value=${this.config.minute_step || DEFAULT_MINUTE_STEP}
|
||||||
|
@value-changed=${this.onValueChange}
|
||||||
|
></paper-input>
|
||||||
|
</div>
|
||||||
|
<div class="side-by-side">
|
||||||
|
<div>
|
||||||
|
<ha-switch
|
||||||
|
style="margin-left: 10px"
|
||||||
|
.checked="${this.config.hide?.seconds === false}"
|
||||||
|
@change="${this.onHideSecondsChange}"
|
||||||
|
></ha-switch>
|
||||||
|
Show seconds?
|
||||||
|
</div>
|
||||||
|
<paper-input
|
||||||
|
type="number"
|
||||||
|
label="Second Step (Optional)"
|
||||||
|
.configValue=${'second_step'}
|
||||||
|
.value=${this.config.second_step || DEFAULT_SECOND_STEP}
|
||||||
|
@value-changed=${this.onValueChange}
|
||||||
|
></paper-input>
|
||||||
|
</div>
|
||||||
|
<div class="side-by-side">
|
||||||
|
<div>
|
||||||
|
<ha-switch
|
||||||
|
.checked="${this.config.hour_mode === 12}"
|
||||||
|
@change="${this.onHourModeChange}"
|
||||||
|
></ha-switch>
|
||||||
|
12-Hour mode
|
||||||
|
</div>
|
||||||
|
${this.config.hour_mode === 12
|
||||||
|
? html`<div>
|
||||||
|
<ha-switch
|
||||||
|
.checked="${this.config.layout?.hour_mode === 'single'}"
|
||||||
|
@change="${this.onHourModeLayoutChange}"
|
||||||
|
></ha-switch>
|
||||||
|
"Single" hour mode layout
|
||||||
|
</div>`
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
<div class="side-by-side">
|
||||||
|
<paper-dropdown-menu
|
||||||
|
style="width: 100%"
|
||||||
|
label="Align Controls (Optional)"
|
||||||
|
@value-changed=${this.onLayoutAlignChange}
|
||||||
|
>
|
||||||
|
<paper-listbox
|
||||||
|
slot="dropdown-content"
|
||||||
|
.selected=${Object.values(Layout.AlignControls).indexOf(
|
||||||
|
this.config.layout?.align_controls ?? DEFAULT_LAYOUT_ALIGN_CONTROLS
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
${Object.values(Layout.AlignControls).map((a) => html`<paper-item>${a}</paper-item>`)}
|
||||||
|
</paper-listbox>
|
||||||
|
</paper-dropdown-menu>
|
||||||
|
<div>
|
||||||
|
<ha-switch
|
||||||
|
.checked="${this.config.link_values}"
|
||||||
|
@change="${this.onLinkValuesChange}"
|
||||||
|
></ha-switch>
|
||||||
|
Link Values
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="side-by-side">
|
||||||
|
<div>
|
||||||
|
<ha-switch
|
||||||
|
.checked="${this.config.layout?.embedded}"
|
||||||
|
@change="${this.onEmbeddedChange}"
|
||||||
|
></ha-switch>
|
||||||
|
Embedded
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfig(config): void {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onHourModeChange({ target: { checked } }): void {
|
||||||
|
const hourMode: HourMode = checked ? 12 : 24;
|
||||||
|
|
||||||
|
const newConfig = { ...this.config, hour_mode: hourMode };
|
||||||
|
this.dispatch(newConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onHideNameChange({ target: { checked } }): void {
|
||||||
|
const newConfig = { ...this.config, hide: { name: !checked } };
|
||||||
|
this.dispatch(newConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onHideSecondsChange({ target: { checked } }): void {
|
||||||
|
const newConfig = { ...this.config, hide: { seconds: !checked } };
|
||||||
|
this.dispatch(newConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onLayoutAlignChange({ detail: { value } }: CustomEvent): void {
|
||||||
|
const layout = { ...this.config.layout, align_controls: value };
|
||||||
|
const newConfig = { ...this.config, layout };
|
||||||
|
this.dispatch(newConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onLayoutNameChange({ detail: { value } }: CustomEvent): void {
|
||||||
|
const layout = { ...this.config.layout, name: value };
|
||||||
|
const newConfig = { ...this.config, layout };
|
||||||
|
this.dispatch(newConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onHourModeLayoutChange({ target: { checked } }): void {
|
||||||
|
const hourModeLayout: Layout.HourMode = checked ? 'single' : 'double';
|
||||||
|
|
||||||
|
const newConfig = {
|
||||||
|
...this.config,
|
||||||
|
layout: { ...this.config.layout, hour_mode: hourModeLayout },
|
||||||
|
};
|
||||||
|
this.dispatch(newConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onLinkValuesChange({ target: { checked } }): void {
|
||||||
|
const newConfig = { ...this.config, link_values: checked };
|
||||||
|
this.dispatch(newConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onEmbeddedChange({ target: { checked } }): void {
|
||||||
|
const newConfig = { ...this.config, layout: { embedded: checked } };
|
||||||
|
this.dispatch(newConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onValueChange(e: CustomEvent): void {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const property = (e.target as any).configValue;
|
||||||
|
let value = e.detail.value;
|
||||||
|
|
||||||
|
if (TimePickerCardEditor.NUMBER_PROPERTIES.indexOf(property) > -1) {
|
||||||
|
value = parseInt(value);
|
||||||
|
|
||||||
|
if (isNaN(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config[property] === value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newConfig = { ...this.config, [property]: value };
|
||||||
|
|
||||||
|
this.dispatch(newConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private dispatch(config: TimePickerCardConfig): void {
|
||||||
|
const event = new CustomEvent(TimePickerCardEditor.CONFIG_CHANGED_EVENT, {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: { config },
|
||||||
|
});
|
||||||
|
|
||||||
|
this.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return css`
|
||||||
|
ha-switch {
|
||||||
|
padding: 16px 0;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
.side-by-side {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.side-by-side > * {
|
||||||
|
flex: 1;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
.suffix {
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,23 @@
|
|||||||
import { TimeUnit } from './time-unit';
|
import { DEFAULT_HOUR_STEP } from '../const';
|
||||||
import { HourMode } from '../types';
|
import { HourMode } from '../types';
|
||||||
|
import { TimeUnit } from './time-unit';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the hour value of a datetime.
|
||||||
|
*/
|
||||||
export class Hour extends TimeUnit {
|
export class Hour extends TimeUnit {
|
||||||
private static readonly DEFAULT_STEP = 1;
|
private static readonly VALUE_LIMIT = 24;
|
||||||
private static readonly MAX = 24;
|
|
||||||
|
|
||||||
constructor(value: number, step = Hour.DEFAULT_STEP, private hourMode: HourMode) {
|
constructor(value: number, step = DEFAULT_HOUR_STEP, private hourMode?: HourMode) {
|
||||||
super(value, step, Hour.MAX);
|
super(value, step, Hour.VALUE_LIMIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
get minValue(): number {
|
||||||
|
return this.hourMode === 12 ? 1 : 0; // there is no 00:00 AM -> it's 12:00 AM.
|
||||||
}
|
}
|
||||||
|
|
||||||
get maxValue(): number {
|
get maxValue(): number {
|
||||||
return this.hourMode || Hour.MAX;
|
return this.hourMode === 12 ? 12 : Hour.VALUE_LIMIT - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
togglePeriod(): void {
|
togglePeriod(): void {
|
||||||
@@ -18,15 +25,15 @@ export class Hour extends TimeUnit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toString(): string {
|
toString(): string {
|
||||||
const value = this.hourMode === 12 ? (this.value + 12) % 12 : this.value;
|
let value;
|
||||||
|
|
||||||
|
if (this.hourMode === 12) {
|
||||||
|
value = (this.value + 12) % 12;
|
||||||
|
value = value === 0 ? 12 : value;
|
||||||
|
} else {
|
||||||
|
value = this.value;
|
||||||
|
}
|
||||||
|
|
||||||
return value < 10 ? `0${value}` : value.toString();
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
|
import { DEFAULT_MINUTE_STEP } from '../const';
|
||||||
import { TimeUnit } from './time-unit';
|
import { TimeUnit } from './time-unit';
|
||||||
|
import { Direction } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* * Represents the minute value of a datetime.
|
||||||
|
*/
|
||||||
export class Minute extends TimeUnit {
|
export class Minute extends TimeUnit {
|
||||||
private static readonly DEFAULT_STEP = 5;
|
private static readonly VALUE_LIMIT = 60;
|
||||||
private static readonly MAX = 60;
|
|
||||||
|
|
||||||
maxValue = Minute.MAX;
|
minValue = 0;
|
||||||
|
maxValue = Minute.VALUE_LIMIT - 1;
|
||||||
|
|
||||||
constructor(value: number, step = Minute.DEFAULT_STEP) {
|
constructor(value: number, step = DEFAULT_MINUTE_STEP) {
|
||||||
super(value, step, Minute.MAX);
|
super(value, step, Minute.VALUE_LIMIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected isValidString(valueStr: string): boolean {
|
/**
|
||||||
const value = parseInt(valueStr);
|
* Returns true if the minutes will overflow to a different hour when changed in {@param direction}.
|
||||||
return !isNaN(value) && value >= 0 && value <= this._limit;
|
* @param direction
|
||||||
|
*/
|
||||||
|
willOverflow(direction: Direction): boolean {
|
||||||
|
const newValue = direction === Direction.UP ? this.value + this._step : this.value - this._step;
|
||||||
|
return newValue >= this._limit || newValue < 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { DEFAULT_SECOND_STEP } from '../const';
|
||||||
|
import { TimeUnit } from './time-unit';
|
||||||
|
import { Direction } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* * Represents the second value of a datetime.
|
||||||
|
*/
|
||||||
|
export class Second extends TimeUnit {
|
||||||
|
private static readonly VALUE_LIMIT = 60;
|
||||||
|
|
||||||
|
minValue = 0;
|
||||||
|
maxValue = Second.VALUE_LIMIT - 1;
|
||||||
|
|
||||||
|
constructor(value: number, step = DEFAULT_SECOND_STEP) {
|
||||||
|
super(value, step, Second.VALUE_LIMIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the seconds will overflow to a different hour when changed in {@param direction}.
|
||||||
|
* @param direction
|
||||||
|
*/
|
||||||
|
willOverflow(direction: Direction): boolean {
|
||||||
|
const newValue = direction === Direction.UP ? this.value + this._step : this.value - this._step;
|
||||||
|
return newValue >= this._limit || newValue < 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
import { Direction } from '../types';
|
import { Direction } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a single unit of a datetime oobject - e.g. hours or minutes.
|
||||||
|
*/
|
||||||
export abstract class TimeUnit {
|
export abstract class TimeUnit {
|
||||||
/**
|
/**
|
||||||
* Return true if the valueStr can be set as a value of this instance.
|
* The min allowed UI value for this instance.
|
||||||
*/
|
*/
|
||||||
protected abstract isValidString(valueStr: string): boolean;
|
abstract minValue: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The max allowed value for this instance. Used for UI validation.
|
* The max allowed value for this instance.
|
||||||
*/
|
*/
|
||||||
abstract maxValue: number;
|
abstract maxValue: number;
|
||||||
|
|
||||||
@@ -23,33 +25,50 @@ export abstract class TimeUnit {
|
|||||||
return this._value;
|
return this._value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a value from {@param stringValue}, if it can be parsed and is valid.
|
||||||
|
* @param stringValue
|
||||||
|
*/
|
||||||
setStringValue(stringValue: string): void {
|
setStringValue(stringValue: string): void {
|
||||||
if (this.isValidString(stringValue)) {
|
if (this.isValidString(stringValue)) {
|
||||||
this.setValue(parseInt(stringValue));
|
this.setValue(parseInt(stringValue));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stepUpdate(direction: Direction): void {
|
/**
|
||||||
direction === Direction.UP ? this.increment() : this.decrement();
|
* Updates the value in {@param direction} by the step size specified in the constructoor.
|
||||||
}
|
* @param direction
|
||||||
|
*/
|
||||||
increment(): void {
|
stepUpdate(direction: Direction, step: number = this._step): void {
|
||||||
this.setValue(this.value + this._step);
|
direction === Direction.UP ? this.increment(step) : this.decrement(step);
|
||||||
}
|
|
||||||
|
|
||||||
decrement(): void {
|
|
||||||
this.setValue(this.value - this._step);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toString(): string {
|
toString(): string {
|
||||||
return this.value < 10 ? `0${this.value}` : this.value.toString();
|
return this.value < 10 ? `0${this.value}` : this.value.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private increment(step: number = this._step): void {
|
||||||
|
this.setValue(this.value + step);
|
||||||
|
}
|
||||||
|
|
||||||
|
private decrement(step: number = this._step): void {
|
||||||
|
this.setValue(this.value - step);
|
||||||
|
}
|
||||||
|
|
||||||
protected setValue(newValue: number): void {
|
protected setValue(newValue: number): void {
|
||||||
|
if (isNaN(newValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (newValue >= this._limit || newValue < 0) {
|
if (newValue >= this._limit || newValue < 0) {
|
||||||
newValue = (newValue + this._limit) % this._limit;
|
newValue = (newValue + this._limit) % this._limit;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._value = newValue;
|
this._value = newValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected isValidString(valueStr: string): boolean {
|
||||||
|
const value = parseInt(valueStr);
|
||||||
|
return !isNaN(value) && value >= 0 && value < this._limit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Direction } from '../types';
|
||||||
|
import { Hour } from './hour';
|
||||||
|
import { Minute } from './minute';
|
||||||
|
import { Second } from './second';
|
||||||
|
|
||||||
|
export class Time {
|
||||||
|
constructor(
|
||||||
|
public hour: Hour,
|
||||||
|
public minute: Minute,
|
||||||
|
public second: Second,
|
||||||
|
private _linkValues: boolean = false
|
||||||
|
) {}
|
||||||
|
|
||||||
|
hourStep(direction: Direction): void {
|
||||||
|
this.hour.stepUpdate(direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
minuteStep(direction: Direction): void {
|
||||||
|
if (this._linkValues && this.minute.willOverflow(direction)) {
|
||||||
|
this.hour.stepUpdate(direction, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.minute.stepUpdate(direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
secondStep(direction: Direction): void {
|
||||||
|
if (this._linkValues && this.second.willOverflow(direction)) {
|
||||||
|
this.minute.stepUpdate(direction, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.second.stepUpdate(direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
get value(): string {
|
||||||
|
return `${this.hour.value}:${this.minute.value}:${this.second.value}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { html, TemplateResult } from 'lit-element';
|
import { html, TemplateResult } from 'lit-element';
|
||||||
import { TimePickerCardConfig } from './types';
|
import { TimePickerCardConfig } from './types';
|
||||||
import { LovelaceCard } from 'custom-card-helpers';
|
import { LovelaceCard } from 'custom-card-helpers';
|
||||||
|
import { HassEntity } from 'home-assistant-js-websocket';
|
||||||
|
|
||||||
export class Partial {
|
export class Partial {
|
||||||
static error(error: string, origConfig: TimePickerCardConfig): TemplateResult {
|
static error(error: string, origConfig: TimePickerCardConfig): TemplateResult {
|
||||||
@@ -14,11 +15,12 @@ export class Partial {
|
|||||||
return html`${errorCard}`;
|
return html`${errorCard}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static header(title: string, icon?: string): TemplateResult | null {
|
static headerName(title: string): TemplateResult {
|
||||||
return html`
|
return html`<div class="time-picker-header">${title}</div>`;
|
||||||
<div class="time-picker-header">
|
}
|
||||||
${icon ? html`<ha-icon class="icon" icon=${icon}></ha-icon>` : ''}${title}
|
|
||||||
</div>
|
static nestedName(name: string, entity: HassEntity): TemplateResult {
|
||||||
`;
|
return html`<state-badge .stateObj=${entity}></state-badge>
|
||||||
|
<div class="entity-name-inside">${name}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { computeDomain, HomeAssistant, LovelaceCard } from 'custom-card-helpers';
|
||||||
import { HassEntity } from 'home-assistant-js-websocket';
|
import { HassEntity } from 'home-assistant-js-websocket';
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
@@ -9,13 +9,23 @@ import {
|
|||||||
property,
|
property,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from 'lit-element';
|
} from 'lit-element';
|
||||||
|
import { ClassInfo, classMap } from 'lit-html/directives/class-map';
|
||||||
import './components/time-period.component';
|
import './components/time-period.component';
|
||||||
import './components/time-unit.component';
|
import './components/time-unit.component';
|
||||||
import { CARD_SIZE, CARD_VERSION } from './const';
|
import {
|
||||||
|
CARD_SIZE,
|
||||||
|
CARD_VERSION,
|
||||||
|
DEFAULT_LAYOUT_ALIGN_CONTROLS,
|
||||||
|
DEFAULT_LAYOUT_HOUR_MODE,
|
||||||
|
ENTITY_DOMAIN,
|
||||||
|
} from './const';
|
||||||
|
import './editor';
|
||||||
import { Hour } from './models/hour';
|
import { Hour } from './models/hour';
|
||||||
import { Minute } from './models/minute';
|
import { Minute } from './models/minute';
|
||||||
|
import { Second } from './models/second';
|
||||||
|
import { Time } from './models/time';
|
||||||
import { Partial } from './partials';
|
import { Partial } from './partials';
|
||||||
import { Period, TimePickerCardConfig } from './types';
|
import { Layout, 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} `,
|
||||||
@@ -23,20 +33,43 @@ console.info(
|
|||||||
'color: white; font-weight: bold; background: dimgray'
|
'color: white; font-weight: bold; background: dimgray'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
window.customCards = window.customCards || [];
|
||||||
|
window.customCards.push({
|
||||||
|
type: 'time-picker-card',
|
||||||
|
name: 'Time Picker Card',
|
||||||
|
description: 'A Time Picker card for setting the time value of Input Datetime entities.',
|
||||||
|
});
|
||||||
|
|
||||||
@customElement('time-picker-card')
|
@customElement('time-picker-card')
|
||||||
export class TimePickerCard extends LitElement {
|
export class TimePickerCard extends LitElement implements LovelaceCard {
|
||||||
@property() private hass!: HomeAssistant;
|
@property({ type: Object }) hass!: HomeAssistant;
|
||||||
@property() private config!: TimePickerCardConfig;
|
@property() private config!: TimePickerCardConfig;
|
||||||
@property() private hour!: Hour;
|
@property() private time!: Time;
|
||||||
@property() private minute!: Minute;
|
|
||||||
@property() private period!: Period;
|
@property() private period!: Period;
|
||||||
|
|
||||||
private get entity(): HassEntity | undefined {
|
private get entity(): HassEntity | undefined {
|
||||||
return this.hass.states[this.config.entity];
|
return this.hass.states[this.config.entity];
|
||||||
}
|
}
|
||||||
|
|
||||||
private get shouldShowName(): boolean {
|
private get isEmbedded(): boolean {
|
||||||
return Boolean(this.config.hide?.name) === false && Boolean(this.name);
|
return this.config.layout?.embedded === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get hasNameInHeader(): boolean {
|
||||||
|
return (
|
||||||
|
Boolean(this.name) &&
|
||||||
|
Boolean(this.config.hide?.name) === false &&
|
||||||
|
this.config.layout?.name !== Layout.Name.INSIDE &&
|
||||||
|
Boolean(this.config.layout?.embedded) === false // embedded layout disables name in header
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get hasNameInside(): boolean {
|
||||||
|
return (
|
||||||
|
Boolean(this.name) &&
|
||||||
|
Boolean(this.config.hide?.name) === false &&
|
||||||
|
(this.config.layout?.name === Layout.Name.INSIDE || Boolean(this.config.layout?.embedded))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get name(): string | undefined {
|
private get name(): string | undefined {
|
||||||
@@ -47,41 +80,88 @@ export class TimePickerCard extends LitElement {
|
|||||||
return this.config.hour_mode === 12;
|
return this.config.hour_mode === 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get layoutAlign(): Layout.AlignControls {
|
||||||
|
return this.config.layout?.align_controls ?? DEFAULT_LAYOUT_ALIGN_CONTROLS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get haCardClass(): ClassInfo {
|
||||||
|
return {
|
||||||
|
embedded: this.isEmbedded,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private get rowClass(): ClassInfo {
|
||||||
|
return {
|
||||||
|
'time-picker-row': true,
|
||||||
|
'with-header-name': this.hasNameInHeader,
|
||||||
|
embedded: this.isEmbedded,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private get contentClass(): ClassInfo {
|
||||||
|
return {
|
||||||
|
'time-picker-content': true,
|
||||||
|
[`layout-${this.layoutAlign}`]: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.entity.entity_id.startsWith('input_datetime')) {
|
if (computeDomain(this.entity.entity_id) !== ENTITY_DOMAIN) {
|
||||||
return Partial.error('You must set an input_datetime entity', this.config);
|
return Partial.error(`You must set an ${ENTITY_DOMAIN} entity`, this.config);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.entity.attributes.has_time) {
|
if (!this.entity.attributes.has_time) {
|
||||||
return Partial.error(
|
return Partial.error(
|
||||||
'You must set an input_datetime entity that sets has_time: true',
|
`You must set an ${ENTITY_DOMAIN} entity that sets has_time: true`,
|
||||||
this.config
|
this.config
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { hour, minute } = this.entity!.attributes;
|
const { hour, minute, second } = this.entity!.attributes;
|
||||||
this.hour = new Hour(hour, this.config.hour_step, this.config.hour_mode);
|
const hourInstance = new Hour(hour, this.config.hour_step, this.config.hour_mode);
|
||||||
this.minute = new Minute(minute, this.config.minute_step);
|
const minuteInstance = new Minute(minute, this.config.minute_step);
|
||||||
this.period = this.hour.value >= 12 ? Period.PM : Period.AM;
|
const secondInstance = new Second(second, this.config.second_step);
|
||||||
|
this.time = new Time(hourInstance, minuteInstance, secondInstance, this.config.link_values);
|
||||||
|
this.period = hourInstance.value >= 12 ? Period.PM : Period.AM;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-card>
|
<ha-card class=${classMap(this.haCardClass)}>
|
||||||
${this.shouldShowName ? Partial.header(this.name!) : ''}
|
${this.hasNameInHeader ? Partial.headerName(this.name!) : ''}
|
||||||
<div class="time-picker-content">
|
<div class=${classMap(this.rowClass)}>
|
||||||
<time-unit .unit=${this.hour} @update=${this.callHassService}></time-unit>
|
${this.hasNameInside ? Partial.nestedName(this.name!, this.entity) : ''}
|
||||||
<div class="time-separator">:</div>
|
|
||||||
<time-unit .unit=${this.minute} @update=${this.callHassService}></time-unit>
|
|
||||||
|
|
||||||
${this.shouldShowPeriod
|
<div class=${classMap(this.contentClass)}>
|
||||||
? html`<time-period
|
<time-unit
|
||||||
.period=${this.period}
|
.unit=${this.time.hour}
|
||||||
@toggle=${this.onPeriodToggle}
|
@stepChange=${this.onHourStepChange}
|
||||||
></time-period>`
|
@update=${this.callHassService}
|
||||||
: ''}
|
></time-unit>
|
||||||
|
<div class="time-separator">:</div>
|
||||||
|
<time-unit
|
||||||
|
.unit=${this.time.minute}
|
||||||
|
@stepChange=${this.onMinuteStepChange}
|
||||||
|
@update=${this.callHassService}
|
||||||
|
></time-unit>
|
||||||
|
${this.config.hide?.seconds === false
|
||||||
|
? html`<div class="time-separator">:</div>
|
||||||
|
<time-unit
|
||||||
|
.unit=${this.time.second}
|
||||||
|
@stepChange=${this.onSecondStepChange}
|
||||||
|
@update=${this.callHassService}
|
||||||
|
></time-unit>`
|
||||||
|
: ''}
|
||||||
|
${this.shouldShowPeriod
|
||||||
|
? html`<time-period
|
||||||
|
.period=${this.period}
|
||||||
|
.mode=${this.config.layout?.hour_mode ?? DEFAULT_LAYOUT_HOUR_MODE}
|
||||||
|
@toggle=${this.onPeriodToggle}
|
||||||
|
></time-period>`
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
@@ -108,7 +188,22 @@ export class TimePickerCard extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onPeriodToggle(): void {
|
private onPeriodToggle(): void {
|
||||||
this.hour.togglePeriod();
|
this.time.hour.togglePeriod();
|
||||||
|
this.callHassService();
|
||||||
|
}
|
||||||
|
|
||||||
|
private onHourStepChange(event: CustomEvent): void {
|
||||||
|
this.time.hourStep(event.detail.direction);
|
||||||
|
this.callHassService();
|
||||||
|
}
|
||||||
|
|
||||||
|
private onMinuteStepChange(event: CustomEvent): void {
|
||||||
|
this.time.minuteStep(event.detail.direction);
|
||||||
|
this.callHassService();
|
||||||
|
}
|
||||||
|
|
||||||
|
private onSecondStepChange(event: CustomEvent): void {
|
||||||
|
this.time.secondStep(event.detail.direction);
|
||||||
this.callHassService();
|
this.callHassService();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,11 +212,9 @@ export class TimePickerCard extends LitElement {
|
|||||||
throw new Error('Unable to update datetime');
|
throw new Error('Unable to update datetime');
|
||||||
}
|
}
|
||||||
|
|
||||||
const time = `${this.hour.value}:${this.minute.value}:00`;
|
return this.hass.callService(ENTITY_DOMAIN, 'set_datetime', {
|
||||||
|
|
||||||
return this.hass.callService('input_datetime', 'set_datetime', {
|
|
||||||
entity_id: this.entity!.entity_id,
|
entity_id: this.entity!.entity_id,
|
||||||
time,
|
time: this.time.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,29 +223,86 @@ export class TimePickerCard extends LitElement {
|
|||||||
:host {
|
:host {
|
||||||
--tpc-elements-background-color: var(
|
--tpc-elements-background-color: var(
|
||||||
--time-picker-elements-background-color,
|
--time-picker-elements-background-color,
|
||||||
var(--dark-primary-color)
|
var(--primary-color)
|
||||||
);
|
);
|
||||||
|
|
||||||
--tpc-icon-color: var(--time-picker-icon-color, var(--primary-text-color));
|
--tpc-icon-color: var(--time-picker-icon-color, var(--primary-text-color));
|
||||||
--tpc-text-color: var(--time-picker-text-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));
|
--tpc-accent-color: var(--time-picker-accent-color, var(--primary-color));
|
||||||
|
--tpc-off-color: var(--time-picker-off-color, var(--disabled-text-color));
|
||||||
|
|
||||||
|
--tpc-border-radius: var(--time-picker-border-radius, var(--ha-card-border-radius, 4px));
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-card.embedded {
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-picker-header {
|
.time-picker-header {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
color: var(--tpc-text-color, #fff);
|
color: var(--tpc-text-color);
|
||||||
background-color: var(--tpc-elements-background-color);
|
background-color: var(--tpc-elements-background-color);
|
||||||
|
border-top-left-radius: var(--tpc-border-radius);
|
||||||
|
border-top-right-radius: var(--tpc-border-radius);
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-picker-content {
|
.time-picker-row {
|
||||||
padding: 8px 16px 16px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-picker-row.embedded {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-picker-row.with-header-name {
|
||||||
|
padding: 8px 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-picker-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-picker-content.layout-left {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-picker-content.layout-center {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.time-picker-content.layout-right {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-name-inside {
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getStubConfig(
|
||||||
|
_: HomeAssistant,
|
||||||
|
entities: Array<string>
|
||||||
|
): Omit<TimePickerCardConfig, 'type'> {
|
||||||
|
const datetimeEntity = entities.find((entityId) => computeDomain(entityId) === ENTITY_DOMAIN);
|
||||||
|
|
||||||
|
return {
|
||||||
|
entity: datetimeEntity || 'input_datetime.example_entity',
|
||||||
|
hour_mode: 24,
|
||||||
|
hour_step: 1,
|
||||||
|
minute_step: 5,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static getConfigElement(): LovelaceCard {
|
||||||
|
return document.createElement('time-picker-card-editor') as LovelaceCard;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,42 @@ import { LovelaceCardConfig } from 'custom-card-helpers';
|
|||||||
export interface TimePickerCardConfig extends LovelaceCardConfig {
|
export interface TimePickerCardConfig extends LovelaceCardConfig {
|
||||||
entity: string;
|
entity: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
link_values?: boolean;
|
||||||
hour_mode?: HourMode;
|
hour_mode?: HourMode;
|
||||||
hour_step?: number;
|
hour_step?: number;
|
||||||
minute_step?: number;
|
minute_step?: number;
|
||||||
|
second_step?: number;
|
||||||
|
layout?: TimePickerLayoutConfig;
|
||||||
hide?: TimePickerHideConfig;
|
hide?: TimePickerHideConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type HourMode = 12 | 24 | undefined;
|
||||||
|
|
||||||
|
export interface TimePickerLayoutConfig {
|
||||||
|
align_controls?: Layout.AlignControls;
|
||||||
|
name?: Layout.Name;
|
||||||
|
hour_mode?: Layout.HourMode;
|
||||||
|
embedded?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace Layout {
|
||||||
|
export type HourMode = 'single' | 'double';
|
||||||
|
|
||||||
|
export enum AlignControls {
|
||||||
|
LEFT = 'left',
|
||||||
|
CENTER = 'center',
|
||||||
|
RIGHT = 'right',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Name {
|
||||||
|
HEADER = 'header',
|
||||||
|
INSIDE = 'inside',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface TimePickerHideConfig {
|
export interface TimePickerHideConfig {
|
||||||
name: boolean;
|
name?: boolean;
|
||||||
|
seconds?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Direction {
|
export enum Direction {
|
||||||
@@ -19,8 +47,6 @@ export enum Direction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum Period {
|
export enum Period {
|
||||||
AM = 'am',
|
AM = 'AM',
|
||||||
PM = 'pm',
|
PM = 'PM',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HourMode = 12 | 24 | undefined;
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export interface CustomCardEntry {
|
||||||
|
type: string;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
preview?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
customCards: Array<CustomCardEntry>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { LovelaceCard } from 'custom-card-helpers';
|
||||||
|
import { customElement, html, LitElement, TemplateResult } from 'lit-element';
|
||||||
|
|
||||||
|
@customElement('hui-error-card')
|
||||||
|
export class HuiErrorCard extends LitElement implements LovelaceCard {
|
||||||
|
setConfig(): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): TemplateResult {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCardSize(): number {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { expect, fixture, html } from '@open-wc/testing';
|
||||||
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import { HassEntity } from 'home-assistant-js-websocket';
|
||||||
|
import '../../src/time-picker-card';
|
||||||
|
import { TimePickerCard } from '../../src/time-picker-card';
|
||||||
|
import { TimePickerCardConfig, HourMode } from '../../src/types';
|
||||||
|
import './test-error-card';
|
||||||
|
|
||||||
|
async function render(config?: {
|
||||||
|
entityId?: string;
|
||||||
|
friendlyName?: string;
|
||||||
|
hasTime?: boolean;
|
||||||
|
hourMode?: HourMode;
|
||||||
|
hideName?: boolean;
|
||||||
|
}): Promise<TimePickerCard> {
|
||||||
|
const { entityId, friendlyName, hasTime, hourMode, hideName } = Object.assign(
|
||||||
|
{
|
||||||
|
entityId: 'input_datetime.wake_time',
|
||||||
|
friendlyName: 'Wake Time',
|
||||||
|
hasTime: true,
|
||||||
|
hideName: false,
|
||||||
|
},
|
||||||
|
config
|
||||||
|
);
|
||||||
|
|
||||||
|
const testEntity: HassEntity = {
|
||||||
|
entity_id: entityId,
|
||||||
|
attributes: {
|
||||||
|
friendly_name: friendlyName,
|
||||||
|
has_time: hasTime,
|
||||||
|
},
|
||||||
|
state: '00:10:00',
|
||||||
|
last_changed: '',
|
||||||
|
last_updated: '',
|
||||||
|
context: {
|
||||||
|
id: entityId,
|
||||||
|
user_id: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const hass: Partial<HomeAssistant> = {
|
||||||
|
states: {
|
||||||
|
[entityId]: testEntity,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const testConfig: TimePickerCardConfig = {
|
||||||
|
type: 'custom:time-picker-card',
|
||||||
|
entity: entityId,
|
||||||
|
hour_mode: hourMode,
|
||||||
|
hide: {
|
||||||
|
name: hideName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const card = await fixture<TimePickerCard>(
|
||||||
|
html`<time-picker-card .hass=${hass} .config=${testConfig}></time-picker-card>`
|
||||||
|
);
|
||||||
|
|
||||||
|
card.setConfig(testConfig);
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorCard(): string {
|
||||||
|
return html`<hui-error-card></hui-error-card>`.getHTML();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('test', () => {
|
||||||
|
it('renders', async () => {
|
||||||
|
const card = await render();
|
||||||
|
expect(card).shadowDom.to.match('time-picker-card');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors if entity is not an input_datetime', async () => {
|
||||||
|
const card = await render({ entityId: 'sensor.test_sensor' });
|
||||||
|
expect(card).shadowDom.to.equal(errorCard());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors if entity doesn't have time", async () => {
|
||||||
|
const card = await render({ hasTime: false });
|
||||||
|
expect(card).shadowDom.to.equal(errorCard());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders card name', async () => {
|
||||||
|
const card = await render();
|
||||||
|
expect(card.shadowRoot?.innerHTML).to.match(/Wake Time/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides card name', async () => {
|
||||||
|
const card = await render({ hideName: true });
|
||||||
|
expect(card.shadowRoot?.innerHTML).not.to.match(/Wake Time/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders time units', async () => {
|
||||||
|
const card = await render();
|
||||||
|
expect(card.shadowRoot?.innerHTML.match(/<time-unit>/g)!.length).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('time-period', () => {
|
||||||
|
it('is not rendered by default', async () => {
|
||||||
|
const card = await render();
|
||||||
|
expect(card.shadowRoot?.innerHTML.match(/<time-period>/g)).to.be.null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders in 12-hour mode', async () => {
|
||||||
|
const card = await render({ hourMode: 12 });
|
||||||
|
expect(card.shadowRoot?.innerHTML.match(/<time-period>/g)!.length).to.equal(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
module.exports = function (config) {
|
||||||
|
return config.set({
|
||||||
|
basePath: './',
|
||||||
|
frameworks: ['mocha', 'chai'],
|
||||||
|
files: ['**/*.test.ts'],
|
||||||
|
preprocessors: {
|
||||||
|
'**/*.test.ts': ['rollup'],
|
||||||
|
},
|
||||||
|
|
||||||
|
rollupPreprocessor: {
|
||||||
|
plugins: [
|
||||||
|
require('@rollup/plugin-node-resolve')(),
|
||||||
|
require('@rollup/plugin-json')(),
|
||||||
|
require('@rollup/plugin-typescript')(),
|
||||||
|
require('@rollup/plugin-commonjs')({
|
||||||
|
namedExports: {
|
||||||
|
chai: ['expect'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
format: 'umd',
|
||||||
|
sourcemap: 'inline',
|
||||||
|
},
|
||||||
|
onwarn: function (message) {
|
||||||
|
if (/Circular dependency/.test(message)) return;
|
||||||
|
console.warn(message);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
reporters: ['dots'],
|
||||||
|
|
||||||
|
port: 9876,
|
||||||
|
colors: true,
|
||||||
|
autoWatch: false,
|
||||||
|
singleRun: true,
|
||||||
|
|
||||||
|
concurrency: Infinity,
|
||||||
|
browsers: ['ChromeHeadless'],
|
||||||
|
|
||||||
|
client: {
|
||||||
|
captureConsole: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { Hour } from '../../src/models/hour';
|
||||||
|
import { Direction } from '../../src/types';
|
||||||
|
import { DEFAULT_HOUR_STEP } from '../../src/const';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
|
||||||
|
describe('Hour', () => {
|
||||||
|
let hour: Hour;
|
||||||
|
|
||||||
|
context('with default config', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
hour = new Hour(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns value', () => {
|
||||||
|
expect(hour.value).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has correct max value', () => {
|
||||||
|
expect(hour.maxValue).to.equal(23);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toString', () => {
|
||||||
|
it('returns formatted string', () => {
|
||||||
|
expect(hour.toString()).to.equal('02');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders values correctly', () => {
|
||||||
|
hour = new Hour(0);
|
||||||
|
expect(hour.toString()).to.equal('00');
|
||||||
|
|
||||||
|
hour = new Hour(6);
|
||||||
|
expect(hour.toString()).to.equal('06');
|
||||||
|
|
||||||
|
hour = new Hour(12);
|
||||||
|
expect(hour.toString()).to.equal('12');
|
||||||
|
|
||||||
|
hour = new Hour(18);
|
||||||
|
expect(hour.toString()).to.equal('18');
|
||||||
|
|
||||||
|
hour = new Hour(23);
|
||||||
|
expect(hour.toString()).to.equal('23');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stepUpdate', () => {
|
||||||
|
it('updates up', () => {
|
||||||
|
hour.stepUpdate(Direction.UP);
|
||||||
|
expect(hour.value).to.equal(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates down', () => {
|
||||||
|
hour.stepUpdate(Direction.DOWN);
|
||||||
|
expect(hour.value).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('goes up from 23 to 0', () => {
|
||||||
|
hour = new Hour(23);
|
||||||
|
hour.stepUpdate(Direction.UP);
|
||||||
|
expect(hour.value).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('goes down from 0 to 23', () => {
|
||||||
|
hour = new Hour(0);
|
||||||
|
hour.stepUpdate(Direction.DOWN);
|
||||||
|
expect(hour.value).to.equal(23);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setStringValue', () => {
|
||||||
|
it('sets value from valid string', () => {
|
||||||
|
hour.setStringValue('3');
|
||||||
|
expect(hour.value).to.equal(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't set value from invalid string", () => {
|
||||||
|
hour.setStringValue('test');
|
||||||
|
expect(hour.value).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't set value from outside of scope", () => {
|
||||||
|
hour.setStringValue('24');
|
||||||
|
expect(hour.value).to.equal(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('with a different step size', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
hour = new Hour(2, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('goes up correctly', () => {
|
||||||
|
hour.stepUpdate(Direction.UP);
|
||||||
|
expect(hour.value).to.equal(7);
|
||||||
|
|
||||||
|
hour.stepUpdate(Direction.UP);
|
||||||
|
hour.stepUpdate(Direction.UP);
|
||||||
|
hour.stepUpdate(Direction.UP);
|
||||||
|
hour.stepUpdate(Direction.UP);
|
||||||
|
|
||||||
|
expect(hour.value).to.equal(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('goes down correclty', () => {
|
||||||
|
hour.stepUpdate(Direction.DOWN);
|
||||||
|
expect(hour.value).to.equal(21);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('in 12 hour mode', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
hour = new Hour(2, DEFAULT_HOUR_STEP, 12);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has correct max value', () => {
|
||||||
|
expect(hour.maxValue).to.equal(12);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('togglePeriod', () => {
|
||||||
|
it('changes values correclty', () => {
|
||||||
|
hour = new Hour(2, DEFAULT_HOUR_STEP, 12);
|
||||||
|
hour.togglePeriod();
|
||||||
|
expect(hour.value).to.equal(14);
|
||||||
|
|
||||||
|
hour = new Hour(0, DEFAULT_HOUR_STEP, 12);
|
||||||
|
hour.togglePeriod();
|
||||||
|
expect(hour.value).to.equal(12);
|
||||||
|
|
||||||
|
hour = new Hour(23, DEFAULT_HOUR_STEP, 12);
|
||||||
|
hour.togglePeriod();
|
||||||
|
expect(hour.value).to.equal(11);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has no side effects', () => {
|
||||||
|
const value = hour.value;
|
||||||
|
hour.togglePeriod();
|
||||||
|
hour.togglePeriod();
|
||||||
|
expect(hour.value).to.equal(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toString', () => {
|
||||||
|
it('renders values above 12 orrectly', () => {
|
||||||
|
hour = new Hour(13, DEFAULT_HOUR_STEP, 12);
|
||||||
|
expect(hour.toString()).to.equal('01');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders values correctly', () => {
|
||||||
|
hour = new Hour(0, DEFAULT_HOUR_STEP, 12);
|
||||||
|
expect(hour.toString()).to.equal('12');
|
||||||
|
|
||||||
|
hour = new Hour(6, DEFAULT_HOUR_STEP, 12);
|
||||||
|
expect(hour.toString()).to.equal('06');
|
||||||
|
|
||||||
|
hour = new Hour(12, DEFAULT_HOUR_STEP, 12);
|
||||||
|
expect(hour.toString()).to.equal('12');
|
||||||
|
|
||||||
|
hour = new Hour(18, DEFAULT_HOUR_STEP, 12);
|
||||||
|
expect(hour.toString()).to.equal('06');
|
||||||
|
|
||||||
|
hour = new Hour(23, DEFAULT_HOUR_STEP, 12);
|
||||||
|
expect(hour.toString()).to.equal('11');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { Minute } from '../../src/models/minute';
|
||||||
|
import { Direction } from '../../src/types';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
|
||||||
|
describe('Minute', () => {
|
||||||
|
let minute: Minute;
|
||||||
|
|
||||||
|
context('with default config', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
minute = new Minute(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns value', () => {
|
||||||
|
expect(minute.value).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has correct max value', () => {
|
||||||
|
expect(minute.maxValue).to.equal(59);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toString', () => {
|
||||||
|
it('renders values correctly', () => {
|
||||||
|
minute = new Minute(0);
|
||||||
|
expect(minute.toString()).to.equal('00');
|
||||||
|
|
||||||
|
minute = new Minute(5);
|
||||||
|
expect(minute.toString()).to.equal('05');
|
||||||
|
|
||||||
|
minute = new Minute(59);
|
||||||
|
expect(minute.toString()).to.equal('59');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stepUpdate', () => {
|
||||||
|
it('updates up', () => {
|
||||||
|
minute.stepUpdate(Direction.UP);
|
||||||
|
expect(minute.value).to.equal(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates down', () => {
|
||||||
|
minute.stepUpdate(Direction.DOWN);
|
||||||
|
expect(minute.value).to.equal(57);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('goes up from 59 to 4', () => {
|
||||||
|
minute = new Minute(59);
|
||||||
|
minute.stepUpdate(Direction.UP);
|
||||||
|
expect(minute.value).to.equal(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('goes down from 0 to 55', () => {
|
||||||
|
minute = new Minute(0);
|
||||||
|
minute.stepUpdate(Direction.DOWN);
|
||||||
|
expect(minute.value).to.equal(55);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setStringValue', () => {
|
||||||
|
it('sets value from valid string', () => {
|
||||||
|
minute.setStringValue('3');
|
||||||
|
expect(minute.value).to.equal(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't set value from invalid string", () => {
|
||||||
|
minute.setStringValue('test');
|
||||||
|
expect(minute.value).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't set value from outside of scope", () => {
|
||||||
|
minute.setStringValue('60');
|
||||||
|
expect(minute.value).to.equal(2);
|
||||||
|
|
||||||
|
minute.setStringValue('65');
|
||||||
|
expect(minute.value).to.equal(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('with a different step size', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
minute = new Minute(2, 15);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('goes up correctly', () => {
|
||||||
|
minute.stepUpdate(Direction.UP);
|
||||||
|
expect(minute.value).to.equal(17);
|
||||||
|
|
||||||
|
minute.stepUpdate(Direction.UP);
|
||||||
|
minute.stepUpdate(Direction.UP);
|
||||||
|
minute.stepUpdate(Direction.UP);
|
||||||
|
expect(minute.value).to.equal(2);
|
||||||
|
|
||||||
|
minute.stepUpdate(Direction.UP);
|
||||||
|
expect(minute.value).to.equal(17);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('goes down correclty', () => {
|
||||||
|
minute.stepUpdate(Direction.DOWN);
|
||||||
|
expect(minute.value).to.equal(47);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('will overflow', () => {
|
||||||
|
it('returns true if the minutes will overflow an hour up', () => {
|
||||||
|
minute.setStringValue('59');
|
||||||
|
expect(minute.willOverflow(Direction.UP)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true if the minutes will overflow an hour down', () => {
|
||||||
|
minute.setStringValue('0');
|
||||||
|
expect(minute.willOverflow(Direction.DOWN)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true if the minutes will not overflow', () => {
|
||||||
|
minute.setStringValue('55');
|
||||||
|
expect(minute.willOverflow(Direction.DOWN)).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { Second } from '../../src/models/second';
|
||||||
|
import { Direction } from '../../src/types';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
|
||||||
|
describe('Second', () => {
|
||||||
|
let second: Second;
|
||||||
|
|
||||||
|
context('with default config', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
second = new Second(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns value', () => {
|
||||||
|
expect(second.value).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has correct max value', () => {
|
||||||
|
expect(second.maxValue).to.equal(59);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toString', () => {
|
||||||
|
it('renders values correctly', () => {
|
||||||
|
second = new Second(0);
|
||||||
|
expect(second.toString()).to.equal('00');
|
||||||
|
|
||||||
|
second = new Second(5);
|
||||||
|
expect(second.toString()).to.equal('05');
|
||||||
|
|
||||||
|
second = new Second(59);
|
||||||
|
expect(second.toString()).to.equal('59');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stepUpdate', () => {
|
||||||
|
it('updates up', () => {
|
||||||
|
second.stepUpdate(Direction.UP);
|
||||||
|
expect(second.value).to.equal(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates down', () => {
|
||||||
|
second.stepUpdate(Direction.DOWN);
|
||||||
|
expect(second.value).to.equal(57);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('goes up from 59 to 4', () => {
|
||||||
|
second = new Second(59);
|
||||||
|
second.stepUpdate(Direction.UP);
|
||||||
|
expect(second.value).to.equal(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('goes down from 0 to 55', () => {
|
||||||
|
second = new Second(0);
|
||||||
|
second.stepUpdate(Direction.DOWN);
|
||||||
|
expect(second.value).to.equal(55);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setStringValue', () => {
|
||||||
|
it('sets value from valid string', () => {
|
||||||
|
second.setStringValue('3');
|
||||||
|
expect(second.value).to.equal(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't set value from invalid string", () => {
|
||||||
|
second.setStringValue('test');
|
||||||
|
expect(second.value).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't set value from outside of scope", () => {
|
||||||
|
second.setStringValue('60');
|
||||||
|
expect(second.value).to.equal(2);
|
||||||
|
|
||||||
|
second.setStringValue('65');
|
||||||
|
expect(second.value).to.equal(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('with a different step size', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
second = new Second(2, 15);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('goes up correctly', () => {
|
||||||
|
second.stepUpdate(Direction.UP);
|
||||||
|
expect(second.value).to.equal(17);
|
||||||
|
|
||||||
|
second.stepUpdate(Direction.UP);
|
||||||
|
second.stepUpdate(Direction.UP);
|
||||||
|
second.stepUpdate(Direction.UP);
|
||||||
|
expect(second.value).to.equal(2);
|
||||||
|
|
||||||
|
second.stepUpdate(Direction.UP);
|
||||||
|
expect(second.value).to.equal(17);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('goes down correclty', () => {
|
||||||
|
second.stepUpdate(Direction.DOWN);
|
||||||
|
expect(second.value).to.equal(47);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('will overflow', () => {
|
||||||
|
it('returns true if the Seconds will overflow an hour up', () => {
|
||||||
|
second.setStringValue('59');
|
||||||
|
expect(second.willOverflow(Direction.UP)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true if the Seconds will overflow an hour down', () => {
|
||||||
|
second.setStringValue('0');
|
||||||
|
expect(second.willOverflow(Direction.DOWN)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true if the Seconds will not overflow', () => {
|
||||||
|
second.setStringValue('55');
|
||||||
|
expect(second.willOverflow(Direction.DOWN)).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"experimentalDecorators": true
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true // for importing sinon in tests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||