Compare commits

...
92 Commits
Author SHA1 Message Date
Georgi Gardev 267fdbd9f0 1.3.0 2022-11-03 18:25:14 +02:00
Georgi Gardev f8b8856029 Update visual editor and screenshots 2022-11-03 18:22:55 +02:00
Georgi Gardev 36cb344343 Implement editor using ha-form 2022-11-03 17:58:11 +02:00
Georgi Gardev 75d038777b Add thin layout option 2022-11-03 14:56:06 +02:00
Georgi Gardev 9e8c15b8cf Transparent background when embedded 2022-11-03 13:55:55 +02:00
Georgi Gardev dda16a32c4 Add tap actions 2022-11-03 13:46:12 +02:00
Georgi Gardev 460bf24903 Remove border only when embedded 2022-11-03 11:22:34 +02:00
Georgi Gardev 49bd482b7b Add dev configurations 2022-11-03 11:22:21 +02:00
Georgi Gardev a2c479af60 Update all dependencies 2022-11-03 11:16:26 +02:00
Georgi Gardev 38b2673b82 .gitignore homeassistant files 2022-11-03 10:02:48 +02:00
Georgi Gardev 33786c14c0 Add homeassistant docker for development 2022-11-03 10:02:37 +02:00
GeorgeSG ee129ddb89 Merge pull request #41 from GeorgeSG/dependabot/npm_and_yarn/terser-4.8.1 2022-11-02 17:29:35 +02:00
GeorgeSG 740354b1cf Merge pull request #44 from OToussaint/patch-1 2022-11-02 17:28:59 +02:00
Olivier Toussaint 835267f89b Fix issue #43
Fix issue #43 HA 2022.11.0
2022-11-02 10:26:42 +01:00
dependabot[bot] a606fd1d2e Bump terser from 4.8.0 to 4.8.1
Bumps [terser](https://github.com/terser/terser) from 4.8.0 to 4.8.1.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-21 01:32:41 +00:00
GeorgeSG 9b4cfeb4e5 Merge pull request #37 from thecatos/hacs-url
Update HACS URL
2022-06-02 08:24:11 +03:00
Sven Serlier f304900617 Update HACS URL 2022-06-02 06:41:49 +02:00
GeorgeSG ad6f3920e6 Add option to hide icon 2022-05-08 11:17:49 +03:00
GeorgeSG 52e6773a37 Merge pull request #36 from GeorgeSG/dependabot/npm_and_yarn/ansi-regex-3.0.1 2022-05-08 10:18:25 +03:00
dependabot[bot] 6dc770d954 Bump ansi-regex from 3.0.0 to 3.0.1
Bumps [ansi-regex](https://github.com/chalk/ansi-regex) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/chalk/ansi-regex/releases)
- [Commits](https://github.com/chalk/ansi-regex/compare/v3.0.0...v3.0.1)

---
updated-dependencies:
- dependency-name: ansi-regex
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-08 07:17:15 +00:00
GeorgeSG 1d88b5ad5b Merge pull request #28 from GeorgeSG/dependabot/npm_and_yarn/pathval-1.1.1 2022-05-08 10:17:05 +03:00
GeorgeSG c8c9e4df69 Merge pull request #30 from GeorgeSG/dependabot/npm_and_yarn/ajv-6.12.6 2022-05-08 10:16:59 +03:00
GeorgeSG a2c61d429b Merge pull request #32 from GeorgeSG/dependabot/npm_and_yarn/karma-6.3.16 2022-05-08 10:16:41 +03:00
GeorgeSG 2a7a024aa3 Merge pull request #33 from GeorgeSG/dependabot/npm_and_yarn/minimist-1.2.6 2022-05-08 10:16:28 +03:00
dependabot[bot] 80ebf6fde6 Bump minimist from 1.2.5 to 1.2.6
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-26 16:02:34 +00:00
dependabot[bot] e9d3a6c72b Bump karma from 5.2.3 to 6.3.16
Bumps [karma](https://github.com/karma-runner/karma) from 5.2.3 to 6.3.16.
- [Release notes](https://github.com/karma-runner/karma/releases)
- [Changelog](https://github.com/karma-runner/karma/blob/master/CHANGELOG.md)
- [Commits](https://github.com/karma-runner/karma/compare/v5.2.3...v6.3.16)

---
updated-dependencies:
- dependency-name: karma
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-02 08:33:43 +00:00
dependabot[bot] bde447ceb4 Bump ajv from 6.12.2 to 6.12.6
Bumps [ajv](https://github.com/ajv-validator/ajv) from 6.12.2 to 6.12.6.
- [Release notes](https://github.com/ajv-validator/ajv/releases)
- [Commits](https://github.com/ajv-validator/ajv/compare/v6.12.2...v6.12.6)

---
updated-dependencies:
- dependency-name: ajv
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-13 10:23:36 +00:00
dependabot[bot] 41bce3de6b Bump pathval from 1.1.0 to 1.1.1
Bumps [pathval](https://github.com/chaijs/pathval) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/chaijs/pathval/releases)
- [Changelog](https://github.com/chaijs/pathval/blob/master/CHANGELOG.md)
- [Commits](https://github.com/chaijs/pathval/compare/v1.1.0...v1.1.1)

---
updated-dependencies:
- dependency-name: pathval
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-11 01:41:17 +00:00
GeorgeSG e5c16c2afd Merge pull request #23 from GeorgeSG/dependabot/npm_and_yarn/path-parse-1.0.7 2021-10-10 21:00:34 +03:00
dependabot[bot] 5d06659370 Bump path-parse from 1.0.6 to 1.0.7
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-12 09:52:08 +00:00
GeorgeSG 7f7a82c653 Merge pull request #21 from GeorgeSG/dependabot/npm_and_yarn/glob-parent-5.1.2
Bump glob-parent from 5.1.1 to 5.1.2
2021-06-12 15:05:50 +03:00
dependabot[bot] 9ebf4feb50 Bump glob-parent from 5.1.1 to 5.1.2
Bumps [glob-parent](https://github.com/gulpjs/glob-parent) from 5.1.1 to 5.1.2.
- [Release notes](https://github.com/gulpjs/glob-parent/releases)
- [Changelog](https://github.com/gulpjs/glob-parent/blob/main/CHANGELOG.md)
- [Commits](https://github.com/gulpjs/glob-parent/compare/v5.1.1...v5.1.2)

---
updated-dependencies:
- dependency-name: glob-parent
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-11 14:38:28 +00:00
GeorgeSG 9aee96ad18 Merge pull request #20 from GeorgeSG/dependabot/npm_and_yarn/lodash-4.17.21
Bump lodash from 4.17.19 to 4.17.21
2021-05-11 16:20:05 +03:00
dependabot[bot] 7e70edfa31 Bump lodash from 4.17.19 to 4.17.21
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.19 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.19...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-11 13:04:45 +00:00
GeorgeSG 31ae5e4097 Merge pull request #19 from GeorgeSG/dependabot/npm_and_yarn/y18n-4.0.1
Bump y18n from 4.0.0 to 4.0.1
2021-04-01 12:04:04 +03:00
dependabot[bot] 27b05d9fcf Bump y18n from 4.0.0 to 4.0.1
Bumps [y18n](https://github.com/yargs/y18n) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-31 23:20:01 +00:00
GeorgeSG 36d9d19959 Update README 2021-03-31 19:49:53 +03:00
GeorgeSG 8c22fbbeb5 1.2.1 2021-03-31 19:41:36 +03:00
GeorgeSG def608444e Fixes #18: Change overflow to auto to avoid showing scrollbars 2021-03-31 19:41:26 +03:00
GeorgeSG afe276a9d0 1.2.0 2021-03-27 21:20:54 +02:00
GeorgeSG c9705e25aa Implement #12: CSS variable for control padding 2021-03-27 21:19:00 +02:00
GeorgeSG f4c8fd283a Fix #13: Fix card overflowing in grid 2021-03-27 20:57:50 +02:00
GeorgeSG ed570dd10a Merge pull request #14 from GeorgeSG/dependabot/npm_and_yarn/socket.io-2.4.1
Bump socket.io from 2.3.0 to 2.4.1
2021-03-27 20:29:53 +02:00
dependabot[bot] 2a032a0bbc Bump socket.io from 2.3.0 to 2.4.1
Bumps [socket.io](https://github.com/socketio/socket.io) from 2.3.0 to 2.4.1.
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/2.4.1/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/2.3.0...2.4.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-21 15:09:14 +00:00
GeorgeSG c3fa2ce6b6 1.1.1 2020-11-22 16:38:06 +02:00
GeorgeSG f4128145fb Fix name and seconds being linked together in Visual Editor 2020-11-22 16:37:48 +02:00
GeorgeSG e8d87e9afb 1.1.0 2020-11-22 13:47:02 +02:00
GeorgeSG e56dc41dcc Fix import 2020-11-22 13:41:48 +02:00
GeorgeSG 4b5fc79630 Add embedded layout option 2020-11-22 13:40:19 +02:00
GeorgeSG 6113ec9415 Add seconds config to visual editor 2020-11-22 13:23:20 +02:00
GeorgeSG f306d95748 Add optional seconds picker 2020-11-22 13:11:16 +02:00
GeorgeSG fe91b79320 Bump dependencies 2020-11-22 12:35:54 +02:00
GeorgeSG 0b1b87ed6e Merge pull request #8 from GeorgeSG/dependabot/npm_and_yarn/http-proxy-1.18.1
Bump http-proxy from 1.18.0 to 1.18.1
2020-09-12 14:02:34 +03:00
dependabot[bot] 81a0f1a95a Bump http-proxy from 1.18.0 to 1.18.1
Bumps [http-proxy](https://github.com/http-party/node-http-proxy) from 1.18.0 to 1.18.1.
- [Release notes](https://github.com/http-party/node-http-proxy/releases)
- [Changelog](https://github.com/http-party/node-http-proxy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/http-party/node-http-proxy/compare/1.18.0...1.18.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-11 22:23:04 +00:00
GeorgeSG 526a52f630 1.0.1 2020-07-17 20:24:39 +03:00
GeorgeSG 72d82d7c0a Merge pull request #7 from GeorgeSG/dependabot/npm_and_yarn/lodash-4.17.19
Bump lodash from 4.17.15 to 4.17.19
2020-07-17 20:24:26 +03:00
dependabot[bot] 21f66a9066 Bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-17 17:23:17 +00:00
GeorgeSG fe93d385e9 1.0.0 2020-07-17 20:22:38 +03:00
GeorgeSG 05e0952624 Implements #5: Add link_values option 2020-07-17 20:20:42 +03:00
GeorgeSG 3c10a9ab44 Fixes #6: Inherit border-radius from theme. Add variable for border radius 2020-07-17 19:09:34 +03:00
GeorgeSG 9a2a57af5c Update README 2020-05-18 22:22:32 +03:00
GeorgeSG 74949a3df2 0.1.1 2020-05-10 00:22:45 +03:00
GeorgeSG 224ee3b043 Remove commit activity from README 2020-05-10 00:18:38 +03:00
GeorgeSG 2e604a66d6 Update README 2020-05-10 00:17:43 +03:00
GeorgeSG b7ad71d979 Rename CI task. Don't lint twice 2020-05-10 00:15:20 +03:00
GeorgeSG 79418592c9 Fix 12AM representation. Add model tests 2020-05-10 00:13:29 +03:00
GeorgeSG 0297c9aba4 Update README 2020-05-09 20:42:35 +03:00
GeorgeSG 588cead35d 0.1.0 2020-05-09 20:39:14 +03:00
GeorgeSG 04462e096c Update README 2020-05-09 20:38:29 +03:00
GeorgeSG 03312aa5f7 Add new screenshot and reorganize README. Add code examples 2020-05-09 20:37:23 +03:00
GeorgeSG 4488b303d2 Add name position option. Refactor types 2020-05-09 20:24:37 +03:00
GeorgeSG 491e571aab Add layout.align option 2020-05-09 19:39:36 +03:00
GeorgeSG ae7dfb7668 Add test step in ci 2020-05-09 16:20:11 +03:00
GeorgeSG cc22b482b4 Add some tests 2020-05-09 16:17:53 +03:00
GeorgeSG 812e7f3016 Bump custom-card-helpers. Refactor types 2020-05-09 13:40:26 +03:00
GeorgeSG 20cba88ef2 Update README - add full URLs to images 2020-05-09 13:17:44 +03:00
GeorgeSG 5b6c9d0af0 Update README - add downloads shield 2020-05-09 12:58:27 +03:00
GeorgeSG 993fda7d34 Update README - HACS default 2020-05-09 12:50:11 +03:00
GeorgeSG df9de8bd00 Refactor editor datetimeEntities 2020-05-08 00:58:58 +03:00
GeorgeSG a7b98e31a4 0.0.5 2020-05-08 00:35:47 +03:00
GeorgeSG a92bb7915c Add Visual Editor support 2020-05-08 00:34:45 +03:00
GeorgeSG 6258b55518 Update README 2020-05-07 22:02:46 +03:00
GeorgeSG b4663377a4 Update screenshots 2020-05-07 21:05:55 +03:00
GeorgeSG 1dc6152986 Update README 2020-05-07 20:53:03 +03:00
GeorgeSG 8e11f71b48 0.0.4 2020-05-07 20:37:44 +03:00
GeorgeSG c646fc9c9e Update default colors and variables 2020-05-07 20:37:33 +03:00
GeorgeSG 2791d8109c 0.0.3 2020-05-07 19:55:59 +03:00
GeorgeSG 14cc1a2c10 Add option to configure the AM/PM layout 2020-05-07 19:55:45 +03:00
GeorgeSG 76a52f0c9f Add some comments 2020-05-07 01:52:24 +03:00
GeorgeSG ce22ceb808 Refactor constants 2020-05-07 01:45:31 +03:00
GeorgeSG 45d3364959 Use ENTITY_DOMAIN in callService 2020-05-07 01:40:29 +03:00
GeorgeSG 6ac3d62823 Use computeDomain 2020-05-07 01:38:05 +03:00
37 changed files with 12273 additions and 1388 deletions
+3 -2
View File
@@ -13,7 +13,8 @@
"rules": {
"@typescript-eslint/camelcase": 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"]
}
+3 -2
View File
@@ -9,12 +9,13 @@ on:
- master
jobs:
build:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build
run: |
npm install
npm run lint
npm run build
- name: Test
run: npm run test
+4
View File
@@ -1,2 +1,6 @@
node_modules/
dist/
devtools/homeassistant/*
!devtools/homeassistant/configuration.yaml
!devtools/homeassistant/ui-lovelace.yaml
+8
View File
@@ -0,0 +1,8 @@
{
"files.exclude": {
"dist/**": true
},
"files.associations": {
"*.yaml": "home-assistant"
}
}
+142 -56
View File
@@ -1,29 +1,33 @@
# Time Picker Card by [@georgesg](https://www.github.com/georgesg)
# Time Picker Card
[![hacs_badge][hacs-shield]][hacs]
[![GitHub Release][releases-shield]][releases]
![CI](https://github.com/GeorgeSG/lovelace-time-picker-card/workflows/CI/badge.svg)
[![Project Maintenance][maintenance-shield]][maintenance]
[![GitHub Activity][commits-shield]][commits]
[![License][license-shield]][license]
[![HACS][hacs-shield]][hacs-link]
[![Downloads][downloads-shield]][downloads-link]
[![GitHub Release][releases-shield]][releases-link]
[![CI][ci-shield]][ci-link]
[![Project Maintenance][maintenance-shield]][maintenance-link]
[![License][license-shield]][license-link]
## 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`).
## Screenshots
![Default theme with card name](examples/default_with_name.png)
![Default theme with no card name](examples/default_without_name.png)
![Cudtom theme ](examples/custom.png)
## 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
resources:
@@ -33,61 +37,143 @@ resources:
## Usage
### Visual Editor
Time Picker Card supports Lovelace's Visual Editor. Click the + button to add a card and search for time picker.
![Visual Editor](https://raw.githubusercontent.com/GeorgeSG/lovelace-time-picker-card/master/examples/visual_editor.png)
## Examples
### Default config - card name shown, 24 hour mode
![Default theme with card name](https://raw.githubusercontent.com/GeorgeSG/lovelace-time-picker-card/master/examples/default_with_name.png)
```yaml
type: 'custom:time-picker-card'
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
![Default theme with no card name](https://raw.githubusercontent.com/GeorgeSG/lovelace-time-picker-card/master/examples/default_without_name.png)
```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
![Default theme with single hour mode](https://raw.githubusercontent.com/GeorgeSG/lovelace-time-picker-card/master/examples/single_hour_mode.png)
```yaml
type: 'custom:time-picker-card'
entity: input_datetime.alarm_time
hour_mode: 12
layout:
hour_mode: single
align_controls: right
hide:
name: true
```
### Custom config - card name inside card and controls aligned right
![Default theme with single hour mode](https://raw.githubusercontent.com/GeorgeSG/lovelace-time-picker-card/master/examples/name_inside.png)
```yaml
type: 'custom:time-picker-card'
entity: input_datetime.alarm_time
layout:
name: inside
align_controls: center
```
### Dark theme, embedded layout
![Dark theme embedded](https://raw.githubusercontent.com/GeorgeSG/lovelace-time-picker-card/master/examples/dark_embedded.png)
### Dark theme, thin layout
![Dark theme thin layout](https://raw.githubusercontent.com/GeorgeSG/lovelace-time-picker-card/master/examples/dark_thin.png)
### With a custom lovelace theme
![Custom theme](https://raw.githubusercontent.com/GeorgeSG/lovelace-time-picker-card/master/examples/custom.png)
## Options
| Name | Type | Requirement | Description | Default |
| ----------- | ------------ | ------------ | --------------------------------------------------------------------------------------------------------- | ------------------------ |
| 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` | |
| name | string | **Optional** | Card name | Entity's `friendly_name` |
| 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` |
| minute_step | number | **Optional** | Minute change when clicking arrows | `5` |
| hide | object | **Optional** | Hide object | `none` |
| Name | Type | Requirement | Description | Default |
| ----------------- | ------------ | ------------ | --------------------------------------------------------------------------------------------------------- | ------------------------ |
| 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` | |
| 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_step | number | **Optional** | Hour change when clicking arrows | `1` |
| minute_step | number | **Optional** | Minute change when clicking arrows | `5` |
| layout | object | **Optional** | Card Layout configuration | `none` |
| hide | object | **Optional** | Hide object | `none` |
| tap_action | action | **Optional** | Home assistant action to perform on tap | `more-info` |
| double_tap_action | action | **Optional** | Home assistant action to perform on tap | `more-info` |
| hold_action | action | **Optional** | Home assistant action to perform on tap | `more-info` |
## Hide Object
### Layout Object
| Name | Type | Requirement | Description | Default |
| ---- | ------- | ------------ | ------------------- | ------- |
| name | boolean | **Optional** | Hides the card name | `false` |
| Name | Value | Requirement | Description | Default |
| -------------- | ------------------------- | ------------ | -------------------------------------------------------------------------------------------------- | -------- |
| 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` |
| thin | boolean | **Optional** | Render with reduced paddings | `false` |
## Theme Variables
### Hide Object
| Name | Default | Description |
| ------------------------------- | --------------------------- | ------------------------------------------- |
| --tpc-elements-background-color | `var(--dark-primary-color)` | Background color for header and ui elements |
| --tpc-icon-color | `var(--primary-text-color)` | Arrow color |
| --tpc-text-color | `white` | Text color |
| --tpc-accent-color | `var(--accent-color)` | AM / PM active color |
| Name | Type | Requirement | Description | Default |
| ------- | ------- | ------------ | ------------------------------------------------- | ------- |
| name | boolean | **Optional** | Hides the card name | `false` |
| icon | boolean | **Optional** | Hides the card icon (only with controls "inside") | `false` |
| seconds | boolean | **Optional** | Hides seconds input | `true` |
### 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 |
| time-picker-control-padding | `8px` | Padding for interactive elements. Increase for larger hitboxes of the controls. Example: `10px 12px` |
## Meta
**Georgi Gardev**
- [gar.dev](https://gar.dev)
- [![GitHub][github-icon]](https://github.com/GeorgeSG/) [GeorgeSG](https://github.com/GeorgeSG/)
- [![Twitter][twitter-icon]](https://twitter.com/georgesg92) [@georgesg92](https://twitter.com/georgesg92)
- [![GitHub][github-icon]][github-link] [GeorgeSG][github-link]
- [![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
[commits]: https://github.com/GeorgeSG/lovelace-time-picker-card/commits/master
[license-shield]: https://img.shields.io/github/license/GeorgeSG/lovelace-time-picker-card?style=flat-square
[license]: https://github.com/GeorgeSG/lovelace-time-picker-card/blob/master/LICENSE
[maintenance-shield]: https://img.shields.io/maintenance/yes/2020.svg?style=flat-square
[maintenance]: https://github.com/GeorgeSG/lovelace-time-picker-card
[releases-shield]: https://img.shields.io/github/release/GeorgeSG/lovelace-time-picker-card.svg?style=flat-square
[releases]: https://github.com/GeorgeSG/lovelace-time-picker-card/releases
[hacs-shield]: https://img.shields.io/badge/HACS-Custom-orange.svg?style=flat-square
[hacs]: https://github.com/custom-components/hacs
[twitter-icon]: http://i.imgur.com/wWzX9uB.png
[hacs-shield]: https://img.shields.io/badge/HACS-Default-brightgreen.svg
[hacs-link]: https://github.com/hacs/integration
[downloads-shield]: https://img.shields.io/github/downloads/GeorgeSG/lovelace-time-picker-card/latest/total?color=brightgreen&logo=github
[downloads-link]: https://github.com/GeorgeSG/lovelace-time-picker-card/releases
[releases-shield]: https://img.shields.io/github/release/GeorgeSG/lovelace-time-picker-card.svg
[releases-link]: https://github.com/GeorgeSG/lovelace-time-picker-card/releases
[ci-shield]: https://img.shields.io/github/workflow/status/GeorgeSG/lovelace-time-picker-card/CI?label=CI&logo=github&
[ci-link]: https://github.com/GeorgeSG/lovelace-time-picker-card/actions?query=workflow%3ACI
[maintenance-shield]: https://img.shields.io/maintenance/yes/2022.svg
[maintenance-link]: https://github.com/GeorgeSG/lovelace-time-picker-card
[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-link]: https://github.com/GeorgeSG/
[twitter-icon]: http://i.imgur.com/wWzX9uB.png
[twitter-link]: https://twitter.com/georgesg92
+27
View File
@@ -0,0 +1,27 @@
# Configure a default setup of Home Assistant (frontend, api, etc)
default_config:
homeassistant:
packages: !include_dir_named packages/
logger:
default: info
logs:
hacs: debug
lovelace:
mode: yaml
resources:
- type: module
url: /hacsfiles/button-card/button-card.js
- type: module
url: /hacsfiles/lovelace-state-switch/state-switch.js
- type: module
url: http://localhost:5000/time-picker-card.js
dashboards:
lovelace-home:
mode: yaml
title: Home
icon: mdi:home
show_in_sidebar: true
filename: ui-lovelace.yaml
+147
View File
@@ -0,0 +1,147 @@
title: Home
views:
- title: dev
path: dev
cards:
- type: vertical-stack
cards:
- type: markdown
content: "# 🕰 Time Picker Card dev"
- type: entities
entities:
- input_datetime.test_date_time
- type: horizontal-stack
cards:
- type: custom:time-picker-card
entity: input_datetime.test_date_time
name: With tap actions
hour_mode: 24
hour_step: 2
tap_action:
action: call-service
service: logbook.log
service_data:
name: "Time picker card · Tap Action"
message: "Debug"
double_tap_action:
action: call-service
service: logbook.log
service_data:
name: "Time picker card · Double Tap Action"
message: "Debug"
hold_action:
action: call-service
service: logbook.log
service_data:
name: "Time picker card · Hold action"
message: "Debug"
- type: custom:time-picker-card
name: Hour mode 12, thin, single, hidden name
entity: input_datetime.test_date_time
hour_mode: 12
layout:
hour_mode: single
thin: true
hide:
name: true
- type: custom:time-picker-card
entity: input_datetime.test_date_time
name: Thin
layout:
thin: true
- type: custom:time-picker-card
entity: input_datetime.test_date_time
name: Hidden seconds
hide:
seconds: false
- type: horizontal-stack
cards:
- type: custom:time-picker-card
entity: input_datetime.test_date_time
name: Hidden name
hour_mode: 12
hide:
name: true
- type: custom:time-picker-card
entity: input_datetime.test_date_time
name: Linked, single hour mode, hidden name
link_values: true
hour_mode: 12
hour_step: 2
minute_step: 3
layout:
hour_mode: single
hide:
name: false
- type: custom:time-picker-card
entity: input_datetime.test_date
name: Error
- title: name inside
path: name-inside
cards:
- type: custom:time-picker-card
entity: input_datetime.test_date_time
name: Inside, controls right
hour_mode: 12
layout:
name: inside
align_controls: right
- type: horizontal-stack
cards:
- type: custom:time-picker-card
entity: input_datetime.test_date_time
name: Inside, hidden name
layout:
name: inside
hide:
name: true
- type: custom:time-picker-card
entity: input_datetime.test_date_time
name: Inside, hidden icon
layout:
name: inside
hide:
icon: true
- title: embedded
path: embedded
cards:
- type: vertical-stack
cards:
- type: entities
entities:
- input_datetime.test_date_time
- type: custom:time-picker-card
entity: input_datetime.test_date_time
name: Embedded thin
hour_mode: 24
hour_step: 2
layout:
embedded: true
thin: true
- type: custom:time-picker-card
entity: input_datetime.test_date_time
name: Inside
hour_mode: 24
hour_step: 2
layout:
name: inside
- type: custom:time-picker-card
entity: input_datetime.test_date_time
hour_mode: 12
layout:
hour_mode: single
hide:
name: true
- type: custom:time-picker-card
entity: input_datetime.test_date_time
hour_mode: 12
layout:
hour_mode: single
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

+8
View File
@@ -0,0 +1,8 @@
module.exports = {
preset: 'ts-jest/presets/js-with-babel',
testRegex: '/test/.*\\.test?\\.ts$',
transformIgnorePatterns: [
'node_modules/(?!(testing-library__dom|@open-wc|lit-html|lit-element|pure-lit|lit-element-state-decoupler)/)',
],
moduleFileExtensions: ['ts', 'js'],
};
+10618 -1163
View File
File diff suppressed because it is too large Load Diff
+23 -23
View File
@@ -1,6 +1,6 @@
{
"name": "time-picker-card",
"version": "0.0.2",
"version": "1.3.0",
"author": "Georgi Gardev <georgi@gardev.com>",
"description": "Lovelace time-picker-card",
"keywords": [
@@ -15,35 +15,35 @@
"license": "MIT",
"repository": "git@github.com:GeorgeSG/lovelace-time-picker-card.git",
"scripts": {
"start": "rollup -c --watch",
"start": "rollup -c --watch --bundleConfigAsCjs",
"build": "npm run lint && npm run rollup",
"lint": "eslint src/*.ts",
"rollup": "rollup -c",
"rollup": "rollup -c --bundleConfigAsCjs",
"test": "jest",
"release-patch": "npm version patch && 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",
"start:ha": "docker run --rm -p8124:8123 -v $(pwd)/devtools/homeassistant:/config homeassistant/home-assistant:latest"
},
"dependencies": {
"custom-card-helpers": "^1.5.0",
"home-assistant-js-websocket": "^5.1.0",
"lit-element": "^2.3.1",
"lit-html": "^1.2.1"
"custom-card-helpers": "^1.9.0",
"home-assistant-js-websocket": "^8.0.0",
"lit": "^2.4.0"
},
"devDependencies": {
"@babel/core": "^7.9.0",
"@rollup/plugin-json": "^4.0.3",
"@rollup/plugin-node-resolve": "^7.1.1",
"@rollup/plugin-typescript": "^4.0.0",
"@typescript-eslint/eslint-plugin": "^2.30.0",
"@typescript-eslint/parser": "^2.30.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.3",
"prettier": "^2.0.5",
"rollup": "^1.32.1",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-serve": "^1.0.1",
"rollup-plugin-terser": "^5.3.0",
"typescript": "^3.8.3"
"@el3um4s/rollup-plugin-terser": "^1.0.2",
"@rollup/plugin-babel": "^6.0.2",
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-json": "^5.0.1",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-typescript": "^9.0.2",
"@types/jest": "^29.2.1",
"eslint": "^8.26.0",
"jest": "^29.2.2",
"prettier": "^2.7.1",
"rollup": "^3.2.5",
"rollup-plugin-serve": "^2.0.1",
"ts-jest": "^29.0.3",
"typescript": "^4.8.4"
}
}
+4 -4
View File
@@ -1,15 +1,15 @@
import nodeResolve from '@rollup/plugin-node-resolve';
import json from '@rollup/plugin-json';
import typescript from '@rollup/plugin-typescript';
import { terser } from 'rollup-plugin-terser';
import babel from 'rollup-plugin-babel';
import { terser } from '@el3um4s/rollup-plugin-terser';
import babel from '@rollup/plugin-babel';
import serve from 'rollup-plugin-serve';
const dev = process.env.ROLLUP_WATCH;
const serveOptions = {
contentBase: ['./dist'],
host: '0.0.0.0',
host: 'localhost',
port: 5000,
allowCrossOrigin: true,
headers: {
@@ -24,9 +24,9 @@ export default {
format: 'es',
},
plugins: [
typescript({ exclude: ['test/**/*'] }),
nodeResolve(),
json(),
typescript(),
babel({
exclude: 'node_modules/**',
}),
+204
View File
@@ -0,0 +1,204 @@
import { noChange } from 'lit';
import { AttributePart, directive, Directive, DirectiveParameters } from 'lit/directive.js';
import { fireEvent } from 'custom-card-helpers';
import { ActionHandlerDetail, ActionHandlerOptions } from 'custom-card-helpers/dist/types';
const isTouch =
'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.maxTouchPoints > 0;
interface ActionHandler extends HTMLElement {
holdTime: number;
bind(element: Element, options): void;
}
interface ActionHandlerElement extends HTMLElement {
actionHandler?: boolean;
}
declare global {
interface HASSDomEvents {
action: ActionHandlerDetail;
}
}
class ActionHandler extends HTMLElement implements ActionHandler {
public holdTime = 500;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public ripple: any;
protected timer?: number;
protected held = false;
private dblClickTimeout?: number;
constructor() {
super();
this.ripple = document.createElement('mwc-ripple');
}
public connectedCallback(): void {
Object.assign(this.style, {
position: 'absolute',
width: isTouch ? '100px' : '50px',
height: isTouch ? '100px' : '50px',
transform: 'translate(-50%, -50%)',
pointerEvents: 'none',
zIndex: '999',
});
this.appendChild(this.ripple);
this.ripple.primary = true;
['touchcancel', 'mouseout', 'mouseup', 'touchmove', 'mousewheel', 'wheel', 'scroll'].forEach(
(ev) => {
document.addEventListener(
ev,
() => {
clearTimeout(this.timer);
this.stopAnimation();
this.timer = undefined;
},
{ passive: true }
);
}
);
}
public bind(element: ActionHandlerElement, options): void {
if (element.actionHandler) {
return;
}
element.actionHandler = true;
element.addEventListener('contextmenu', (ev: Event) => {
const e = ev || window.event;
if (e.preventDefault) {
e.preventDefault();
}
if (e.stopPropagation) {
e.stopPropagation();
}
e.cancelBubble = true;
e.returnValue = false;
return false;
});
const start = (ev: Event): void => {
this.held = false;
let x;
let y;
if ((ev as TouchEvent).touches) {
x = (ev as TouchEvent).touches[0].pageX;
y = (ev as TouchEvent).touches[0].pageY;
} else {
x = (ev as MouseEvent).pageX;
y = (ev as MouseEvent).pageY;
}
this.timer = window.setTimeout(() => {
this.startAnimation(x, y);
this.held = true;
}, this.holdTime);
};
const end = (ev: Event): void => {
// Prevent mouse event if touch event
ev.preventDefault();
if (['touchend', 'touchcancel'].includes(ev.type) && this.timer === undefined) {
return;
}
clearTimeout(this.timer);
this.stopAnimation();
this.timer = undefined;
if (this.held) {
fireEvent(element, 'action', { action: 'hold' });
} else if (options.hasDoubleClick) {
if ((ev.type === 'click' && (ev as MouseEvent).detail < 2) || !this.dblClickTimeout) {
this.dblClickTimeout = window.setTimeout(() => {
this.dblClickTimeout = undefined;
fireEvent(element, 'action', { action: 'tap' });
}, 250);
} else {
clearTimeout(this.dblClickTimeout);
this.dblClickTimeout = undefined;
fireEvent(element, 'action', { action: 'double_tap' });
}
} else {
fireEvent(element, 'action', { action: 'tap' });
}
};
const handleEnter = (ev: KeyboardEvent): void => {
if (ev.keyCode !== 13) {
return;
}
end(ev);
};
element.addEventListener('touchstart', start, { passive: true });
element.addEventListener('touchend', end);
element.addEventListener('touchcancel', end);
element.addEventListener('mousedown', start, { passive: true });
element.addEventListener('click', end);
element.addEventListener('keyup', handleEnter);
}
private startAnimation(x: number, y: number): void {
Object.assign(this.style, {
left: `${x}px`,
top: `${y}px`,
display: null,
});
this.ripple.disabled = false;
this.ripple.active = true;
this.ripple.unbounded = true;
}
private stopAnimation(): void {
this.ripple.active = false;
this.ripple.disabled = true;
this.style.display = 'none';
}
}
// TODO You need to replace all instances of "action-handler-boilerplate" with "action-handler-<your card name>"
customElements.define('action-handler-boilerplate', ActionHandler);
const getActionHandler = (): ActionHandler => {
const body = document.body;
if (body.querySelector('action-handler-boilerplate')) {
return body.querySelector('action-handler-boilerplate') as ActionHandler;
}
const actionhandler = document.createElement('action-handler-boilerplate');
body.appendChild(actionhandler);
return actionhandler as ActionHandler;
};
export const actionHandlerBind = (
element: ActionHandlerElement,
options?: ActionHandlerOptions
): void => {
const actionhandler: ActionHandler = getActionHandler();
if (!actionhandler) {
return;
}
actionhandler.bind(element, options);
};
export const actionHandler = directive(
class extends Directive {
update(part: AttributePart, [options]: DirectiveParameters<this>) {
actionHandlerBind(part.element as ActionHandlerElement, options);
return noChange;
}
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
render(_options?: ActionHandlerOptions) {}
}
);
+28 -20
View File
@@ -1,20 +1,18 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from 'lit-element';
import { ClassInfo, classMap } from 'lit-html/directives/class-map';
import { Period } from '../types';
import { css, CSSResult, html, LitElement, TemplateResult } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { ClassInfo, classMap } from 'lit/directives/class-map.js';
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')
export class TimePeriodComponent extends LitElement {
static readonly EVENT_TOGGLE = 'toggle';
@property() private period!: Period;
@property() private mode!: Layout.HourMode;
private get amClass(): ClassInfo {
return { 'time-period': true, active: this.period === Period.AM };
@@ -26,12 +24,7 @@ export class TimePeriodComponent extends LitElement {
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>
${this.mode === 'single' ? this.renderSingle() : this.renderDouble()}
</div>`;
}
@@ -40,6 +33,21 @@ export class TimePeriodComponent extends LitElement {
this.dispatchEvent(event);
}
private renderSingle(): TemplateResult {
return html`<div class="time-period active" @click=${this.onTimePeriodChange}>
${this.period}<mwc-ripple></mwc-ripple>
</div>`;
}
private renderDouble(): TemplateResult {
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 {
return css`
.time-period-selector {
@@ -48,9 +56,9 @@ export class TimePeriodComponent extends LitElement {
.time-period {
width: 30px;
padding: 8px;
background: var(--tpc-elements-background-color);
color: var(--tpc-text-color, #fff);
padding: var(--tpc-control-padding);
background: var(--tpc-off-color);
color: var(--tpc-text-color);
text-align: center;
font-size: 1em;
cursor: pointer;
+12 -19
View File
@@ -1,18 +1,15 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from 'lit-element';
import { LitElement, TemplateResult, html, CSSResult, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { TimeUnit } from '../models/time-unit';
import { Direction } from '../types';
/**
* Renders an AM/PM picker.
*/
@customElement('time-unit')
export class TimeUnitComponent extends LitElement {
static readonly EVENT_UPDATE = 'update';
static readonly EVENT_STEP_CHANGE = 'stepChange';
@property() private unit!: TimeUnit;
@@ -24,7 +21,7 @@ export class TimeUnitComponent extends LitElement {
class="time-input"
type="number"
placeholder="MM"
min="0"
min=${this.unit.minValue}
max=${this.unit.maxValue}
.value="${this.unit.toString()}"
@change=${this.onInputChange}
@@ -36,16 +33,12 @@ export class TimeUnitComponent extends LitElement {
onInputChange({ target: { value } }: { target: HTMLInputElement }): void {
this.unit.setStringValue(value);
this.emitUpdate();
const event = new CustomEvent(TimeUnitComponent.EVENT_UPDATE);
this.dispatchEvent(event);
}
onStepChangerClick(direction: Direction): void {
this.unit.stepUpdate(direction);
this.emitUpdate();
}
private emitUpdate(): void {
const event = new CustomEvent(TimeUnitComponent.EVENT_UPDATE);
const event = new CustomEvent(TimeUnitComponent.EVENT_STEP_CHANGE, { detail: { direction } });
this.dispatchEvent(event);
}
@@ -69,7 +62,7 @@ export class TimeUnitComponent extends LitElement {
.time-picker-icon {
width: 30px;
padding: 8px;
padding: var(--tpc-control-padding);
text-align: center;
cursor: pointer;
color: var(--tpc-icon-color);
@@ -77,7 +70,7 @@ export class TimeUnitComponent extends LitElement {
.time-input {
width: 30px;
padding: 8px 8px 6px;
padding: var(--tpc-control-padding);
background: var(--tpc-elements-background-color);
border: 0;
border-bottom: 2px solid var(--tpc-elements-background-color);
+12
View File
@@ -1,4 +1,16 @@
import * as pkg from '../package.json';
import { Layout } from './types';
export const CARD_VERSION = pkg.version;
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_HOUR_MODE = 24;
export const DEFAULT_LAYOUT_HOUR_MODE: Layout.HourMode = 'double';
export const DEFAULT_LAYOUT_ALIGN_CONTROLS = Layout.AlignControls.CENTER;
export const DEFAULT_LAYOUT_NAME = Layout.Name.HEADER;
+158
View File
@@ -0,0 +1,158 @@
import { HomeAssistant, LovelaceCardEditor } from 'custom-card-helpers';
import { html, LitElement, TemplateResult } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { TimePickerCardConfig } from './types';
const NAME_TO_LABEL_MAP = {
entity: 'input_datetime entity id',
name: 'Name',
hour_step: 'Hour step',
minute_step: 'Minute step',
hour_mode: 'Hour mode',
link_values: 'Link values',
align_controls: 'Align controls',
embedded: 'Embedded?',
thin: 'Thin layout?',
icon: 'Icon',
seconds: 'Seconds',
};
const SCHEMA = [
{ name: 'entity', selector: { entity: { domain: 'input_datetime' } } },
{
name: 'name',
selector: { text: {} },
},
{
type: 'grid',
schema: [
{
name: 'hour_step',
type: 'integer',
required: true,
default: 1,
valueMin: 1,
valueMax: 24,
},
{
name: 'minute_step',
type: 'integer',
required: true,
default: 5,
valueMin: 1,
valueMax: 60,
},
{
name: 'hour_mode',
type: 'select',
options: [
[12, '12'],
[24, '24'],
],
},
{ name: 'link_values', type: 'boolean' },
],
},
{
type: 'expandable',
name: 'layout',
title: 'Layout controls',
schema: [
{
name: 'hour_mode',
type: 'select',
options: [
['single', 'single'],
['double', 'double'],
],
},
{
name: 'align_controls',
type: 'select',
options: [
['left', 'left'],
['center', 'center'],
['right', 'right'],
],
},
{
name: 'name',
type: 'select',
options: [
['header', 'header'],
['inside', 'inside'],
],
},
{ name: 'embedded', type: 'boolean' },
{ name: 'thin', type: 'boolean' },
],
},
{
type: 'expandable',
name: 'hide',
title: 'Hide controls',
schema: [
{
type: 'grid',
name: '',
schema: [
{ name: 'name', type: 'boolean' },
{ name: 'icon', type: 'boolean' },
{ name: 'seconds', type: 'boolean' },
],
},
],
},
{
type: 'expandable',
title: 'Actions',
schema: [
{ name: 'tap_action', selector: { action: {} } },
{ name: 'double_tap_action', selector: { action: {} } },
{ name: 'hold_action', selector: { action: {} } },
],
},
];
@customElement('time-picker-card-editor')
export class TimePickerCardEditor extends LitElement implements LovelaceCardEditor {
private static readonly CONFIG_CHANGED_EVENT = 'config-changed';
@property({ type: Object }) hass!: HomeAssistant;
@property() private config!: TimePickerCardConfig;
private computeLabel({ name }) {
return NAME_TO_LABEL_MAP[name] || name;
}
private valueChanged(ev: CustomEvent): void {
const newConfig = { ...this.config, ...ev.detail.value };
this.dispatch(newConfig);
}
render(): TemplateResult {
return html`
<ha-form
.hass=${this.hass}
.data=${this.config}
.schema=${SCHEMA}
.computeLabel=${this.computeLabel}
@value-changed=${this.valueChanged}
></ha-form>
`;
}
setConfig(config): void {
this.config = config;
}
private dispatch(config: TimePickerCardConfig): void {
const event = new CustomEvent(TimePickerCardEditor.CONFIG_CHANGED_EVENT, {
bubbles: true,
composed: true,
detail: { config },
});
this.dispatchEvent(event);
}
}
+21 -14
View File
@@ -1,16 +1,23 @@
import { TimeUnit } from './time-unit';
import { DEFAULT_HOUR_STEP } from '../const';
import { HourMode } from '../types';
import { TimeUnit } from './time-unit';
/**
* Represents the hour value of a datetime.
*/
export class Hour extends TimeUnit {
private static readonly DEFAULT_STEP = 1;
private static readonly MAX = 24;
private static readonly VALUE_LIMIT = 24;
constructor(value: number, step = Hour.DEFAULT_STEP, private hourMode: HourMode) {
super(value, step, Hour.MAX);
constructor(value: number, step = DEFAULT_HOUR_STEP, private hourMode?: HourMode) {
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 {
return this.hourMode || Hour.MAX;
return this.hourMode === 12 ? 12 : Hour.VALUE_LIMIT - 1;
}
togglePeriod(): void {
@@ -18,15 +25,15 @@ export class Hour extends TimeUnit {
}
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();
}
protected isValidString(valueStr: string): boolean {
const value = parseInt(valueStr);
const limit = this.hourMode || this._limit;
return !isNaN(value) && value >= 0 && value <= limit;
}
}
+17 -8
View File
@@ -1,17 +1,26 @@
import { DEFAULT_MINUTE_STEP } from '../const';
import { TimeUnit } from './time-unit';
import { Direction } from '../types';
/**
* * Represents the minute value of a datetime.
*/
export class Minute extends TimeUnit {
private static readonly DEFAULT_STEP = 5;
private static readonly MAX = 60;
private static readonly VALUE_LIMIT = 60;
maxValue = Minute.MAX;
minValue = 0;
maxValue = Minute.VALUE_LIMIT - 1;
constructor(value: number, step = Minute.DEFAULT_STEP) {
super(value, step, Minute.MAX);
constructor(value: number, step = DEFAULT_MINUTE_STEP) {
super(value, step, Minute.VALUE_LIMIT);
}
protected isValidString(valueStr: string): boolean {
const value = parseInt(valueStr);
return !isNaN(value) && value >= 0 && value <= this._limit;
/**
* Returns true if the minutes 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;
}
}
+26
View File
@@ -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;
}
}
+33 -14
View File
@@ -1,13 +1,15 @@
import { Direction } from '../types';
/**
* Represents a single unit of a datetime oobject - e.g. hours or minutes.
*/
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;
@@ -23,33 +25,50 @@ export abstract class TimeUnit {
return this._value;
}
/**
* Sets a value from {@param stringValue}, if it can be parsed and is valid.
* @param stringValue
*/
setStringValue(stringValue: string): void {
if (this.isValidString(stringValue)) {
this.setValue(parseInt(stringValue));
}
}
stepUpdate(direction: Direction): void {
direction === Direction.UP ? this.increment() : this.decrement();
}
increment(): void {
this.setValue(this.value + this._step);
}
decrement(): void {
this.setValue(this.value - this._step);
/**
* Updates the value in {@param direction} by the step size specified in the constructoor.
* @param direction
*/
stepUpdate(direction: Direction, step: number = this._step): void {
direction === Direction.UP ? this.increment(step) : this.decrement(step);
}
toString(): string {
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 {
if (isNaN(newValue)) {
return;
}
if (newValue >= this._limit || newValue < 0) {
newValue = (newValue + this._limit) % this._limit;
}
this._value = newValue;
}
protected isValidString(valueStr: string): boolean {
const value = parseInt(valueStr);
return !isNaN(value) && value >= 0 && value < this._limit;
}
}
+37
View File
@@ -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}`;
}
}
+2 -10
View File
@@ -1,6 +1,6 @@
import { html, TemplateResult } from 'lit-element';
import { TimePickerCardConfig } from './types';
import { LovelaceCard } from 'custom-card-helpers';
import { html, TemplateResult } from 'lit';
import { TimePickerCardConfig } from './types';
export class Partial {
static error(error: string, origConfig: TimePickerCardConfig): TemplateResult {
@@ -13,12 +13,4 @@ export class Partial {
return html`${errorCard}`;
}
static header(title: string, icon?: string): TemplateResult | null {
return html`
<div class="time-picker-header">
${icon ? html`<ha-icon class="icon" icon=${icon}></ha-icon>` : ''}${title}
</div>
`;
}
}
+284 -46
View File
@@ -1,21 +1,36 @@
import { HomeAssistant } from 'custom-card-helpers';
import { HassEntity } from 'home-assistant-js-websocket';
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from 'lit-element';
ActionHandlerEvent,
computeDomain,
handleAction,
hasAction,
HomeAssistant,
LovelaceCard,
} from 'custom-card-helpers';
import { HassEntity } from 'home-assistant-js-websocket';
import { css, CSSResult, html, LitElement, TemplateResult } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { ClassInfo, classMap } from 'lit/directives/class-map.js';
import { actionHandler } from './action-handler-directive';
import './components/time-period.component';
import './components/time-unit.component';
import { CARD_SIZE, CARD_VERSION } from './const';
import {
CARD_SIZE,
CARD_VERSION,
DEFAULT_HOUR_MODE,
DEFAULT_HOUR_STEP,
DEFAULT_LAYOUT_ALIGN_CONTROLS,
DEFAULT_LAYOUT_HOUR_MODE,
DEFAULT_LAYOUT_NAME,
DEFAULT_MINUTE_STEP,
ENTITY_DOMAIN,
} from './const';
import './editor';
import { Hour } from './models/hour';
import { Minute } from './models/minute';
import { Second } from './models/second';
import { Time } from './models/time';
import { Partial } from './partials';
import { Period, TimePickerCardConfig } from './types';
import { Layout, Period, TimePickerCardConfig, TimePickerHideConfig } from './types';
console.info(
`%c TIME-PICKER-CARD \n%c Version ${CARD_VERSION} `,
@@ -23,20 +38,42 @@ console.info(
'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')
export class TimePickerCard extends LitElement {
@property() private hass!: HomeAssistant;
export class TimePickerCard extends LitElement implements LovelaceCard {
@property({ type: Object }) hass!: HomeAssistant;
@property() private config!: TimePickerCardConfig;
@property() private hour!: Hour;
@property() private minute!: Minute;
@property() private time!: Time;
@property() private period!: Period;
private get entity(): HassEntity | undefined {
return this.hass.states[this.config.entity];
}
private get shouldShowName(): boolean {
return Boolean(this.config.hide?.name) === false && Boolean(this.name);
private get isEmbedded(): boolean {
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) &&
(this.config.layout?.name === Layout.Name.INSIDE || Boolean(this.config.layout?.embedded))
);
}
private get name(): string | undefined {
@@ -47,41 +84,143 @@ export class TimePickerCard extends LitElement {
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,
thin: this.config.layout?.thin === true,
};
}
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,
};
}
private handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass, this.config, ev.detail.action!);
}
private renderHeaderName(title: string): TemplateResult {
return html`<div
class="time-picker-header"
@action=${this.handleAction}
.actionHandler="${actionHandler({
hasHold: hasAction(this.config.hold_action),
hasDoubleClick: hasAction(this.config.double_tap_action),
})}"
>
${title}
</div>`;
}
private renderNestedName(
name: string,
entity: HassEntity,
hide?: TimePickerHideConfig
): TemplateResult {
const icon = html`<state-badge
class="entity-icon"
.stateObj=${entity}
@action=${this.handleAction}
.actionHandler="${actionHandler({
hasHold: hasAction(this.config.hold_action),
hasDoubleClick: hasAction(this.config.double_tap_action),
})}"
></state-badge>`;
const label = html`<div
class="entity-name-inside"
@action=${this.handleAction}
.actionHandler="${actionHandler({
hasHold: hasAction(this.config!.hold_action),
hasDoubleClick: hasAction(this.config.double_tap_action),
})}"
>
${name}
</div>`;
const visibleElements = [
{ element: icon, visible: !hide?.icon },
{ element: label, visible: !hide?.name },
]
.filter(({ visible }) => visible)
.map(({ element }) => element);
return html`${visibleElements}`;
}
render(): TemplateResult | null {
if (!this.entity) {
return Partial.error('Entity not found', this.config);
}
if (!this.entity.entity_id.startsWith('input_datetime')) {
return Partial.error('You must set an input_datetime entity', this.config);
if (computeDomain(this.entity.entity_id) !== ENTITY_DOMAIN) {
return Partial.error(`You must set an ${ENTITY_DOMAIN} entity`, this.config);
}
if (!this.entity.attributes.has_time) {
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
);
}
const { hour, minute } = this.entity!.attributes;
this.hour = new Hour(hour, this.config.hour_step, this.config.hour_mode);
this.minute = new Minute(minute, this.config.minute_step);
this.period = this.hour.value >= 12 ? Period.PM : Period.AM;
const { hour, minute, second } = this.entity!.attributes;
const hourInstance = new Hour(hour, this.config.hour_step, this.config.hour_mode);
const minuteInstance = new Minute(minute, this.config.minute_step);
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`
<ha-card>
${this.shouldShowName ? Partial.header(this.name!) : ''}
<div class="time-picker-content">
<time-unit .unit=${this.hour} @update=${this.callHassService}></time-unit>
<div class="time-separator">:</div>
<time-unit .unit=${this.minute} @update=${this.callHassService}></time-unit>
${this.shouldShowPeriod
? html`<time-period
.period=${this.period}
@toggle=${this.onPeriodToggle}
></time-period>`
<ha-card class=${classMap(this.haCardClass)}>
${this.hasNameInHeader ? this.renderHeaderName(this.name!) : ''}
<div class=${classMap(this.rowClass)}>
${this.hasNameInside
? this.renderNestedName(this.name!, this.entity, this.config.hide)
: ''}
<div class=${classMap(this.contentClass)}>
<time-unit
.unit=${this.time.hour}
@stepChange=${this.onHourStepChange}
@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>
</ha-card>
`;
@@ -108,7 +247,22 @@ export class TimePickerCard extends LitElement {
}
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();
}
@@ -117,11 +271,9 @@ export class TimePickerCard extends LitElement {
throw new Error('Unable to update datetime');
}
const time = `${this.hour.value}:${this.minute.value}:00`;
return this.hass.callService('input_datetime', 'set_datetime', {
return this.hass.callService(ENTITY_DOMAIN, 'set_datetime', {
entity_id: this.entity!.entity_id,
time,
time: this.time.value,
});
}
@@ -130,29 +282,115 @@ export class TimePickerCard extends LitElement {
:host {
--tpc-elements-background-color: var(
--time-picker-elements-background-color,
var(--dark-primary-color)
var(--primary-color)
);
--tpc-control-padding: var(--time-picker-control-padding, 8px);
--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));
--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 {
overflow: auto;
}
ha-card.embedded {
box-shadow: none;
border: none;
background: transparent;
}
.time-picker-header {
padding: 16px;
color: var(--tpc-text-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;
text-align: center;
}
.time-picker-content {
padding: 8px 16px 16px;
.thin > .time-picker-header {
padding: 4px;
}
.time-picker-row {
display: flex;
flex-direction: row;
align-items: center;
padding: 16px;
}
.thin .time-picker-row {
padding: 0 !important ;
}
.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;
}
.time-picker-content.layout-right {
justify-content: flex-end;
}
.entity-icon {
cursor: pointer;
}
.entity-name-inside {
margin-left: 16px;
cursor: pointer;
}
`;
}
static getStubConfig(
_: HomeAssistant,
entities: Array<string>
): Omit<TimePickerCardConfig, 'type'> {
const datetimeEntity = entities.find((entityId) => computeDomain(entityId) === ENTITY_DOMAIN);
return {
entity: datetimeEntity || '',
hour_mode: DEFAULT_HOUR_MODE,
hour_step: DEFAULT_HOUR_STEP,
minute_step: DEFAULT_MINUTE_STEP,
layout: {
hour_mode: DEFAULT_LAYOUT_HOUR_MODE,
align_controls: DEFAULT_LAYOUT_ALIGN_CONTROLS,
name: DEFAULT_LAYOUT_NAME,
},
hide: {
seconds: true,
},
};
}
static getConfigElement(): LovelaceCard {
return document.createElement('time-picker-card-editor') as LovelaceCard;
}
}
+37 -6
View File
@@ -1,16 +1,49 @@
import { LovelaceCardConfig } from 'custom-card-helpers';
import { ActionConfig, LovelaceCardConfig } from 'custom-card-helpers';
export interface TimePickerCardConfig extends LovelaceCardConfig {
entity: string;
name?: string;
link_values?: boolean;
hour_mode?: HourMode;
hour_step?: number;
minute_step?: number;
second_step?: number;
layout?: TimePickerLayoutConfig;
hide?: TimePickerHideConfig;
tap_action?: ActionConfig;
double_tap_action?: ActionConfig;
hold_action?: ActionConfig;
}
export type HourMode = 12 | 24 | undefined;
export interface TimePickerLayoutConfig {
align_controls?: Layout.AlignControls;
name?: Layout.Name;
hour_mode?: Layout.HourMode;
embedded?: boolean;
thin?: 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 {
name: boolean;
name?: boolean;
icon?: boolean;
seconds?: boolean;
}
export enum Direction {
@@ -19,8 +52,6 @@ export enum Direction {
}
export enum Period {
AM = 'am',
PM = 'pm',
AM = 'AM',
PM = 'PM',
}
export type HourMode = 12 | 24 | undefined;
+12
View File
@@ -0,0 +1,12 @@
export interface CustomCardEntry {
type: string;
name?: string;
description?: string;
preview?: boolean;
}
declare global {
interface Window {
customCards: Array<CustomCardEntry>;
}
}
+165
View File
@@ -0,0 +1,165 @@
import { Hour } from '../../src/models/hour';
import { Direction } from '../../src/types';
import { DEFAULT_HOUR_STEP } from '../../src/const';
describe('Hour', () => {
let hour: Hour;
describe('with default config', () => {
beforeEach(() => {
hour = new Hour(2);
});
it('returns value', () => {
expect(hour.value).toEqual(2);
});
it('has correct max value', () => {
expect(hour.maxValue).toEqual(23);
});
describe('toString', () => {
it('returns formatted string', () => {
expect(hour.toString()).toEqual('02');
});
it('renders values correctly', () => {
hour = new Hour(0);
expect(hour.toString()).toEqual('00');
hour = new Hour(6);
expect(hour.toString()).toEqual('06');
hour = new Hour(12);
expect(hour.toString()).toEqual('12');
hour = new Hour(18);
expect(hour.toString()).toEqual('18');
hour = new Hour(23);
expect(hour.toString()).toEqual('23');
});
});
describe('stepUpdate', () => {
it('updates up', () => {
hour.stepUpdate(Direction.UP);
expect(hour.value).toEqual(3);
});
it('updates down', () => {
hour.stepUpdate(Direction.DOWN);
expect(hour.value).toEqual(1);
});
it('goes up from 23 to 0', () => {
hour = new Hour(23);
hour.stepUpdate(Direction.UP);
expect(hour.value).toEqual(0);
});
it('goes down from 0 to 23', () => {
hour = new Hour(0);
hour.stepUpdate(Direction.DOWN);
expect(hour.value).toEqual(23);
});
});
describe('setStringValue', () => {
it('sets value from valid string', () => {
hour.setStringValue('3');
expect(hour.value).toEqual(3);
});
it("doesn't set value from invalid string", () => {
hour.setStringValue('test');
expect(hour.value).toEqual(2);
});
it("doesn't set value from outside of scope", () => {
hour.setStringValue('24');
expect(hour.value).toEqual(2);
});
});
});
describe('with a different step size', () => {
beforeEach(() => {
hour = new Hour(2, 5);
});
it('goes up correctly', () => {
hour.stepUpdate(Direction.UP);
expect(hour.value).toEqual(7);
hour.stepUpdate(Direction.UP);
hour.stepUpdate(Direction.UP);
hour.stepUpdate(Direction.UP);
hour.stepUpdate(Direction.UP);
expect(hour.value).toEqual(3);
});
it('goes down correclty', () => {
hour.stepUpdate(Direction.DOWN);
expect(hour.value).toEqual(21);
});
});
describe('in 12 hour mode', () => {
beforeEach(() => {
hour = new Hour(2, DEFAULT_HOUR_STEP, 12);
});
it('has correct max value', () => {
expect(hour.maxValue).toEqual(12);
});
describe('togglePeriod', () => {
it('changes values correclty', () => {
hour = new Hour(2, DEFAULT_HOUR_STEP, 12);
hour.togglePeriod();
expect(hour.value).toEqual(14);
hour = new Hour(0, DEFAULT_HOUR_STEP, 12);
hour.togglePeriod();
expect(hour.value).toEqual(12);
hour = new Hour(23, DEFAULT_HOUR_STEP, 12);
hour.togglePeriod();
expect(hour.value).toEqual(11);
});
it('has no side effects', () => {
const value = hour.value;
hour.togglePeriod();
hour.togglePeriod();
expect(hour.value).toEqual(value);
});
});
describe('toString', () => {
it('renders values above 12 orrectly', () => {
hour = new Hour(13, DEFAULT_HOUR_STEP, 12);
expect(hour.toString()).toEqual('01');
});
it('renders values correctly', () => {
hour = new Hour(0, DEFAULT_HOUR_STEP, 12);
expect(hour.toString()).toEqual('12');
hour = new Hour(6, DEFAULT_HOUR_STEP, 12);
expect(hour.toString()).toEqual('06');
hour = new Hour(12, DEFAULT_HOUR_STEP, 12);
expect(hour.toString()).toEqual('12');
hour = new Hour(18, DEFAULT_HOUR_STEP, 12);
expect(hour.toString()).toEqual('06');
hour = new Hour(23, DEFAULT_HOUR_STEP, 12);
expect(hour.toString()).toEqual('11');
});
});
});
});
+118
View File
@@ -0,0 +1,118 @@
import { Minute } from '../../src/models/minute';
import { Direction } from '../../src/types';
describe('Minute', () => {
let minute: Minute;
describe('with default config', () => {
beforeEach(() => {
minute = new Minute(2);
});
it('returns value', () => {
expect(minute.value).toEqual(2);
});
it('has correct max value', () => {
expect(minute.maxValue).toEqual(59);
});
describe('toString', () => {
it('renders values correctly', () => {
minute = new Minute(0);
expect(minute.toString()).toEqual('00');
minute = new Minute(5);
expect(minute.toString()).toEqual('05');
minute = new Minute(59);
expect(minute.toString()).toEqual('59');
});
});
describe('stepUpdate', () => {
it('updates up', () => {
minute.stepUpdate(Direction.UP);
expect(minute.value).toEqual(7);
});
it('updates down', () => {
minute.stepUpdate(Direction.DOWN);
expect(minute.value).toEqual(57);
});
it('goes up from 59 to 4', () => {
minute = new Minute(59);
minute.stepUpdate(Direction.UP);
expect(minute.value).toEqual(4);
});
it('goes down from 0 to 55', () => {
minute = new Minute(0);
minute.stepUpdate(Direction.DOWN);
expect(minute.value).toEqual(55);
});
});
describe('setStringValue', () => {
it('sets value from valid string', () => {
minute.setStringValue('3');
expect(minute.value).toEqual(3);
});
it("doesn't set value from invalid string", () => {
minute.setStringValue('test');
expect(minute.value).toEqual(2);
});
it("doesn't set value from outside of scope", () => {
minute.setStringValue('60');
expect(minute.value).toEqual(2);
minute.setStringValue('65');
expect(minute.value).toEqual(2);
});
});
});
describe('with a different step size', () => {
beforeEach(() => {
minute = new Minute(2, 15);
});
it('goes up correctly', () => {
minute.stepUpdate(Direction.UP);
expect(minute.value).toEqual(17);
minute.stepUpdate(Direction.UP);
minute.stepUpdate(Direction.UP);
minute.stepUpdate(Direction.UP);
expect(minute.value).toEqual(2);
minute.stepUpdate(Direction.UP);
expect(minute.value).toEqual(17);
});
it('goes down correclty', () => {
minute.stepUpdate(Direction.DOWN);
expect(minute.value).toEqual(47);
});
});
describe('will overflow', () => {
it('returns true if the minutes will overflow an hour up', () => {
minute.setStringValue('59');
expect(minute.willOverflow(Direction.UP)).toBe(true);
});
it('returns true if the minutes will overflow an hour down', () => {
minute.setStringValue('0');
expect(minute.willOverflow(Direction.DOWN)).toBe(true);
});
it('returns true if the minutes will not overflow', () => {
minute.setStringValue('55');
expect(minute.willOverflow(Direction.DOWN)).toBe(false);
});
});
});
+118
View File
@@ -0,0 +1,118 @@
import { Second } from '../../src/models/second';
import { Direction } from '../../src/types';
describe('Second', () => {
let second: Second;
describe('with default config', () => {
beforeEach(() => {
second = new Second(2);
});
it('returns value', () => {
expect(second.value).toEqual(2);
});
it('has correct max value', () => {
expect(second.maxValue).toEqual(59);
});
describe('toString', () => {
it('renders values correctly', () => {
second = new Second(0);
expect(second.toString()).toEqual('00');
second = new Second(5);
expect(second.toString()).toEqual('05');
second = new Second(59);
expect(second.toString()).toEqual('59');
});
});
describe('stepUpdate', () => {
it('updates up', () => {
second.stepUpdate(Direction.UP);
expect(second.value).toEqual(7);
});
it('updates down', () => {
second.stepUpdate(Direction.DOWN);
expect(second.value).toEqual(57);
});
it('goes up from 59 to 4', () => {
second = new Second(59);
second.stepUpdate(Direction.UP);
expect(second.value).toEqual(4);
});
it('goes down from 0 to 55', () => {
second = new Second(0);
second.stepUpdate(Direction.DOWN);
expect(second.value).toEqual(55);
});
});
describe('setStringValue', () => {
it('sets value from valid string', () => {
second.setStringValue('3');
expect(second.value).toEqual(3);
});
it("doesn't set value from invalid string", () => {
second.setStringValue('test');
expect(second.value).toEqual(2);
});
it("doesn't set value from outside of scope", () => {
second.setStringValue('60');
expect(second.value).toEqual(2);
second.setStringValue('65');
expect(second.value).toEqual(2);
});
});
});
describe('with a different step size', () => {
beforeEach(() => {
second = new Second(2, 15);
});
it('goes up correctly', () => {
second.stepUpdate(Direction.UP);
expect(second.value).toEqual(17);
second.stepUpdate(Direction.UP);
second.stepUpdate(Direction.UP);
second.stepUpdate(Direction.UP);
expect(second.value).toEqual(2);
second.stepUpdate(Direction.UP);
expect(second.value).toEqual(17);
});
it('goes down correclty', () => {
second.stepUpdate(Direction.DOWN);
expect(second.value).toEqual(47);
});
});
describe('will overflow', () => {
it('returns true if the Seconds will overflow an hour up', () => {
second.setStringValue('59');
expect(second.willOverflow(Direction.UP)).toBe(true);
});
it('returns true if the Seconds will overflow an hour down', () => {
second.setStringValue('0');
expect(second.willOverflow(Direction.DOWN)).toBe(true);
});
it('returns true if the Seconds will not overflow', () => {
second.setStringValue('55');
expect(second.willOverflow(Direction.DOWN)).toBe(false);
});
});
});
+2 -1
View File
@@ -12,6 +12,7 @@
"noImplicitAny": false,
"skipLibCheck": true,
"resolveJsonModule": true,
"experimentalDecorators": true
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true // for importing sinon in tests
}
}