Compare commits

...
13 Commits
Author SHA1 Message Date
Georgi Gardev 9c6880377b 1.3.1 2022-11-03 19:24:09 +02:00
Georgi Gardev 0ba02f605c Fix lint issues 2022-11-03 18:40:06 +02:00
Georgi Gardev 8b07d1be6d Bring eslint-plugin back 2022-11-03 18:34:21 +02:00
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
30 changed files with 11947 additions and 4269 deletions
+1 -3
View File
@@ -2,8 +2,6 @@
"parser": "@typescript-eslint/parser", // Specifies the ESLint parser "parser": "@typescript-eslint/parser", // Specifies the ESLint parser
"extends": [ "extends": [
"plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin "plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
"prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
"plugin:prettier/recommended" // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
], ],
"parserOptions": { "parserOptions": {
"ecmaVersion": 2018, // Allows for the parsing of modern ECMAScript features "ecmaVersion": 2018, // Allows for the parsing of modern ECMAScript features
@@ -14,7 +12,7 @@
"@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 "@typescript-eslint/no-namespace": 0,
}, },
"ignorePatterns": ["dist/", "node_modules/", "*.js"] "ignorePatterns": ["dist/", "node_modules/", "*.js"]
} }
+4
View File
@@ -1,2 +1,6 @@
node_modules/ node_modules/
dist/ dist/
devtools/homeassistant/*
!devtools/homeassistant/configuration.yaml
!devtools/homeassistant/ui-lovelace.yaml
+3
View File
@@ -1,5 +1,8 @@
{ {
"files.exclude": { "files.exclude": {
"dist/**": true "dist/**": true
},
"files.associations": {
"*.yaml": "home-assistant"
} }
} }
+27 -16
View File
@@ -76,6 +76,7 @@ entity: input_datetime.alarm_time
hour_mode: 12 hour_mode: 12
layout: layout:
hour_mode: single hour_mode: single
align_controls: right
hide: hide:
name: true name: true
``` ```
@@ -89,26 +90,35 @@ type: 'custom:time-picker-card'
entity: input_datetime.alarm_time entity: input_datetime.alarm_time
layout: layout:
name: inside name: inside
align_controls: right 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 ### With a custom lovelace theme
![Custom theme](https://raw.githubusercontent.com/GeorgeSG/lovelace-time-picker-card/master/examples/custom.png) ![Custom theme](https://raw.githubusercontent.com/GeorgeSG/lovelace-time-picker-card/master/examples/custom.png)
## Options ## Options
| Name | Type | Requirement | Description | Default | | Name | Type | Requirement | Description | Default |
| ----------- | ------------ | ------------ | --------------------------------------------------------------------------------------------------------- | ------------------------ | | ----------------- | ------------ | ------------ | --------------------------------------------------------------------------------------------------------- | ------------------------ |
| 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` | | 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` | | layout | object | **Optional** | Card Layout configuration | `none` |
| hide | object | **Optional** | Hide object | `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` |
### Layout Object ### Layout Object
@@ -118,14 +128,15 @@ layout:
| align_controls | `left`, `center`, `right` | **Optional** | Horizontal alignment of the controls | `center` | | 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` | | 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` | | embedded | boolean | **Optional** | Render with embedded style - disables padding, box shadow, and card header | `false` |
| thin | boolean | **Optional** | Render with reduced paddings | `false` |
### Hide Object ### Hide Object
| Name | Type | Requirement | Description | Default | | Name | Type | Requirement | Description | Default |
| ------- | ------- | ------------ | ------------------- | ------- | | ------- | ------- | ------------ | ------------------------------------------------- | ------- |
| name | boolean | **Optional** | Hides the card name | `false` | | name | boolean | **Optional** | Hides the card name | `false` |
| icon | boolean | **Optional** | Hides the card icon (only with controls "inside") | `false` | | icon | boolean | **Optional** | Hides the card icon (only with controls "inside") | `false` |
| seconds | boolean | **Optional** | Hides seconds input | `true` | | seconds | boolean | **Optional** | Hides seconds input | `true` |
### Theme Variables ### Theme Variables
+97 -33
View File
@@ -1,37 +1,5 @@
title: Home title: Home
views: views:
- title: Grid
path: grid
cards:
- type: grid
cards:
- type: button
icon: mdi:reload
name: Theme
tap_action:
action: call-service
service: frontend.reload_themes
- type: custom:time-picker-card
entity: input_datetime.test_date_time
link_values: true
hour_mode: 12
hour_step: 2
minute_step: 3
layout:
hour_mode: single
name: inside
hide:
name: false
- type: entities
entities:
- input_datetime.test_date_time
- input_datetime.test_date_time
- input_datetime.test_date_time
- input_datetime.test_date_time
- input_datetime.test_date_time
- input_datetime.test_date_time
- title: dev - title: dev
path: dev path: dev
cards: cards:
@@ -46,21 +14,45 @@ views:
cards: cards:
- type: custom:time-picker-card - type: custom:time-picker-card
entity: input_datetime.test_date_time entity: input_datetime.test_date_time
name: Alarm time name: With tap actions
hour_mode: 24 hour_mode: 24
hour_step: 2 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 - type: custom:time-picker-card
name: Hour mode 12, thin, single, hidden name
entity: input_datetime.test_date_time entity: input_datetime.test_date_time
hour_mode: 12 hour_mode: 12
layout: layout:
hour_mode: single hour_mode: single
thin: true
hide: hide:
name: true name: true
- type: custom:time-picker-card - type: custom:time-picker-card
entity: input_datetime.test_date_time entity: input_datetime.test_date_time
name: Thin
layout:
thin: true
- type: custom:time-picker-card - type: custom:time-picker-card
entity: input_datetime.test_date_time entity: input_datetime.test_date_time
name: Hidden seconds
hide: hide:
seconds: false seconds: false
@@ -68,11 +60,13 @@ views:
cards: cards:
- type: custom:time-picker-card - type: custom:time-picker-card
entity: input_datetime.test_date_time entity: input_datetime.test_date_time
name: Hidden name
hour_mode: 12 hour_mode: 12
hide: hide:
name: true name: true
- type: custom:time-picker-card - type: custom:time-picker-card
entity: input_datetime.test_date_time entity: input_datetime.test_date_time
name: Linked, single hour mode, hidden name
link_values: true link_values: true
hour_mode: 12 hour_mode: 12
hour_step: 2 hour_step: 2
@@ -81,3 +75,73 @@ views:
hour_mode: single hour_mode: single
hide: hide:
name: false 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.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

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'],
};
+11255 -3595
View File
File diff suppressed because it is too large Load Diff
+23 -34
View File
@@ -1,6 +1,6 @@
{ {
"name": "time-picker-card", "name": "time-picker-card",
"version": "1.2.1", "version": "1.3.1",
"author": "Georgi Gardev <georgi@gardev.com>", "author": "Georgi Gardev <georgi@gardev.com>",
"description": "Lovelace time-picker-card", "description": "Lovelace time-picker-card",
"keywords": [ "keywords": [
@@ -15,47 +15,36 @@
"license": "MIT", "license": "MIT",
"repository": "git@github.com:GeorgeSG/lovelace-time-picker-card.git", "repository": "git@github.com:GeorgeSG/lovelace-time-picker-card.git",
"scripts": { "scripts": {
"start": "rollup -c --watch", "start": "rollup -c --watch --bundleConfigAsCjs",
"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 --bundleConfigAsCjs",
"test": "./node_modules/karma/bin/karma start ./test/karma.conf.js", "test": "jest",
"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",
"start:ha": "docker run --rm -p8124:8123 -v $(pwd)/devtools/homeassistant:/config homeassistant/home-assistant:latest", "start:ha": "docker run --rm -p8124:8123 -v $(pwd)/devtools/homeassistant:/config homeassistant/home-assistant:latest"
}, },
"dependencies": { "dependencies": {
"custom-card-helpers": "^1.6.6", "custom-card-helpers": "^1.9.0",
"home-assistant-js-websocket": "^5.7.0", "home-assistant-js-websocket": "^8.0.0",
"lit-element": "^2.4.0", "lit": "^2.4.0"
"lit-html": "^1.3.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.12.7", "@el3um4s/rollup-plugin-terser": "^1.0.2",
"@open-wc/testing": "^2.5.32", "@rollup/plugin-babel": "^6.0.2",
"@rollup/plugin-commonjs": "^11.1.0", "@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^5.0.1",
"@rollup/plugin-node-resolve": "^7.1.3", "@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-typescript": "^4.1.2", "@rollup/plugin-typescript": "^9.0.2",
"@types/chai": "^4.2.14", "@types/jest": "^29.2.1",
"@typescript-eslint/eslint-plugin": "^2.34.0", "@typescript-eslint/eslint-plugin": "^5.42.0",
"@typescript-eslint/parser": "^2.34.0", "eslint": "^8.26.0",
"eslint": "^6.8.0", "jest": "^29.2.2",
"eslint-config-prettier": "^6.15.0", "prettier": "^2.7.1",
"eslint-plugin-prettier": "^3.1.4", "rollup": "^3.2.5",
"karma": "^6.3.16", "rollup-plugin-serve": "^2.0.1",
"karma-chai": "^0.1.0", "ts-jest": "^29.0.3",
"karma-chrome-launcher": "^3.1.0", "typescript": "^4.8.4"
"karma-mocha": "^2.0.1",
"karma-rollup-preprocessor": "^7.0.5",
"mocha": "^7.2.0",
"prettier": "^2.2.0",
"rollup": "^1.32.1",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-serve": "^1.1.0",
"rollup-plugin-terser": "^5.3.1",
"sinon": "^9.2.1",
"typescript": "^3.9.7"
} }
} }
+4 -4
View File
@@ -1,15 +1,15 @@
import nodeResolve from '@rollup/plugin-node-resolve'; import nodeResolve from '@rollup/plugin-node-resolve';
import json from '@rollup/plugin-json'; import json from '@rollup/plugin-json';
import typescript from '@rollup/plugin-typescript'; import typescript from '@rollup/plugin-typescript';
import { terser } from 'rollup-plugin-terser'; import { terser } from '@el3um4s/rollup-plugin-terser';
import babel from 'rollup-plugin-babel'; import babel from '@rollup/plugin-babel';
import serve from 'rollup-plugin-serve'; import serve from 'rollup-plugin-serve';
const dev = process.env.ROLLUP_WATCH; const dev = process.env.ROLLUP_WATCH;
const serveOptions = { const serveOptions = {
contentBase: ['./dist'], contentBase: ['./dist'],
host: '0.0.0.0', host: 'localhost',
port: 5000, port: 5000,
allowCrossOrigin: true, allowCrossOrigin: true,
headers: { headers: {
@@ -24,9 +24,9 @@ export default {
format: 'es', format: 'es',
}, },
plugins: [ plugins: [
typescript({ exclude: ['test/**/*'] }),
nodeResolve(), nodeResolve(),
json(), json(),
typescript(),
babel({ babel({
exclude: 'node_modules/**', exclude: 'node_modules/**',
}), }),
+205
View File
@@ -0,0 +1,205 @@
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 {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
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, @typescript-eslint/explicit-function-return-type
render(_options?: ActionHandlerOptions) {}
}
);
+3 -10
View File
@@ -1,13 +1,6 @@
import { import { css, CSSResult, html, LitElement, TemplateResult } from 'lit';
css, import { customElement, property } from 'lit/decorators.js';
CSSResult, import { ClassInfo, classMap } from 'lit/directives/class-map.js';
customElement,
html,
LitElement,
property,
TemplateResult,
} from 'lit-element';
import { ClassInfo, classMap } from 'lit-html/directives/class-map';
import { Layout, Period } from '../types'; import { Layout, Period } from '../types';
/** /**
+2 -9
View File
@@ -1,12 +1,5 @@
import { import { LitElement, TemplateResult, html, CSSResult, css } from 'lit';
css, import { customElement, property } from 'lit/decorators.js';
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from 'lit-element';
import { TimeUnit } from '../models/time-unit'; import { TimeUnit } from '../models/time-unit';
import { Direction } from '../types'; import { Direction } from '../types';
+2 -1
View File
@@ -10,6 +10,7 @@ export const ENTITY_DOMAIN = 'input_datetime';
export const DEFAULT_HOUR_STEP = 1; export const DEFAULT_HOUR_STEP = 1;
export const DEFAULT_MINUTE_STEP = 5; export const DEFAULT_MINUTE_STEP = 5;
export const DEFAULT_SECOND_STEP = 5; export const DEFAULT_SECOND_STEP = 5;
export const DEFAULT_LAYOUT_HOUR_MODE = 'double'; 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_ALIGN_CONTROLS = Layout.AlignControls.CENTER;
export const DEFAULT_LAYOUT_NAME = Layout.Name.HEADER; export const DEFAULT_LAYOUT_NAME = Layout.Name.HEADER;
+129 -269
View File
@@ -1,272 +1,151 @@
import { computeDomain, HomeAssistant, LovelaceCardEditor } from 'custom-card-helpers'; import { HomeAssistant, LovelaceCardEditor } from 'custom-card-helpers';
import { HassEntity } from 'home-assistant-js-websocket'; import { html, LitElement, TemplateResult } from 'lit';
import { import { customElement, property } from 'lit/decorators.js';
css, import { TimePickerCardConfig } from './types';
CSSResult,
customElement, const NAME_TO_LABEL_MAP = {
html, entity: 'input_datetime entity id',
LitElement, name: 'Name',
property, hour_step: 'Hour step',
TemplateResult, minute_step: 'Minute step',
} from 'lit-element'; hour_mode: 'Hour mode',
import { link_values: 'Link values',
DEFAULT_HOUR_STEP, align_controls: 'Align controls',
DEFAULT_MINUTE_STEP, embedded: 'Embedded?',
ENTITY_DOMAIN, thin: 'Thin layout?',
DEFAULT_LAYOUT_ALIGN_CONTROLS, icon: 'Icon',
DEFAULT_LAYOUT_NAME, seconds: 'Seconds',
DEFAULT_SECOND_STEP, };
} from './const';
import { TimePickerCardConfig, Layout, HourMode } from './types'; 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') @customElement('time-picker-card-editor')
export class TimePickerCardEditor extends LitElement implements LovelaceCardEditor { 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'; private static readonly CONFIG_CHANGED_EVENT = 'config-changed';
@property({ type: Object }) hass!: HomeAssistant; @property({ type: Object }) hass!: HomeAssistant;
@property() private config!: TimePickerCardConfig; @property() private config!: TimePickerCardConfig;
private get entity(): HassEntity | undefined { private computeLabel({ name }): string {
return this.hass.states[this.config.entity]; return NAME_TO_LABEL_MAP[name] || name;
} }
private get datetimeEntities(): Array<string> { private valueChanged(ev: CustomEvent): void {
return Object.keys(this.hass.states).filter( const newConfig = { ...this.config, ...ev.detail.value };
(entityId) => this.dispatch(newConfig);
computeDomain(entityId) === ENTITY_DOMAIN &&
this.hass.states[entityId].attributes.has_time === true
);
} }
render(): TemplateResult { render(): TemplateResult {
return html`<div class="card-config"> return html`
<paper-dropdown-menu <ha-form
style="width: 100%" .hass=${this.hass}
label="Entity (Required)" .data=${this.config}
.configValue=${'entity'} .schema=${SCHEMA}
@value-changed=${this.onValueChange} .computeLabel=${this.computeLabel}
> @value-changed=${this.valueChanged}
<paper-listbox ></ha-form>
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>
<div>
<ha-switch
style="margin-left: 10px"
.checked="${!Boolean(this.config.hide?.icon)}"
@change="${this.onHideIconChange}"
></ha-switch>
Show icon?
</div>
</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 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 { setConfig(config): void {
this.config = config; 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 hide = { ...this.config.hide, name: !checked };
const newConfig = { ...this.config, hide };
this.dispatch(newConfig);
}
private onHideIconChange({ target: { checked } }): void {
const hide = { ...this.config.hide, icon: !checked };
const newConfig = { ...this.config, hide };
this.dispatch(newConfig);
}
private onHideSecondsChange({ target: { checked } }): void {
const hide = { ...this.config.hide, seconds: !checked };
const newConfig = { ...this.config, hide };
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 { private dispatch(config: TimePickerCardConfig): void {
const event = new CustomEvent(TimePickerCardEditor.CONFIG_CHANGED_EVENT, { const event = new CustomEvent(TimePickerCardEditor.CONFIG_CHANGED_EVENT, {
bubbles: true, bubbles: true,
@@ -276,23 +155,4 @@ export class TimePickerCardEditor extends LitElement implements LovelaceCardEdit
this.dispatchEvent(event); 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;
}
`;
}
} }
+2 -23
View File
@@ -1,7 +1,6 @@
import { html, TemplateResult } from 'lit-element';
import { TimePickerCardConfig, TimePickerHideConfig } from './types';
import { LovelaceCard } from 'custom-card-helpers'; import { LovelaceCard } from 'custom-card-helpers';
import { HassEntity } from 'home-assistant-js-websocket'; import { html, TemplateResult } from 'lit';
import { TimePickerCardConfig } from './types';
export class Partial { export class Partial {
static error(error: string, origConfig: TimePickerCardConfig): TemplateResult { static error(error: string, origConfig: TimePickerCardConfig): TemplateResult {
@@ -14,24 +13,4 @@ export class Partial {
return html`${errorCard}`; return html`${errorCard}`;
} }
static headerName(title: string): TemplateResult {
return html`<div class="time-picker-header">${title}</div>`;
}
static nestedName(name: string, entity: HassEntity, hide?: TimePickerHideConfig): TemplateResult {
const icon = html`<state-badge .stateObj=${entity}></state-badge>`;
const label = html`<div class="entity-name-inside">${name}</div>`;
const visibleElements = [
{ show: !hide?.icon, value: icon },
{ show: !hide?.name, value: label },
]
.filter(({ show }) => show)
.map(({ value }) => value);
return html`${visibleElements}`;
// return hideIcon ? label : html`${icon}${label}`;
}
} }
+101 -19
View File
@@ -1,22 +1,27 @@
import { computeDomain, HomeAssistant, LovelaceCard } from 'custom-card-helpers';
import { HassEntity } from 'home-assistant-js-websocket';
import { import {
css, ActionHandlerEvent,
CSSResult, computeDomain,
customElement, handleAction,
html, hasAction,
LitElement, HomeAssistant,
property, LovelaceCard,
TemplateResult, } from 'custom-card-helpers';
} from 'lit-element'; import { HassEntity } from 'home-assistant-js-websocket';
import { ClassInfo, classMap } from 'lit-html/directives/class-map'; 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-period.component';
import './components/time-unit.component'; import './components/time-unit.component';
import { import {
CARD_SIZE, CARD_SIZE,
CARD_VERSION, CARD_VERSION,
DEFAULT_HOUR_MODE,
DEFAULT_HOUR_STEP,
DEFAULT_LAYOUT_ALIGN_CONTROLS, DEFAULT_LAYOUT_ALIGN_CONTROLS,
DEFAULT_LAYOUT_HOUR_MODE, DEFAULT_LAYOUT_HOUR_MODE,
DEFAULT_LAYOUT_NAME,
DEFAULT_MINUTE_STEP,
ENTITY_DOMAIN, ENTITY_DOMAIN,
} from './const'; } from './const';
import './editor'; import './editor';
@@ -25,7 +30,7 @@ import { Minute } from './models/minute';
import { Second } from './models/second'; import { Second } from './models/second';
import { Time } from './models/time'; import { Time } from './models/time';
import { Partial } from './partials'; import { Partial } from './partials';
import { Layout, Period, TimePickerCardConfig } from './types'; import { Layout, Period, TimePickerCardConfig, TimePickerHideConfig } from './types';
console.info( console.info(
`%c TIME-PICKER-CARD \n%c Version ${CARD_VERSION} `, `%c TIME-PICKER-CARD \n%c Version ${CARD_VERSION} `,
@@ -86,6 +91,7 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
private get haCardClass(): ClassInfo { private get haCardClass(): ClassInfo {
return { return {
embedded: this.isEmbedded, embedded: this.isEmbedded,
thin: this.config.layout?.thin === true,
}; };
} }
@@ -104,6 +110,58 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
}; };
} }
private handleAction(ev: ActionHandlerEvent): void {
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 { 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);
@@ -129,9 +187,11 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
return html` return html`
<ha-card class=${classMap(this.haCardClass)}> <ha-card class=${classMap(this.haCardClass)}>
${this.hasNameInHeader ? Partial.headerName(this.name!) : ''} ${this.hasNameInHeader ? this.renderHeaderName(this.name!) : ''}
<div class=${classMap(this.rowClass)}> <div class=${classMap(this.rowClass)}>
${this.hasNameInside ? Partial.nestedName(this.name!, this.entity, this.config.hide) : ''} ${this.hasNameInside
? this.renderNestedName(this.name!, this.entity, this.config.hide)
: ''}
<div class=${classMap(this.contentClass)}> <div class=${classMap(this.contentClass)}>
<time-unit <time-unit
@@ -237,11 +297,12 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
ha-card { ha-card {
overflow: auto; overflow: auto;
border: none;
} }
ha-card.embedded { ha-card.embedded {
box-shadow: none; box-shadow: none;
border: none;
background: transparent;
} }
.time-picker-header { .time-picker-header {
@@ -254,6 +315,10 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
text-align: center; text-align: center;
} }
.thin > .time-picker-header {
padding: 4px;
}
.time-picker-row { .time-picker-row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -261,6 +326,10 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
padding: 16px; padding: 16px;
} }
.thin .time-picker-row {
padding: 0 !important ;
}
.time-picker-row.embedded { .time-picker-row.embedded {
padding: 0; padding: 0;
} }
@@ -288,8 +357,13 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
justify-content: flex-end; justify-content: flex-end;
} }
.entity-icon {
cursor: pointer;
}
.entity-name-inside { .entity-name-inside {
margin-left: 16px; margin-left: 16px;
cursor: pointer;
} }
`; `;
} }
@@ -301,10 +375,18 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
const datetimeEntity = entities.find((entityId) => computeDomain(entityId) === ENTITY_DOMAIN); const datetimeEntity = entities.find((entityId) => computeDomain(entityId) === ENTITY_DOMAIN);
return { return {
entity: datetimeEntity || 'input_datetime.example_entity', entity: datetimeEntity || '',
hour_mode: 24, hour_mode: DEFAULT_HOUR_MODE,
hour_step: 1, hour_step: DEFAULT_HOUR_STEP,
minute_step: 5, 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,
},
}; };
} }
+5 -1
View File
@@ -1,4 +1,4 @@
import { LovelaceCardConfig } from 'custom-card-helpers'; import { ActionConfig, LovelaceCardConfig } from 'custom-card-helpers';
export interface TimePickerCardConfig extends LovelaceCardConfig { export interface TimePickerCardConfig extends LovelaceCardConfig {
entity: string; entity: string;
@@ -10,6 +10,9 @@ export interface TimePickerCardConfig extends LovelaceCardConfig {
second_step?: number; second_step?: number;
layout?: TimePickerLayoutConfig; layout?: TimePickerLayoutConfig;
hide?: TimePickerHideConfig; hide?: TimePickerHideConfig;
tap_action?: ActionConfig;
double_tap_action?: ActionConfig;
hold_action?: ActionConfig;
} }
export type HourMode = 12 | 24 | undefined; export type HourMode = 12 | 24 | undefined;
@@ -19,6 +22,7 @@ export interface TimePickerLayoutConfig {
name?: Layout.Name; name?: Layout.Name;
hour_mode?: Layout.HourMode; hour_mode?: Layout.HourMode;
embedded?: boolean; embedded?: boolean;
thin?: boolean;
} }
export namespace Layout { export namespace Layout {
-17
View File
@@ -1,17 +0,0 @@
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;
}
}
-111
View File
@@ -1,111 +0,0 @@
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);
});
});
});
-45
View File
@@ -1,45 +0,0 @@
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,
},
});
};
+32 -33
View File
@@ -1,165 +1,164 @@
import { Hour } from '../../src/models/hour'; import { Hour } from '../../src/models/hour';
import { Direction } from '../../src/types'; import { Direction } from '../../src/types';
import { DEFAULT_HOUR_STEP } from '../../src/const'; import { DEFAULT_HOUR_STEP } from '../../src/const';
import { expect } from 'chai';
describe('Hour', () => { describe('Hour', () => {
let hour: Hour; let hour: Hour;
context('with default config', () => { describe('with default config', () => {
beforeEach(() => { beforeEach(() => {
hour = new Hour(2); hour = new Hour(2);
}); });
it('returns value', () => { it('returns value', () => {
expect(hour.value).to.equal(2); expect(hour.value).toEqual(2);
}); });
it('has correct max value', () => { it('has correct max value', () => {
expect(hour.maxValue).to.equal(23); expect(hour.maxValue).toEqual(23);
}); });
describe('toString', () => { describe('toString', () => {
it('returns formatted string', () => { it('returns formatted string', () => {
expect(hour.toString()).to.equal('02'); expect(hour.toString()).toEqual('02');
}); });
it('renders values correctly', () => { it('renders values correctly', () => {
hour = new Hour(0); hour = new Hour(0);
expect(hour.toString()).to.equal('00'); expect(hour.toString()).toEqual('00');
hour = new Hour(6); hour = new Hour(6);
expect(hour.toString()).to.equal('06'); expect(hour.toString()).toEqual('06');
hour = new Hour(12); hour = new Hour(12);
expect(hour.toString()).to.equal('12'); expect(hour.toString()).toEqual('12');
hour = new Hour(18); hour = new Hour(18);
expect(hour.toString()).to.equal('18'); expect(hour.toString()).toEqual('18');
hour = new Hour(23); hour = new Hour(23);
expect(hour.toString()).to.equal('23'); expect(hour.toString()).toEqual('23');
}); });
}); });
describe('stepUpdate', () => { describe('stepUpdate', () => {
it('updates up', () => { it('updates up', () => {
hour.stepUpdate(Direction.UP); hour.stepUpdate(Direction.UP);
expect(hour.value).to.equal(3); expect(hour.value).toEqual(3);
}); });
it('updates down', () => { it('updates down', () => {
hour.stepUpdate(Direction.DOWN); hour.stepUpdate(Direction.DOWN);
expect(hour.value).to.equal(1); expect(hour.value).toEqual(1);
}); });
it('goes up from 23 to 0', () => { it('goes up from 23 to 0', () => {
hour = new Hour(23); hour = new Hour(23);
hour.stepUpdate(Direction.UP); hour.stepUpdate(Direction.UP);
expect(hour.value).to.equal(0); expect(hour.value).toEqual(0);
}); });
it('goes down from 0 to 23', () => { it('goes down from 0 to 23', () => {
hour = new Hour(0); hour = new Hour(0);
hour.stepUpdate(Direction.DOWN); hour.stepUpdate(Direction.DOWN);
expect(hour.value).to.equal(23); expect(hour.value).toEqual(23);
}); });
}); });
describe('setStringValue', () => { describe('setStringValue', () => {
it('sets value from valid string', () => { it('sets value from valid string', () => {
hour.setStringValue('3'); hour.setStringValue('3');
expect(hour.value).to.equal(3); expect(hour.value).toEqual(3);
}); });
it("doesn't set value from invalid string", () => { it("doesn't set value from invalid string", () => {
hour.setStringValue('test'); hour.setStringValue('test');
expect(hour.value).to.equal(2); expect(hour.value).toEqual(2);
}); });
it("doesn't set value from outside of scope", () => { it("doesn't set value from outside of scope", () => {
hour.setStringValue('24'); hour.setStringValue('24');
expect(hour.value).to.equal(2); expect(hour.value).toEqual(2);
}); });
}); });
}); });
context('with a different step size', () => { describe('with a different step size', () => {
beforeEach(() => { beforeEach(() => {
hour = new Hour(2, 5); hour = new Hour(2, 5);
}); });
it('goes up correctly', () => { it('goes up correctly', () => {
hour.stepUpdate(Direction.UP); hour.stepUpdate(Direction.UP);
expect(hour.value).to.equal(7); expect(hour.value).toEqual(7);
hour.stepUpdate(Direction.UP); hour.stepUpdate(Direction.UP);
hour.stepUpdate(Direction.UP); hour.stepUpdate(Direction.UP);
hour.stepUpdate(Direction.UP); hour.stepUpdate(Direction.UP);
hour.stepUpdate(Direction.UP); hour.stepUpdate(Direction.UP);
expect(hour.value).to.equal(3); expect(hour.value).toEqual(3);
}); });
it('goes down correclty', () => { it('goes down correclty', () => {
hour.stepUpdate(Direction.DOWN); hour.stepUpdate(Direction.DOWN);
expect(hour.value).to.equal(21); expect(hour.value).toEqual(21);
}); });
}); });
context('in 12 hour mode', () => { describe('in 12 hour mode', () => {
beforeEach(() => { beforeEach(() => {
hour = new Hour(2, DEFAULT_HOUR_STEP, 12); hour = new Hour(2, DEFAULT_HOUR_STEP, 12);
}); });
it('has correct max value', () => { it('has correct max value', () => {
expect(hour.maxValue).to.equal(12); expect(hour.maxValue).toEqual(12);
}); });
describe('togglePeriod', () => { describe('togglePeriod', () => {
it('changes values correclty', () => { it('changes values correclty', () => {
hour = new Hour(2, DEFAULT_HOUR_STEP, 12); hour = new Hour(2, DEFAULT_HOUR_STEP, 12);
hour.togglePeriod(); hour.togglePeriod();
expect(hour.value).to.equal(14); expect(hour.value).toEqual(14);
hour = new Hour(0, DEFAULT_HOUR_STEP, 12); hour = new Hour(0, DEFAULT_HOUR_STEP, 12);
hour.togglePeriod(); hour.togglePeriod();
expect(hour.value).to.equal(12); expect(hour.value).toEqual(12);
hour = new Hour(23, DEFAULT_HOUR_STEP, 12); hour = new Hour(23, DEFAULT_HOUR_STEP, 12);
hour.togglePeriod(); hour.togglePeriod();
expect(hour.value).to.equal(11); expect(hour.value).toEqual(11);
}); });
it('has no side effects', () => { it('has no side effects', () => {
const value = hour.value; const value = hour.value;
hour.togglePeriod(); hour.togglePeriod();
hour.togglePeriod(); hour.togglePeriod();
expect(hour.value).to.equal(value); expect(hour.value).toEqual(value);
}); });
}); });
describe('toString', () => { describe('toString', () => {
it('renders values above 12 orrectly', () => { it('renders values above 12 orrectly', () => {
hour = new Hour(13, DEFAULT_HOUR_STEP, 12); hour = new Hour(13, DEFAULT_HOUR_STEP, 12);
expect(hour.toString()).to.equal('01'); expect(hour.toString()).toEqual('01');
}); });
it('renders values correctly', () => { it('renders values correctly', () => {
hour = new Hour(0, DEFAULT_HOUR_STEP, 12); hour = new Hour(0, DEFAULT_HOUR_STEP, 12);
expect(hour.toString()).to.equal('12'); expect(hour.toString()).toEqual('12');
hour = new Hour(6, DEFAULT_HOUR_STEP, 12); hour = new Hour(6, DEFAULT_HOUR_STEP, 12);
expect(hour.toString()).to.equal('06'); expect(hour.toString()).toEqual('06');
hour = new Hour(12, DEFAULT_HOUR_STEP, 12); hour = new Hour(12, DEFAULT_HOUR_STEP, 12);
expect(hour.toString()).to.equal('12'); expect(hour.toString()).toEqual('12');
hour = new Hour(18, DEFAULT_HOUR_STEP, 12); hour = new Hour(18, DEFAULT_HOUR_STEP, 12);
expect(hour.toString()).to.equal('06'); expect(hour.toString()).toEqual('06');
hour = new Hour(23, DEFAULT_HOUR_STEP, 12); hour = new Hour(23, DEFAULT_HOUR_STEP, 12);
expect(hour.toString()).to.equal('11'); expect(hour.toString()).toEqual('11');
}); });
}); });
}); });
+22 -23
View File
@@ -1,119 +1,118 @@
import { Minute } from '../../src/models/minute'; import { Minute } from '../../src/models/minute';
import { Direction } from '../../src/types'; import { Direction } from '../../src/types';
import { expect } from 'chai';
describe('Minute', () => { describe('Minute', () => {
let minute: Minute; let minute: Minute;
context('with default config', () => { describe('with default config', () => {
beforeEach(() => { beforeEach(() => {
minute = new Minute(2); minute = new Minute(2);
}); });
it('returns value', () => { it('returns value', () => {
expect(minute.value).to.equal(2); expect(minute.value).toEqual(2);
}); });
it('has correct max value', () => { it('has correct max value', () => {
expect(minute.maxValue).to.equal(59); expect(minute.maxValue).toEqual(59);
}); });
describe('toString', () => { describe('toString', () => {
it('renders values correctly', () => { it('renders values correctly', () => {
minute = new Minute(0); minute = new Minute(0);
expect(minute.toString()).to.equal('00'); expect(minute.toString()).toEqual('00');
minute = new Minute(5); minute = new Minute(5);
expect(minute.toString()).to.equal('05'); expect(minute.toString()).toEqual('05');
minute = new Minute(59); minute = new Minute(59);
expect(minute.toString()).to.equal('59'); expect(minute.toString()).toEqual('59');
}); });
}); });
describe('stepUpdate', () => { describe('stepUpdate', () => {
it('updates up', () => { it('updates up', () => {
minute.stepUpdate(Direction.UP); minute.stepUpdate(Direction.UP);
expect(minute.value).to.equal(7); expect(minute.value).toEqual(7);
}); });
it('updates down', () => { it('updates down', () => {
minute.stepUpdate(Direction.DOWN); minute.stepUpdate(Direction.DOWN);
expect(minute.value).to.equal(57); expect(minute.value).toEqual(57);
}); });
it('goes up from 59 to 4', () => { it('goes up from 59 to 4', () => {
minute = new Minute(59); minute = new Minute(59);
minute.stepUpdate(Direction.UP); minute.stepUpdate(Direction.UP);
expect(minute.value).to.equal(4); expect(minute.value).toEqual(4);
}); });
it('goes down from 0 to 55', () => { it('goes down from 0 to 55', () => {
minute = new Minute(0); minute = new Minute(0);
minute.stepUpdate(Direction.DOWN); minute.stepUpdate(Direction.DOWN);
expect(minute.value).to.equal(55); expect(minute.value).toEqual(55);
}); });
}); });
describe('setStringValue', () => { describe('setStringValue', () => {
it('sets value from valid string', () => { it('sets value from valid string', () => {
minute.setStringValue('3'); minute.setStringValue('3');
expect(minute.value).to.equal(3); expect(minute.value).toEqual(3);
}); });
it("doesn't set value from invalid string", () => { it("doesn't set value from invalid string", () => {
minute.setStringValue('test'); minute.setStringValue('test');
expect(minute.value).to.equal(2); expect(minute.value).toEqual(2);
}); });
it("doesn't set value from outside of scope", () => { it("doesn't set value from outside of scope", () => {
minute.setStringValue('60'); minute.setStringValue('60');
expect(minute.value).to.equal(2); expect(minute.value).toEqual(2);
minute.setStringValue('65'); minute.setStringValue('65');
expect(minute.value).to.equal(2); expect(minute.value).toEqual(2);
}); });
}); });
}); });
context('with a different step size', () => { describe('with a different step size', () => {
beforeEach(() => { beforeEach(() => {
minute = new Minute(2, 15); minute = new Minute(2, 15);
}); });
it('goes up correctly', () => { it('goes up correctly', () => {
minute.stepUpdate(Direction.UP); minute.stepUpdate(Direction.UP);
expect(minute.value).to.equal(17); expect(minute.value).toEqual(17);
minute.stepUpdate(Direction.UP); minute.stepUpdate(Direction.UP);
minute.stepUpdate(Direction.UP); minute.stepUpdate(Direction.UP);
minute.stepUpdate(Direction.UP); minute.stepUpdate(Direction.UP);
expect(minute.value).to.equal(2); expect(minute.value).toEqual(2);
minute.stepUpdate(Direction.UP); minute.stepUpdate(Direction.UP);
expect(minute.value).to.equal(17); expect(minute.value).toEqual(17);
}); });
it('goes down correclty', () => { it('goes down correclty', () => {
minute.stepUpdate(Direction.DOWN); minute.stepUpdate(Direction.DOWN);
expect(minute.value).to.equal(47); expect(minute.value).toEqual(47);
}); });
}); });
describe('will overflow', () => { describe('will overflow', () => {
it('returns true if the minutes will overflow an hour up', () => { it('returns true if the minutes will overflow an hour up', () => {
minute.setStringValue('59'); minute.setStringValue('59');
expect(minute.willOverflow(Direction.UP)).to.be.true; expect(minute.willOverflow(Direction.UP)).toBe(true);
}); });
it('returns true if the minutes will overflow an hour down', () => { it('returns true if the minutes will overflow an hour down', () => {
minute.setStringValue('0'); minute.setStringValue('0');
expect(minute.willOverflow(Direction.DOWN)).to.be.true; expect(minute.willOverflow(Direction.DOWN)).toBe(true);
}); });
it('returns true if the minutes will not overflow', () => { it('returns true if the minutes will not overflow', () => {
minute.setStringValue('55'); minute.setStringValue('55');
expect(minute.willOverflow(Direction.DOWN)).to.be.false; expect(minute.willOverflow(Direction.DOWN)).toBe(false);
}); });
}); });
}); });
+22 -23
View File
@@ -1,119 +1,118 @@
import { Second } from '../../src/models/second'; import { Second } from '../../src/models/second';
import { Direction } from '../../src/types'; import { Direction } from '../../src/types';
import { expect } from 'chai';
describe('Second', () => { describe('Second', () => {
let second: Second; let second: Second;
context('with default config', () => { describe('with default config', () => {
beforeEach(() => { beforeEach(() => {
second = new Second(2); second = new Second(2);
}); });
it('returns value', () => { it('returns value', () => {
expect(second.value).to.equal(2); expect(second.value).toEqual(2);
}); });
it('has correct max value', () => { it('has correct max value', () => {
expect(second.maxValue).to.equal(59); expect(second.maxValue).toEqual(59);
}); });
describe('toString', () => { describe('toString', () => {
it('renders values correctly', () => { it('renders values correctly', () => {
second = new Second(0); second = new Second(0);
expect(second.toString()).to.equal('00'); expect(second.toString()).toEqual('00');
second = new Second(5); second = new Second(5);
expect(second.toString()).to.equal('05'); expect(second.toString()).toEqual('05');
second = new Second(59); second = new Second(59);
expect(second.toString()).to.equal('59'); expect(second.toString()).toEqual('59');
}); });
}); });
describe('stepUpdate', () => { describe('stepUpdate', () => {
it('updates up', () => { it('updates up', () => {
second.stepUpdate(Direction.UP); second.stepUpdate(Direction.UP);
expect(second.value).to.equal(7); expect(second.value).toEqual(7);
}); });
it('updates down', () => { it('updates down', () => {
second.stepUpdate(Direction.DOWN); second.stepUpdate(Direction.DOWN);
expect(second.value).to.equal(57); expect(second.value).toEqual(57);
}); });
it('goes up from 59 to 4', () => { it('goes up from 59 to 4', () => {
second = new Second(59); second = new Second(59);
second.stepUpdate(Direction.UP); second.stepUpdate(Direction.UP);
expect(second.value).to.equal(4); expect(second.value).toEqual(4);
}); });
it('goes down from 0 to 55', () => { it('goes down from 0 to 55', () => {
second = new Second(0); second = new Second(0);
second.stepUpdate(Direction.DOWN); second.stepUpdate(Direction.DOWN);
expect(second.value).to.equal(55); expect(second.value).toEqual(55);
}); });
}); });
describe('setStringValue', () => { describe('setStringValue', () => {
it('sets value from valid string', () => { it('sets value from valid string', () => {
second.setStringValue('3'); second.setStringValue('3');
expect(second.value).to.equal(3); expect(second.value).toEqual(3);
}); });
it("doesn't set value from invalid string", () => { it("doesn't set value from invalid string", () => {
second.setStringValue('test'); second.setStringValue('test');
expect(second.value).to.equal(2); expect(second.value).toEqual(2);
}); });
it("doesn't set value from outside of scope", () => { it("doesn't set value from outside of scope", () => {
second.setStringValue('60'); second.setStringValue('60');
expect(second.value).to.equal(2); expect(second.value).toEqual(2);
second.setStringValue('65'); second.setStringValue('65');
expect(second.value).to.equal(2); expect(second.value).toEqual(2);
}); });
}); });
}); });
context('with a different step size', () => { describe('with a different step size', () => {
beforeEach(() => { beforeEach(() => {
second = new Second(2, 15); second = new Second(2, 15);
}); });
it('goes up correctly', () => { it('goes up correctly', () => {
second.stepUpdate(Direction.UP); second.stepUpdate(Direction.UP);
expect(second.value).to.equal(17); expect(second.value).toEqual(17);
second.stepUpdate(Direction.UP); second.stepUpdate(Direction.UP);
second.stepUpdate(Direction.UP); second.stepUpdate(Direction.UP);
second.stepUpdate(Direction.UP); second.stepUpdate(Direction.UP);
expect(second.value).to.equal(2); expect(second.value).toEqual(2);
second.stepUpdate(Direction.UP); second.stepUpdate(Direction.UP);
expect(second.value).to.equal(17); expect(second.value).toEqual(17);
}); });
it('goes down correclty', () => { it('goes down correclty', () => {
second.stepUpdate(Direction.DOWN); second.stepUpdate(Direction.DOWN);
expect(second.value).to.equal(47); expect(second.value).toEqual(47);
}); });
}); });
describe('will overflow', () => { describe('will overflow', () => {
it('returns true if the Seconds will overflow an hour up', () => { it('returns true if the Seconds will overflow an hour up', () => {
second.setStringValue('59'); second.setStringValue('59');
expect(second.willOverflow(Direction.UP)).to.be.true; expect(second.willOverflow(Direction.UP)).toBe(true);
}); });
it('returns true if the Seconds will overflow an hour down', () => { it('returns true if the Seconds will overflow an hour down', () => {
second.setStringValue('0'); second.setStringValue('0');
expect(second.willOverflow(Direction.DOWN)).to.be.true; expect(second.willOverflow(Direction.DOWN)).toBe(true);
}); });
it('returns true if the Seconds will not overflow', () => { it('returns true if the Seconds will not overflow', () => {
second.setStringValue('55'); second.setStringValue('55');
expect(second.willOverflow(Direction.DOWN)).to.be.false; expect(second.willOverflow(Direction.DOWN)).toBe(false);
}); });
}); });
}); });