Skip to content

Commit

Permalink
lots of new changes
Browse files Browse the repository at this point in the history
* HACS badge
* hacs.json
* New action handling with double tap support
* Cleanup
  • Loading branch information
iantrich committed Oct 30, 2019
1 parent 3e606bb commit 078499f
Show file tree
Hide file tree
Showing 10 changed files with 695 additions and 1,244 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ A community driven boilerplate of best practices for Home Assistant Lovelace cus

[![GitHub Release][releases-shield]][releases]
[![License][license-shield]](LICENSE.md)
[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/custom-components/hacs)

![Project Maintenance][maintenance-shield]
[![GitHub Activity][commits-shield]][commits]
Expand Down Expand Up @@ -50,9 +51,13 @@ Install necessary modules
Do a test lint & build on the project. You can see available scripts in the package.json
`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)

Expand Down
18 changes: 18 additions & 0 deletions dist/boilerplate-card.js

Large diffs are not rendered by default.

19 changes: 0 additions & 19 deletions dist/card.js

This file was deleted.

4 changes: 4 additions & 0 deletions hacs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "Boilerplate Card",
"render_readme": true
}
38 changes: 19 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "boilerplate-card",
"version": "1.1.0",
"version": "1.1.1",
"description": "Lovelace boilerplate-card",
"keywords": [
"home-assistant",
Expand All @@ -12,30 +12,30 @@
],
"module": "boilerplate-card.js",
"repository": "[email protected]:custom_cards/boilerplate-card.git",
"author": "BoilerPlate <boilerplate@email.com>",
"author": "Ian Richardson <iantrich@gmail.com>",
"license": "MIT",
"dependencies": {
"custom-card-helpers": "^1.0.8",
"home-assistant-js-websocket": "^3.4.0",
"lit-element": "^2.0.1"
"custom-card-helpers": "^1.3.5",
"home-assistant-js-websocket": "^4.4.0",
"lit-element": "^2.2.1"
},
"devDependencies": {
"@babel/core": "^7.4.3",
"@babel/plugin-proposal-class-properties": "^7.4.0",
"@babel/core": "^7.5.5",
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-proposal-decorators": "^7.4.0",
"@typescript-eslint/eslint-plugin": "^1.4.2",
"@typescript-eslint/parser": "^1.4.1",
"eslint": "^5.14.1",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-plugin-import": "^2.16.0",
"prettier": "^1.16.4",
"rollup": "^1.2.3",
"rollup-plugin-babel": "^4.3.2",
"rollup-plugin-node-resolve": "^4.0.1",
"rollup-plugin-terser": "^4.0.4",
"rollup-plugin-typescript2": "^0.19.2",
"@typescript-eslint/eslint-plugin": "^2.0.0",
"@typescript-eslint/parser": "^2.0.0",
"eslint": "^6.2.2",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-plugin-import": "^2.18.2",
"prettier": "^1.18.2",
"rollup": "^1.20.2",
"rollup-plugin-babel": "^4.3.3",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-terser": "^5.1.1",
"rollup-plugin-typescript2": "^0.23.0",
"rollup-plugin-uglify": "^6.0.2",
"typescript": "^3.3.3333"
"typescript": "^3.5.3"
},
"scripts": {
"start": "rollup -c --watch",
Expand Down
2 changes: 1 addition & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import babel from 'rollup-plugin-babel';
import { terser } from "rollup-plugin-terser";

export default {
input: ['src/card.ts'],
input: ['src/boilerplate-card.ts'],
output: {
dir: './dist',
format: 'es',
Expand Down
206 changes: 206 additions & 0 deletions src/action-handler-directive.ts
Original file line number Diff line number Diff line change
@@ -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);
}
);
34 changes: 18 additions & 16 deletions src/card.ts → src/boilerplate-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,20 @@ import {
} from "lit-element";
import {
HomeAssistant,
handleClick,
longPress,
hasConfigOrEntityChanged
hasConfigOrEntityChanged,
hasAction,
ActionHandlerEvent,
handleAction
} from "custom-card-helpers";

import { BoilerplateConfig } from "./types";
import { actionHandler } from "./action-handler-directive";

// TODO Name your custom element
@customElement("boilerplate-card")
class BoilerplateCard extends LitElement {
// TODO Add any properities that should cause your element to re-render here
@property() public hass?: HomeAssistant;

@property() private _config?: BoilerplateConfig;

public setConfig(config: BoilerplateConfig): void {
Expand All @@ -31,11 +32,14 @@ class BoilerplateCard extends LitElement {
throw new Error("Invalid configuration");
}

this._config = config;
this._config = {
name: "Boilerplate",
...config
};
}

protected shouldUpdate(changedProps: PropertyValues): boolean {
return hasConfigOrEntityChanged(this, changedProps);
return hasConfigOrEntityChanged(this, changedProps, false);
}

protected render(): TemplateResult | void {
Expand All @@ -54,20 +58,18 @@ class BoilerplateCard extends LitElement {

return html`
<ha-card
.header=${this._config.name ? this._config.name : "Boilerplate"}
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longpress="${longPress()}"
.header=${this._config.name}
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleTap: hasAction(this._config!.double_tap_action)
})}
></ha-card>
`;
}

private _handleTap(): void {
handleClick(this, this.hass!, this._config!, false);
}

private _handleHold(): void {
handleClick(this, this.hass!, this._config!, true);
private _handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass!, this._config!, ev.detail.action!);
}

static get styles(): CSSResult {
Expand Down
3 changes: 2 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -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
export interface BoilerplateConfig {
Expand All @@ -9,4 +9,5 @@ export interface BoilerplateConfig {
entity?: string;
tap_aciton?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
}
Loading

0 comments on commit 078499f

Please sign in to comment.