lots of new changes
* HACS badge * hacs.json * New action handling with double tap support * Cleanup
This commit is contained in:
@@ -4,6 +4,7 @@ A community driven boilerplate of best practices for Home Assistant Lovelace cus
|
|||||||
|
|
||||||
[![GitHub Release][releases-shield]][releases]
|
[![GitHub Release][releases-shield]][releases]
|
||||||
[![License][license-shield]](LICENSE.md)
|
[![License][license-shield]](LICENSE.md)
|
||||||
|
[](https://github.com/custom-components/hacs)
|
||||||
|
|
||||||
![Project Maintenance][maintenance-shield]
|
![Project Maintenance][maintenance-shield]
|
||||||
[![GitHub Activity][commits-shield]][commits]
|
[![GitHub Activity][commits-shield]][commits]
|
||||||
@@ -50,9 +51,13 @@ Install necessary modules
|
|||||||
Do a test lint & build on the project. You can see available scripts in the package.json
|
Do a test lint & build on the project. You can see available scripts in the package.json
|
||||||
`npm run build`
|
`npm run build`
|
||||||
|
|
||||||
### Step 4
|
## Step 4
|
||||||
|
|
||||||
Customize to suit your needs and contribute it back to the custom-cards org
|
Search the repo for all instances of "TODO" and handle the changes/suggestions
|
||||||
|
|
||||||
|
### Step 5
|
||||||
|
|
||||||
|
Customize to suit your needs and contribute it back to the community
|
||||||
|
|
||||||
[Troubleshooting](https://github.com/thomasloven/hass-config/wiki/Lovelace-Plugins)
|
[Troubleshooting](https://github.com/thomasloven/hass-config/wiki/Lovelace-Plugins)
|
||||||
|
|
||||||
|
|||||||
Vendored
+18
File diff suppressed because one or more lines are too long
Vendored
-19
File diff suppressed because one or more lines are too long
+19
-19
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "boilerplate-card",
|
"name": "boilerplate-card",
|
||||||
"version": "1.1.0",
|
"version": "1.1.1",
|
||||||
"description": "Lovelace boilerplate-card",
|
"description": "Lovelace boilerplate-card",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"home-assistant",
|
"home-assistant",
|
||||||
@@ -12,30 +12,30 @@
|
|||||||
],
|
],
|
||||||
"module": "boilerplate-card.js",
|
"module": "boilerplate-card.js",
|
||||||
"repository": "git@github.com:custom_cards/boilerplate-card.git",
|
"repository": "git@github.com:custom_cards/boilerplate-card.git",
|
||||||
"author": "BoilerPlate <boilerplate@email.com>",
|
"author": "Ian Richardson <iantrich@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"custom-card-helpers": "^1.0.8",
|
"custom-card-helpers": "^1.3.5",
|
||||||
"home-assistant-js-websocket": "^3.4.0",
|
"home-assistant-js-websocket": "^4.4.0",
|
||||||
"lit-element": "^2.0.1"
|
"lit-element": "^2.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.4.3",
|
"@babel/core": "^7.5.5",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.4.0",
|
"@babel/plugin-proposal-class-properties": "^7.5.5",
|
||||||
"@babel/plugin-proposal-decorators": "^7.4.0",
|
"@babel/plugin-proposal-decorators": "^7.4.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^1.4.2",
|
"@typescript-eslint/eslint-plugin": "^2.0.0",
|
||||||
"@typescript-eslint/parser": "^1.4.1",
|
"@typescript-eslint/parser": "^2.0.0",
|
||||||
"eslint": "^5.14.1",
|
"eslint": "^6.2.2",
|
||||||
"eslint-config-airbnb-base": "^13.1.0",
|
"eslint-config-airbnb-base": "^14.0.0",
|
||||||
"eslint-plugin-import": "^2.16.0",
|
"eslint-plugin-import": "^2.18.2",
|
||||||
"prettier": "^1.16.4",
|
"prettier": "^1.18.2",
|
||||||
"rollup": "^1.2.3",
|
"rollup": "^1.20.2",
|
||||||
"rollup-plugin-babel": "^4.3.2",
|
"rollup-plugin-babel": "^4.3.3",
|
||||||
"rollup-plugin-node-resolve": "^4.0.1",
|
"rollup-plugin-node-resolve": "^5.2.0",
|
||||||
"rollup-plugin-terser": "^4.0.4",
|
"rollup-plugin-terser": "^5.1.1",
|
||||||
"rollup-plugin-typescript2": "^0.19.2",
|
"rollup-plugin-typescript2": "^0.23.0",
|
||||||
"rollup-plugin-uglify": "^6.0.2",
|
"rollup-plugin-uglify": "^6.0.2",
|
||||||
"typescript": "^3.3.3333"
|
"typescript": "^3.5.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "rollup -c --watch",
|
"start": "rollup -c --watch",
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ import babel from 'rollup-plugin-babel';
|
|||||||
import { terser } from "rollup-plugin-terser";
|
import { terser } from "rollup-plugin-terser";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: ['src/card.ts'],
|
input: ['src/boilerplate-card.ts'],
|
||||||
output: {
|
output: {
|
||||||
dir: './dist',
|
dir: './dist',
|
||||||
format: 'es',
|
format: 'es',
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import { directive, PropertyPart } from "lit-html";
|
||||||
|
import { fireEvent, ActionHandlerOptions } from "custom-card-helpers";
|
||||||
|
|
||||||
|
const isTouch =
|
||||||
|
"ontouchstart" in window ||
|
||||||
|
navigator.maxTouchPoints > 0 ||
|
||||||
|
navigator.msMaxTouchPoints > 0;
|
||||||
|
|
||||||
|
interface ActionHandler extends HTMLElement {
|
||||||
|
holdTime: number;
|
||||||
|
bind(element: Element, options): void;
|
||||||
|
}
|
||||||
|
interface ActionHandlerElement extends Element {
|
||||||
|
actionHandler?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ActionHandler extends HTMLElement implements ActionHandler {
|
||||||
|
public holdTime: number;
|
||||||
|
public ripple: any;
|
||||||
|
protected timer: number | undefined;
|
||||||
|
protected held: boolean;
|
||||||
|
protected cooldownStart: boolean;
|
||||||
|
protected cooldownEnd: boolean;
|
||||||
|
private dblClickTimeout: number | undefined;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.holdTime = 500;
|
||||||
|
this.ripple = document.createElement("mwc-ripple");
|
||||||
|
this.timer = undefined;
|
||||||
|
this.held = false;
|
||||||
|
this.cooldownStart = false;
|
||||||
|
this.cooldownEnd = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public connectedCallback() {
|
||||||
|
Object.assign(this.style, {
|
||||||
|
position: "absolute",
|
||||||
|
width: isTouch ? "100px" : "50px",
|
||||||
|
height: isTouch ? "100px" : "50px",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
pointerEvents: "none",
|
||||||
|
});
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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 clickStart = (ev: Event) => {
|
||||||
|
if (this.cooldownStart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.hasHold) {
|
||||||
|
this.timer = window.setTimeout(() => {
|
||||||
|
this.startAnimation(x, y);
|
||||||
|
this.held = true;
|
||||||
|
}, this.holdTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cooldownStart = true;
|
||||||
|
window.setTimeout(() => (this.cooldownStart = false), 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clickEnd = (ev: Event) => {
|
||||||
|
if (
|
||||||
|
this.cooldownEnd ||
|
||||||
|
(["touchend", "touchcancel"].includes(ev.type) &&
|
||||||
|
this.timer === undefined)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
this.stopAnimation();
|
||||||
|
this.timer = undefined;
|
||||||
|
if (this.held) {
|
||||||
|
fireEvent(element as HTMLElement, "action", { action: "hold" });
|
||||||
|
} else if (options.hasDoubleTap) {
|
||||||
|
if ((ev as MouseEvent).detail === 1) {
|
||||||
|
this.dblClickTimeout = window.setTimeout(() => {
|
||||||
|
fireEvent(element as HTMLElement, "action", { action: "tap" });
|
||||||
|
}, 250);
|
||||||
|
} else {
|
||||||
|
clearTimeout(this.dblClickTimeout);
|
||||||
|
fireEvent(element as HTMLElement, "action", { action: "double_tap" });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fireEvent(element as HTMLElement, "action", { action: "tap" });
|
||||||
|
}
|
||||||
|
this.cooldownEnd = true;
|
||||||
|
window.setTimeout(() => (this.cooldownEnd = false), 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
element.addEventListener("touchstart", clickStart, { passive: true });
|
||||||
|
element.addEventListener("touchend", clickEnd);
|
||||||
|
element.addEventListener("touchcancel", clickEnd);
|
||||||
|
|
||||||
|
// iOS 13 sends a complete normal touchstart-touchend series of events followed by a mousedown-click series.
|
||||||
|
// That might be a bug, but until it's fixed, this should make action-handler work.
|
||||||
|
// If it's not a bug that is fixed, this might need updating with the next iOS version.
|
||||||
|
// Note that all events (both touch and mouse) must be listened for in order to work on computers with both mouse and touchscreen.
|
||||||
|
const isIOS13 = window.navigator.userAgent.match(/iPhone OS 13_/);
|
||||||
|
if (!isIOS13) {
|
||||||
|
element.addEventListener("mousedown", clickStart, { passive: true });
|
||||||
|
element.addEventListener("click", clickEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private startAnimation(x: number, y: number) {
|
||||||
|
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() {
|
||||||
|
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 geActionHandler = (): 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
|
||||||
|
) => {
|
||||||
|
const actionhandler: ActionHandler = geActionHandler();
|
||||||
|
if (!actionhandler) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
actionhandler.bind(element, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actionHandler = directive(
|
||||||
|
(options: ActionHandlerOptions = {}) => (part: PropertyPart) => {
|
||||||
|
actionHandlerBind(part.committer.element, options);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -10,19 +10,20 @@ import {
|
|||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import {
|
import {
|
||||||
HomeAssistant,
|
HomeAssistant,
|
||||||
handleClick,
|
hasConfigOrEntityChanged,
|
||||||
longPress,
|
hasAction,
|
||||||
hasConfigOrEntityChanged
|
ActionHandlerEvent,
|
||||||
|
handleAction
|
||||||
} from "custom-card-helpers";
|
} from "custom-card-helpers";
|
||||||
|
|
||||||
import { BoilerplateConfig } from "./types";
|
import { BoilerplateConfig } from "./types";
|
||||||
|
import { actionHandler } from "./action-handler-directive";
|
||||||
|
|
||||||
// TODO Name your custom element
|
// TODO Name your custom element
|
||||||
@customElement("boilerplate-card")
|
@customElement("boilerplate-card")
|
||||||
class BoilerplateCard extends LitElement {
|
class BoilerplateCard extends LitElement {
|
||||||
// TODO Add any properities that should cause your element to re-render here
|
// TODO Add any properities that should cause your element to re-render here
|
||||||
@property() public hass?: HomeAssistant;
|
@property() public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property() private _config?: BoilerplateConfig;
|
@property() private _config?: BoilerplateConfig;
|
||||||
|
|
||||||
public setConfig(config: BoilerplateConfig): void {
|
public setConfig(config: BoilerplateConfig): void {
|
||||||
@@ -31,11 +32,14 @@ class BoilerplateCard extends LitElement {
|
|||||||
throw new Error("Invalid configuration");
|
throw new Error("Invalid configuration");
|
||||||
}
|
}
|
||||||
|
|
||||||
this._config = config;
|
this._config = {
|
||||||
|
name: "Boilerplate",
|
||||||
|
...config
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||||
return hasConfigOrEntityChanged(this, changedProps);
|
return hasConfigOrEntityChanged(this, changedProps, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
@@ -54,20 +58,18 @@ class BoilerplateCard extends LitElement {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-card
|
<ha-card
|
||||||
.header=${this._config.name ? this._config.name : "Boilerplate"}
|
.header=${this._config.name}
|
||||||
@ha-click="${this._handleTap}"
|
@action=${this._handleAction}
|
||||||
@ha-hold="${this._handleHold}"
|
.actionHandler=${actionHandler({
|
||||||
.longpress="${longPress()}"
|
hasHold: hasAction(this._config!.hold_action),
|
||||||
|
hasDoubleTap: hasAction(this._config!.double_tap_action)
|
||||||
|
})}
|
||||||
></ha-card>
|
></ha-card>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleTap(): void {
|
private _handleAction(ev: ActionHandlerEvent) {
|
||||||
handleClick(this, this.hass!, this._config!, false);
|
handleAction(this, this.hass!, this._config!, ev.detail.action!);
|
||||||
}
|
|
||||||
|
|
||||||
private _handleHold(): void {
|
|
||||||
handleClick(this, this.hass!, this._config!, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResult {
|
static get styles(): CSSResult {
|
||||||
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
import { ActionConfig } from "custom-card-helpers";
|
import { ActionConfig, HASSDomEvent } from "custom-card-helpers";
|
||||||
|
|
||||||
// TODO Add your configuration elements here for type-checking
|
// TODO Add your configuration elements here for type-checking
|
||||||
export interface BoilerplateConfig {
|
export interface BoilerplateConfig {
|
||||||
@@ -9,4 +9,5 @@ export interface BoilerplateConfig {
|
|||||||
entity?: string;
|
entity?: string;
|
||||||
tap_aciton?: ActionConfig;
|
tap_aciton?: ActionConfig;
|
||||||
hold_action?: ActionConfig;
|
hold_action?: ActionConfig;
|
||||||
|
double_tap_action?: ActionConfig;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user