custom-card-helpers

This commit is contained in:
Ian Richardson
2019-05-06 18:05:35 -05:00
parent 9e63c27066
commit 0745d2f4fb
11 changed files with 2640 additions and 103 deletions
+4
View File
@@ -12,6 +12,10 @@ rules:
no-nested-ternary: 0
camelcase: 0
no-unused-vars: 0
quotes: 0
comma-dangle: 0
import/no-unresolved: 0
import/prefer-default-export: 0
globals:
window: true
Event: true
-1
View File
@@ -1,3 +1,2 @@
/node_modules/
/.rpt2_cache/
yarn.lock
+25 -6
View File
@@ -1,5 +1,6 @@
# Boilerplate Card
A community driven boilerplate of best practices from Home Assistant Lovelace custom cards
A community driven boilerplate of best practices for Home Assistant Lovelace custom cards
[![GitHub Release][releases-shield]][releases]
[![License][license-shield]](LICENSE.md)
@@ -12,27 +13,45 @@ A community driven boilerplate of best practices from Home Assistant Lovelace cu
## Options
| Name | Type | Requirement | Description
| ---- | ---- | ------- | -----------
| Name | Type | Requirement | Description | Default
| ---- | ---- | ------- | ----------- | -------
| type | string | **Required** | `custom:boilerplate-card`
| name | string | **Optional** | Card name
| show_error | boolean | **Optional** | Show what an error looks like for the card
| show_warning | boolean | **Optional** | Show what a warning looks like for the card
| name | string | **Optional** | Card name | `Boilerplate`
| show_error | boolean | **Optional** | Show what an error looks like for the card | `false`
| show_warning | boolean | **Optional** | Show what a warning looks like for the card | `false`
| entity | string | **Optional** | Home Assistant entity ID. | `none`
| tap_action | object | **Optional** | Action to take on tap | `action: more-info`
| hold_action | object | **Optional** | Action to take on hold | `none`
## Action Options
| Name | Type | Requirement | Description | Default
| ---- | ---- | ------- | ----------- | -------
| action | string | **Required** | Action to perform (toggle-menu, more-info, toggle, call-service, navigate url, none) | `toggle-menu` for menu and `more-info` for items
| navigation_path | string | **Optional** | Path to navigate to (e.g. /lovelace/0/) when action defined as navigate | `none`
| url | string | **Optional** | URL to open on click when action is url. The URL will open in a new tab | `none`
| service | string | **Optional** | Service to call (e.g. media_player.media_play_pause) when action defined as call-service | `none`
| service_data | object | **Optional** | Service data to include (e.g. entity_id: media_player.bedroom) when action defined as call-service | `none`
| haptic | string | **Optional** | Haptic feedback for the [Beta IOS App](http://home-assistant.io/ios/beta) _success, warning, failure, light, medium, heavy, selection_ | `none`
## Starting a new card from boilerplate-card
### Step 1
Clone this repo
### Step 2
Install necessary modules
`yarn install` or `npm install`
### Step 3
Do a test lint & build on the project. You can see available scripts in the package.json
`npm run build`
### Step 4
Customize to suit your needs and contribute it back to the custom-cards org
[Troubleshooting](https://github.com/thomasloven/hass-config/wiki/Lovelace-Plugins)
-1
View File
@@ -1 +0,0 @@
+131 -56
View File
@@ -1543,7 +1543,6 @@ class UpdatingElement extends HTMLElement {
Object.defineProperty(this.prototype, name, {
// tslint:disable-next-line:no-any no symbol in index
get() {
// tslint:disable-next-line:no-any no symbol in index
return this[key];
},
set(value) {
@@ -1551,7 +1550,7 @@ class UpdatingElement extends HTMLElement {
const oldValue = this[name];
// tslint:disable-next-line:no-any no symbol in index
this[key] = value;
this.requestUpdate(name, oldValue);
this._requestUpdate(name, oldValue);
},
configurable: true,
enumerable: true
@@ -1655,6 +1654,8 @@ class UpdatingElement extends HTMLElement {
*/
initialize() {
this._saveInstanceProperties();
// ensures first update will be caught by an early access of `updateComplete`
this._requestUpdate();
}
/**
* Fixes any properties set on the instance before upgrade time.
@@ -1695,7 +1696,7 @@ class UpdatingElement extends HTMLElement {
}
connectedCallback() {
this._updateState = this._updateState | STATE_HAS_CONNECTED;
// Ensure connection triggers an update. Updates cannot complete before
// Ensure first connection completes an update. Updates cannot complete before
// connection and if one is pending connection the `_hasConnectionResolver`
// will exist. If so, resolve it to complete the update, otherwise
// requestUpdate.
@@ -1703,9 +1704,6 @@ class UpdatingElement extends HTMLElement {
this._hasConnectedResolver();
this._hasConnectedResolver = undefined;
}
else {
this.requestUpdate();
}
}
/**
* Allows for `super.disconnectedCallback()` in extensions while
@@ -1769,6 +1767,42 @@ class UpdatingElement extends HTMLElement {
this._updateState = this._updateState & ~STATE_IS_REFLECTING_TO_PROPERTY;
}
}
/**
* This private version of `requestUpdate` does not access or return the
* `updateComplete` promise. This promise can be overridden and is therefore
* not free to access.
*/
_requestUpdate(name, oldValue) {
let shouldRequestUpdate = true;
// If we have a property key, perform property update steps.
if (name !== undefined) {
const ctor = this.constructor;
const options = ctor._classProperties.get(name) || defaultPropertyDeclaration;
if (ctor._valueHasChanged(this[name], oldValue, options.hasChanged)) {
if (!this._changedProperties.has(name)) {
this._changedProperties.set(name, oldValue);
}
// Add to reflecting properties set.
// Note, it's important that every change has a chance to add the
// property to `_reflectingProperties`. This ensures setting
// attribute + property reflects correctly.
if (options.reflect === true &&
!(this._updateState & STATE_IS_REFLECTING_TO_PROPERTY)) {
if (this._reflectingProperties === undefined) {
this._reflectingProperties = new Map();
}
this._reflectingProperties.set(name, options);
}
}
else {
// Abort the request if the property should not be considered changed.
shouldRequestUpdate = false;
}
}
if (!this._hasRequestedUpdate && shouldRequestUpdate) {
this._enqueueUpdate();
}
}
/**
* Requests an update which is processed asynchronously. This should
* be called when an element should update based on some state not triggered
@@ -1783,31 +1817,7 @@ class UpdatingElement extends HTMLElement {
* @returns {Promise} A Promise that is resolved when the update completes.
*/
requestUpdate(name, oldValue) {
let shouldRequestUpdate = true;
// if we have a property key, perform property update steps.
if (name !== undefined && !this._changedProperties.has(name)) {
const ctor = this.constructor;
const options = ctor._classProperties.get(name) || defaultPropertyDeclaration;
if (ctor._valueHasChanged(this[name], oldValue, options.hasChanged)) {
// track old value when changing.
this._changedProperties.set(name, oldValue);
// add to reflecting properties set
if (options.reflect === true &&
!(this._updateState & STATE_IS_REFLECTING_TO_PROPERTY)) {
if (this._reflectingProperties === undefined) {
this._reflectingProperties = new Map();
}
this._reflectingProperties.set(name, options);
}
// abort the request if the property should not be considered changed.
}
else {
shouldRequestUpdate = false;
}
}
if (!this._hasRequestedUpdate && shouldRequestUpdate) {
this._enqueueUpdate();
}
this._requestUpdate(name, oldValue);
return this.updateComplete;
}
/**
@@ -1817,22 +1827,36 @@ class UpdatingElement extends HTMLElement {
// Mark state updating...
this._updateState = this._updateState | STATE_UPDATE_REQUESTED;
let resolve;
let reject;
const previousUpdatePromise = this._updatePromise;
this._updatePromise = new Promise((res) => resolve = res);
// Ensure any previous update has resolved before updating.
// This `await` also ensures that property changes are batched.
await previousUpdatePromise;
this._updatePromise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
try {
// Ensure any previous update has resolved before updating.
// This `await` also ensures that property changes are batched.
await previousUpdatePromise;
}
catch (e) {
// Ignore any previous errors. We only care that the previous cycle is
// done. Any error should have been handled in the previous update.
}
// Make sure the element has connected before updating.
if (!this._hasConnected) {
await new Promise((res) => this._hasConnectedResolver = res);
}
// Allow `performUpdate` to be asynchronous to enable scheduling of updates.
const result = this.performUpdate();
// Note, this is to avoid delaying an additional microtask unless we need
// to.
if (result != null &&
typeof result.then === 'function') {
await result;
try {
const result = this.performUpdate();
// If `performUpdate` returns a Promise, we await it. This is done to
// enable coordinating updates with a scheduler. Note, the result is
// checked to avoid delaying an additional microtask unless we need to.
if (result != null) {
await result;
}
}
catch (e) {
reject(e);
}
resolve(!this._hasRequestedUpdate);
}
@@ -1846,10 +1870,13 @@ class UpdatingElement extends HTMLElement {
return (this._updateState & STATE_HAS_UPDATED);
}
/**
* Performs an element update.
* Performs an element update. Note, if an exception is thrown during the
* update, `firstUpdated` and `updated` will not be called.
*
* You can override this method to change the timing of updates. For instance,
* to schedule updates to occur just before the next frame:
* You can override this method to change the timing of updates. If this
* method is overridden, `super.performUpdate()` must be called.
*
* For instance, to schedule updates to occur just before the next frame:
*
* ```
* protected async performUpdate(): Promise<unknown> {
@@ -1863,19 +1890,31 @@ class UpdatingElement extends HTMLElement {
if (this._instanceProperties) {
this._applyInstanceProperties();
}
if (this.shouldUpdate(this._changedProperties)) {
const changedProperties = this._changedProperties;
this.update(changedProperties);
let shouldUpdate = false;
const changedProperties = this._changedProperties;
try {
shouldUpdate = this.shouldUpdate(changedProperties);
if (shouldUpdate) {
this.update(changedProperties);
}
}
catch (e) {
// Prevent `firstUpdated` and `updated` from running when there's an
// update exception.
shouldUpdate = false;
throw e;
}
finally {
// Ensure element can accept additional updates after an exception.
this._markUpdated();
}
if (shouldUpdate) {
if (!(this._updateState & STATE_HAS_UPDATED)) {
this._updateState = this._updateState | STATE_HAS_UPDATED;
this.firstUpdated(changedProperties);
}
this.updated(changedProperties);
}
else {
this._markUpdated();
}
}
_markUpdated() {
this._changedProperties = new Map();
@@ -1885,7 +1924,8 @@ class UpdatingElement extends HTMLElement {
* Returns a Promise that resolves when the element has completed updating.
* The Promise value is a boolean that is `true` if the element completed the
* update without triggering another update. The Promise result is `false` if
* a property was set inside `updated()`. This getter can be implemented to
* a property was set inside `updated()`. If the Promise is rejected, an
* exception was thrown during the update. This getter can be implemented to
* await additional state. For example, it is sometimes useful to await a
* rendered element before fulfilling this Promise. To do this, first await
* `super.updateComplete` then any subsequent state.
@@ -2196,7 +2236,8 @@ class LitElement extends UpdatingElement {
*/
initialize() {
super.initialize();
this.renderRoot = this.createRenderRoot();
this.renderRoot =
this.createRenderRoot();
// Note, if renderRoot is not a shadowRoot, styles would/could apply to the
// element's getRootNode(). While this could be done, we're choosing not to
// support this now since it would require different logic around de-duping.
@@ -2302,15 +2343,38 @@ LitElement.finalized = true;
*/
LitElement.render = render$1;
/**
* Parse or format dates
* @class fecha
*/
function shorten(arr, sLen) {
var newArr = [];
for (var i = 0, len = arr.length; i < len; i++) {
newArr.push(arr[i].substr(0, sLen));
}
return newArr;
}
var dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
var monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
var monthNamesShort = shorten(monthNames, 3);
var dayNamesShort = shorten(dayNames, 3);
function e(t){return t.substr(0,t.indexOf("."))}!function(){try{(new Date).toLocaleDateString("i");}catch(t){return "RangeError"===t.name}}(),function(){try{(new Date).toLocaleString("i");}catch(t){return "RangeError"===t.name}}(),function(){try{(new Date).toLocaleTimeString("i");}catch(t){return "RangeError"===t.name}}();var s=["closed","locked","off"],h=function(t,e,n,o){o=o||{},n=null==n?{}:n;var i=new Event(e,{bubbles:void 0===o.bubbles||o.bubbles,cancelable:Boolean(o.cancelable),composed:void 0===o.composed||o.composed});return i.detail=n,t.dispatchEvent(i),i},m=function(t,e,n){void 0===n&&(n=!1),n?history.replaceState(null,"",e):history.pushState(null,"",e),h(window,"location-changed",{replace:n});},v=function(t,n,o){void 0===o&&(o=!0);var i,r=e(n),a="group"===r?"homeassistant":r;switch(r){case"lock":i=o?"unlock":"lock";break;case"cover":i=o?"open_cover":"close_cover";break;default:i=o?"turn_on":"turn_off";}return t.callService(a,i,{entity_id:n})},f=function(t,e){var n=s.includes(t.states[e].state);return v(t,e,n)},w=function(t,e){h(t,"haptic",e);},g=function(t,e,n,o){var i;switch(o&&n.hold_action?i=n.hold_action:!o&&n.tap_action&&(i=n.tap_action),i||(i={action:"more-info"}),i.action){case"more-info":n.entity&&(h(t,"hass-more-info",{entityId:n.entity}),i.haptic&&w(t,i.haptic));break;case"navigate":i.navigation_path&&(m(0,i.navigation_path),i.haptic&&w(t,i.haptic));break;case"url":i.url&&window.open(i.url),i.haptic&&w(t,i.haptic);break;case"toggle":n.entity&&(f(e,n.entity),i.haptic&&w(t,i.haptic));break;case"call-service":if(!i.service)return;var r=i.service.split(".",2);e.callService(r[0],r[1],i.service_data),i.haptic&&w(t,i.haptic);}};function _(t,e){if(e.has("_config"))return !0;var n=e.get("hass");return !n||n.states[t._config.entity]!==t.hass.states[t._config.entity]}const b=new WeakMap;String(Math.random()).slice(2);try{const t={get capture(){return !1}};window.addEventListener("test",t,t),window.removeEventListener("test",t,t);}catch(t){}(window.litHtmlVersions||(window.litHtmlVersions=[])).push("1.0.0");var y="ontouchstart"in window||navigator.maxTouchPoints>0||navigator.msMaxTouchPoints>0,E=function(t){function e(){t.call(this),this.holdTime=500,this.ripple=document.createElement("paper-ripple"),this.timer=void 0,this.held=!1,this.cooldownStart=!1,this.cooldownEnd=!1;}return t&&(e.__proto__=t),(e.prototype=Object.create(t&&t.prototype)).constructor=e,e.prototype.connectedCallback=function(){var t=this;Object.assign(this.style,{borderRadius:"50%",position:"absolute",width:y?"100px":"50px",height:y?"100px":"50px",transform:"translate(-50%, -50%)",pointerEvents:"none"}),this.appendChild(this.ripple),this.ripple.style.color="#03a9f4",this.ripple.style.color="var(--primary-color)",["touchcancel","mouseout","mouseup","touchmove","mousewheel","wheel","scroll"].forEach(function(e){document.addEventListener(e,function(){clearTimeout(t.timer),t.stopAnimation(),t.timer=void 0;},{passive:!0});});},e.prototype.bind=function(t){var e=this;if(!t.longPress){t.longPress=!0,t.addEventListener("contextmenu",function(t){var e=t||window.event;return e.preventDefault&&e.preventDefault(),e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0,e.returnValue=!1,!1});var n=function(t){var n,o;e.cooldownStart||(e.held=!1,t.touches?(n=t.touches[0].pageX,o=t.touches[0].pageY):(n=t.pageX,o=t.pageY),e.timer=window.setTimeout(function(){e.startAnimation(n,o),e.held=!0;},e.holdTime),e.cooldownStart=!0,window.setTimeout(function(){return e.cooldownStart=!1},100));},o=function(n){e.cooldownEnd||["touchend","touchcancel"].includes(n.type)&&void 0===e.timer||(clearTimeout(e.timer),e.stopAnimation(),e.timer=void 0,t.dispatchEvent(e.held?new Event("ha-hold"):new Event("ha-click")),e.cooldownEnd=!0,window.setTimeout(function(){return e.cooldownEnd=!1},100));};t.addEventListener("touchstart",n,{passive:!0}),t.addEventListener("touchend",o),t.addEventListener("touchcancel",o),t.addEventListener("mousedown",n,{passive:!0}),t.addEventListener("click",o);}},e.prototype.startAnimation=function(t,e){Object.assign(this.style,{left:t+"px",top:e+"px",display:null}),this.ripple.holdDown=!0,this.ripple.simulatedRipple();},e.prototype.stopAnimation=function(){this.ripple.holdDown=!1,this.style.display="none";},e}(HTMLElement);customElements.get("long-press")||customElements.define("long-press",E);var k=function(t){var e=function(){var t=document.body;if(t.querySelector("long-press"))return t.querySelector("long-press");var e=document.createElement("long-press");return t.appendChild(e),e}();e&&e.bind(t);},S=(t=>(...t)=>{const e=function(){return function(t){k(t.committer.element);}}(...t);return b.set(e,!0),e})();
// TODO Name your custom element
let BoilerplateCard = class BoilerplateCard extends LitElement {
setConfig(config) {
// TODO Check for required fields and that they are of the proper format
if (!config || config.show_error) {
throw new Error('Invalid configuration');
throw new Error("Invalid configuration");
}
this._config = config;
}
shouldUpdate(changedProps) {
return _(this, changedProps);
}
render() {
if (!this._config || !this.hass) {
return html ``;
@@ -2324,9 +2388,20 @@ let BoilerplateCard = class BoilerplateCard extends LitElement {
`;
}
return html `
<ha-card .header=${this._config.name ? this._config.name : 'Boilerplate'}></ha-card>
<ha-card
.header=${this._config.name ? this._config.name : "Boilerplate"}
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longpress="${S()}"
></ha-card>
`;
}
_handleTap() {
g(this, this.hass, this._config, false);
}
_handleHold() {
g(this, this.hass, this._config, true);
}
static get styles() {
return css `
.warning {
@@ -2345,5 +2420,5 @@ __decorate([
property()
], BoilerplateCard.prototype, "_config", void 0);
BoilerplateCard = __decorate([
customElement('boilerplate-card')
customElement("boilerplate-card")
], BoilerplateCard);
+9 -3
View File
@@ -1,6 +1,6 @@
{
"name": "boilerplate-card",
"version": "1.0.0",
"version": "1.1.0",
"description": "Lovelace boilerplate-card",
"keywords": [
"home-assistant",
@@ -15,16 +15,22 @@
"author": "BoilerPlate <boilerplate@email.com>",
"license": "MIT",
"dependencies": {
"custom-card-helpers": "^1.0.8",
"home-assistant-js-websocket": "^3.4.0",
"lit-element": "^2.0.1"
},
"devDependencies": {
"@babel/core": "^7.4.3",
"@babel/plugin-proposal-class-properties": "^7.4.0",
"@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",
"eslint-plugin-typescript": "^0.14.0",
"prettier": "^1.16.4",
"rollup": "^1.2.3",
"rollup-plugin-babel": "^4.3.2",
"rollup-plugin-node-resolve": "^4.0.1",
"rollup-plugin-typescript2": "^0.19.2",
"typescript": "^3.3.3333"
@@ -32,7 +38,7 @@
"scripts": {
"start": "rollup -c --watch",
"build": "npm run lint && npm run rollup",
"lint": "eslint src/index.ts",
"lint": "eslint src/*.ts",
"rollup": "rollup -c"
}
}
+14 -24
View File
@@ -1,27 +1,17 @@
import resolve from 'rollup-plugin-node-resolve';
import typescript from 'rollup-plugin-typescript2';
import typescript from 'rollup-plugin-typescript2';
import babel from 'rollup-plugin-babel';
const commonPlugins = [
export default {
input: ['src/card.ts'],
output: {
dir: './dist',
format: 'es',
},
plugins: [
resolve(),
typescript()
];
export default [
{
input: 'src/index.ts',
output: {
file: 'boilerplate-card.js',
format: 'es'
},
plugins: [...commonPlugins]
},
{
input: 'src/editor.ts',
output: {
file: 'boilerplate-card-editor.js',
format: 'es'
},
plugins: [...commonPlugins]
}
]
typescript(),
babel({
exclude: 'node_modules/**'
})],
};
+30 -12
View File
@@ -6,33 +6,38 @@ import {
CSSResult,
TemplateResult,
css,
} from 'lit-element';
PropertyValues
} from "lit-element";
import {
HomeAssistant,
handleClick,
longPress,
hasConfigOrEntityChanged
} from "custom-card-helpers";
// TODO Add your configuration elements here for type-checking
interface BoilerplateConfig {
type: string;
name?: string;
show_warning?: boolean;
show_error?: boolean;
}
import { BoilerplateConfig } from "./types";
// TODO Name your custom element
@customElement('boilerplate-card')
@customElement("boilerplate-card")
class BoilerplateCard extends LitElement {
// TODO Add any properities that should cause your element to re-render here
@property() public hass?: any;
@property() public hass?: HomeAssistant;
@property() private _config?: BoilerplateConfig;
public setConfig(config: BoilerplateConfig): void {
// TODO Check for required fields and that they are of the proper format
if (!config || config.show_error) {
throw new Error('Invalid configuration');
throw new Error("Invalid configuration");
}
this._config = config;
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
return hasConfigOrEntityChanged(this, changedProps);
}
protected render(): TemplateResult | void {
if (!this._config || !this.hass) {
return html``;
@@ -48,10 +53,23 @@ class BoilerplateCard extends LitElement {
}
return html`
<ha-card .header=${this._config.name ? this._config.name : 'Boilerplate'}></ha-card>
<ha-card
.header=${this._config.name ? this._config.name : "Boilerplate"}
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longpress="${longPress()}"
></ha-card>
`;
}
private _handleTap(): void {
handleClick(this, this.hass!, this._config!, false);
}
private _handleHold(): void {
handleClick(this, this.hass!, this._config!, true);
}
static get styles(): CSSResult {
return css`
.warning {
+1
View File
@@ -0,0 +1 @@
// TODO
+12
View File
@@ -0,0 +1,12 @@
import { ActionConfig } from "custom-card-helpers";
// TODO Add your configuration elements here for type-checking
export interface BoilerplateConfig {
type: string;
name?: string;
show_warning?: boolean;
show_error?: boolean;
entity?: string;
tap_aciton?: ActionConfig;
hold_action?: ActionConfig;
}
+2414
View File
File diff suppressed because it is too large Load Diff