Repeatable hold_action, double tap & light color in css var (#148)

* Repeatable hold_action

* Cleanup

* lock customization

* CSS reorder

* Support for double click

* light entity css color variable

* doc fix

* css fix
This commit is contained in:
Jérôme W 2019-05-10 14:49:44 +02:00 committed by GitHub
parent c0f50ff675
commit 8023fd8265
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 315 additions and 136 deletions

View File

@ -25,6 +25,7 @@ Lovelace Button card for your entities.
- [Templates](#templates)
- [Styles](#styles)
- [Easy styling options](#easy-styling-options)
- [Light entity color variable](#light-entity-color-variable)
- [ADVANCED styling options](#advanced-styling-options)
- [Installation](#installation)
- [Manual Installation](#manual-installation)
@ -47,7 +48,7 @@ Lovelace Button card for your entities.
## Features
- works with any toggleable entity
- 6 available actions on **tap** and/or **hold**: `none`, `toggle`, `more-info`, `navigate`, `url` and `call-service`
- 6 available actions on **tap** and/or **hold** and/or **double click**: `none`, `toggle`, `more-info`, `navigate`, `url` and `call-service`
- state display (optional)
- custom color (optional), or based on light rgb value/temperature
- custom state definition with customizable color, icon and style (optional)
@ -82,6 +83,7 @@ Lovelace Button card for your entities.
| `size` | string | `40%` | `20px` | Size of the icon. Can be percentage or pixel |
| `tap_action` | object | optional | See [Action](#Action) | Define the type of action on click, if undefined, toggle will be used. |
| `hold_action` | object | optional | See [Action](#Action) | Define the type of action on hold, if undefined, nothing happens. |
| `dbltap_action` | object | optional | See [Action](#Action) | Define the type of action on double click, if undefined, nothing happens. |
| `name` | string | optional | `Air conditioner` | Define an optional text to show below the icon |
| `label` | string | optional | Any string that you want | Display a label below the card. See [Layouts](#layout) for more information. |
| `label_template` | string | optional | | See [templates](#templates). Any javascript code which returns a string. Overrides `label` |
@ -215,6 +217,7 @@ The `style` object members are:
* `name`: styles for the name
* `state`: styles for the state
* `label`: styles for the label
* `lock`: styles for the lock icon (see [here](https://github.com/custom-cards/button-card/blob/master/src/styles.ts#L36-L49) for the default style)
```yaml
- type: custom:button-card
@ -254,6 +257,21 @@ This will render:
See [styling](#styling) for a complete example.
#### Light entity color variable
If a light entity is assigned to the button, then the CSS variable `--button-card-light-color` will contain the current light color so that you can use it in other parts of the button. When off, it will be set to `var(--paper-item-icon-color)`
![css-var](examples/color-variable.gif)
```yaml
styles:
name:
color: var(--button-card-light-color)
card:
- -webkit-box-shadow: 0px 0px 9px 3px var(--button-card-light-color)
- box-shadow: 0px 0px 9px 3px var(--button-card-light-color)
```
#### ADVANCED styling options
For advanced styling, there are 2 other options in the `styles` config object:
@ -298,6 +316,7 @@ Some examples:
```
* Apple Homekit-like buttons:
![apple-like-buttons](examples/apple_style.gif)
```yaml
- type: custom:button-card

195
dist/button-card.js vendored
View File

@ -2394,6 +2394,36 @@ const unsafeHTML = directive(value => part => {
previousValues.set(part, { value, fragment });
});
/**
* @license
* Copyright (c) 2018 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
/**
* For AttributeParts, sets the attribute if the value is defined and removes
* the attribute if the value is undefined.
*
* For other part types, this directive is a no-op.
*/
const ifDefined = directive(value => part => {
if (value === undefined && part instanceof AttributePart) {
if (value !== part.value) {
const name = part.committer.name;
part.committer.element.removeAttribute(name);
}
} else {
part.setValue(value);
}
});
/** Constants to be used in the frontend. */
// Constants should be alphabetically sorted by name.
// Arrays with values should be alphabetically sorted if order doesn't matter.
@ -3510,9 +3540,11 @@ const forwardHaptic = (el, hapticType) => {
fireEvent(el, "haptic", hapticType);
};
const handleClick = (node, hass, config, hold) => {
const handleClick = (node, hass, config, hold, dblClick) => {
let actionConfig;
if (hold && config.hold_action) {
if (dblClick && config.dbltap_action) {
actionConfig = config.dbltap_action;
} else if (hold && config.hold_action) {
actionConfig = config.hold_action;
} else if (!hold && config.tap_action) {
actionConfig = config.tap_action;
@ -3561,32 +3593,33 @@ const handleClick = (node, hass, config, hold) => {
// See https://github.com/home-assistant/home-assistant-polymer/pull/2457
// on how to undo mwc -> paper migration
// import "@material/mwc-ripple";
const isTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
// import '@material/mwc-ripple';
const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
class LongPress extends HTMLElement {
constructor() {
super();
this.holdTime = 500;
this.ripple = document.createElement("paper-ripple");
this.ripple = document.createElement('paper-ripple');
this.timer = undefined;
this.held = false;
this.cooldownStart = false;
this.cooldownEnd = false;
this.nbClicks = 0;
}
connectedCallback() {
Object.assign(this.style, {
borderRadius: "50%",
position: "absolute",
width: isTouch ? "100px" : "50px",
height: isTouch ? "100px" : "50px",
transform: "translate(-50%, -50%)",
pointerEvents: "none"
borderRadius: '50%',
position: 'absolute',
width: isTouch ? '100px' : '50px',
height: isTouch ? '100px' : '50px',
transform: 'translate(-50%, -50%)',
pointerEvents: 'none'
});
this.appendChild(this.ripple);
this.ripple.style.color = "#03a9f4"; // paper-ripple
this.ripple.style.color = "var(--primary-color)"; // paper-ripple
this.ripple.style.color = '#03a9f4'; // paper-ripple
this.ripple.style.color = 'var(--primary-color)'; // paper-ripple
// this.ripple.primary = true;
["touchcancel", "mouseout", "mouseup", "touchmove", "mousewheel", "wheel", "scroll"].forEach(ev => {
['touchcancel', 'mouseout', 'mouseup', 'touchmove', 'mousewheel', 'wheel', 'scroll'].forEach(ev => {
document.addEventListener(ev, () => {
clearTimeout(this.timer);
this.stopAnimation();
@ -3595,11 +3628,12 @@ class LongPress extends HTMLElement {
});
}
bind(element) {
/* eslint no-param-reassign: 0 */
if (element.longPress) {
return;
}
element.longPress = true;
element.addEventListener("contextmenu", ev => {
element.addEventListener('contextmenu', ev => {
const e = ev || window.event;
if (e.preventDefault) {
e.preventDefault();
@ -3628,30 +3662,60 @@ class LongPress extends HTMLElement {
this.timer = window.setTimeout(() => {
this.startAnimation(x, y);
this.held = true;
if (element.repeat && !element.isRepeating) {
element.isRepeating = true;
this.repeatTimeout = setInterval(() => {
element.dispatchEvent(new Event('ha-hold'));
}, element.repeat);
}
}, this.holdTime);
this.cooldownStart = true;
window.setTimeout(() => this.cooldownStart = false, 100);
};
const clickEnd = ev => {
if (this.cooldownEnd || ["touchend", "touchcancel"].includes(ev.type) && this.timer === undefined) {
if (this.cooldownEnd || ['touchend', 'touchcancel'].includes(ev.type) && this.timer === undefined) {
if (element.isRepeating && this.repeatTimeout) {
clearInterval(this.repeatTimeout);
element.isRepeating = false;
}
return;
}
clearTimeout(this.timer);
if (element.isRepeating && this.repeatTimeout) {
clearInterval(this.repeatTimeout);
}
element.isRepeating = false;
this.stopAnimation();
this.timer = undefined;
if (this.held) {
element.dispatchEvent(new Event("ha-hold"));
if (!element.repeat) {
element.dispatchEvent(new Event('ha-hold'));
}
} else if (element.hasDblClick) {
if (this.nbClicks === 0) {
this.nbClicks += 1;
this.dblClickTimeout = window.setTimeout(() => {
if (this.nbClicks === 1) {
this.nbClicks = 0;
element.dispatchEvent(new Event('ha-click'));
}
}, 250);
} else {
this.nbClicks = 0;
clearTimeout(this.dblClickTimeout);
element.dispatchEvent(new Event('ha-dblclick'));
}
} else {
element.dispatchEvent(new Event("ha-click"));
element.dispatchEvent(new Event('ha-click'));
}
this.cooldownEnd = true;
window.setTimeout(() => this.cooldownEnd = false, 100);
};
element.addEventListener("touchstart", clickStart, { passive: true });
element.addEventListener("touchend", clickEnd);
element.addEventListener("touchcancel", clickEnd);
element.addEventListener("mousedown", clickStart, { passive: true });
element.addEventListener("click", clickEnd);
element.addEventListener('touchstart', clickStart, { passive: true });
element.addEventListener('touchend', clickEnd);
element.addEventListener('touchcancel', clickEnd);
element.addEventListener('mousedown', clickStart, { passive: true });
element.addEventListener('click', clickEnd);
}
startAnimation(x, y) {
Object.assign(this.style, {
@ -3669,16 +3733,16 @@ class LongPress extends HTMLElement {
this.ripple.holdDown = false; // paper-ripple
// this.ripple.active = false;
// this.ripple.disabled = true;
this.style.display = "none";
this.style.display = 'none';
}
}
customElements.define("long-press-button-card", LongPress);
customElements.define('long-press-button-card', LongPress);
const getLongPress = () => {
const body = document.body;
if (body.querySelector("long-press-button-card")) {
return body.querySelector("long-press-button-card");
if (body.querySelector('long-press-button-card')) {
return body.querySelector('long-press-button-card');
}
const longpress = document.createElement("long-press-button-card");
const longpress = document.createElement('long-press-button-card');
body.appendChild(longpress);
return longpress;
};
@ -3727,22 +3791,25 @@ const styles = css`
overflow: hidden;
}
#overlay {
align-items: flex-start;
justify-content: flex-end;
padding: 8px 7px;
opacity: 0.5;
/* DO NOT override items below */
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
text-align: right;
z-index: 1;
display: flex;
}
#lock {
margin-top: 8px;
opacity: 0.5;
margin-right: 7px;
-webkit-animation-duration: 5s;
animation-duration: 5s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
margin: unset;
}
@keyframes fadeOut{
0% {opacity: 0.5;}
@ -4016,7 +4083,7 @@ const styles = css`
#container.icon_state_name2nd #name {
align-self: center;
}
#container.icon_state_name2nd #state {
#container.icon_state_name2nd #label {
align-self: start;
}
@ -4145,6 +4212,27 @@ let ButtonCard = class ButtonCard extends LitElement {
return this.config.default_color;
}
}
_getColorForLightEntity(state) {
let color = this.config.default_color;
if (state) {
if (state.attributes.rgb_color) {
color = `rgb(${state.attributes.rgb_color.join(',')})`;
if (state.attributes.brightness) {
color = applyBrightnessToColor(color, (state.attributes.brightness + 245) / 5);
}
} else if (state.attributes.color_temp && state.attributes.min_mireds && state.attributes.max_mireds) {
color = getLightColorBasedOnTemperature(state.attributes.color_temp, state.attributes.min_mireds, state.attributes.max_mireds);
if (state.attributes.brightness) {
color = applyBrightnessToColor(color, (state.attributes.brightness + 245) / 5);
}
} else if (state.attributes.brightness) {
color = applyBrightnessToColor(this._getDefaultColorForState(state), (state.attributes.brightness + 245) / 5);
} else {
color = this._getDefaultColorForState(state);
}
}
return color;
}
_buildCssColorAttribute(state, configState) {
let colorValue = '';
let color;
@ -4156,25 +4244,7 @@ let ButtonCard = class ButtonCard extends LitElement {
colorValue = this.config.color;
}
if (colorValue == 'auto') {
if (state) {
if (state.attributes.rgb_color) {
color = `rgb(${state.attributes.rgb_color.join(',')})`;
if (state.attributes.brightness) {
color = applyBrightnessToColor(color, (state.attributes.brightness + 245) / 5);
}
} else if (state.attributes.color_temp && state.attributes.min_mireds && state.attributes.max_mireds) {
color = getLightColorBasedOnTemperature(state.attributes.color_temp, state.attributes.min_mireds, state.attributes.max_mireds);
if (state.attributes.brightness) {
color = applyBrightnessToColor(color, (state.attributes.brightness + 245) / 5);
}
} else if (state.attributes.brightness) {
color = applyBrightnessToColor(this._getDefaultColorForState(state), (state.attributes.brightness + 245) / 5);
} else {
color = this._getDefaultColorForState(state);
}
} else {
color = this.config.default_color;
}
color = this._getColorForLightEntity(state);
} else if (colorValue) {
color = colorValue;
} else if (state) {
@ -4330,7 +4400,8 @@ let ButtonCard = class ButtonCard extends LitElement {
const color = this._buildCssColorAttribute(state, configState);
let buttonColor = color;
let cardStyle = {};
const lockStyle = {};
let lockStyle = {};
const lockStyleFromConfig = this._buildStyleGeneric(configState, 'lock');
const configCardStyle = this._buildStyleGeneric(configState, 'card');
if (configCardStyle.width) {
this.style.setProperty('flex', '0 0 auto');
@ -4354,8 +4425,10 @@ let ButtonCard = class ButtonCard extends LitElement {
cardStyle = configCardStyle;
break;
}
this.style.setProperty('--button-card-light-color', this._getColorForLightEntity(state));
lockStyle = Object.assign({}, lockStyle, lockStyleFromConfig);
return html`
<ha-card class="button-card-main ${this._isClickable(state) ? '' : 'disabled'}" style=${styleMap(cardStyle)} @ha-click="${this._handleTap}" @ha-hold="${this._handleHold}" .longpress="${longPress()}" .config="${this.config}">
<ha-card class="button-card-main ${this._isClickable(state) ? '' : 'disabled'}" style=${styleMap(cardStyle)} @ha-click="${this._handleTap}" @ha-hold="${this._handleHold}" @ha-dblclick=${this._handleDblTap} .hasDblClick=${this.config.dbltap_action.action !== 'none'} .repeat=${ifDefined(this.config.hold_action.repeat)} .longpress="${longPress()}" .config="${this.config}">
${this._getLock(lockStyle)}
${this._buttonContent(state, configState, buttonColor)}
${this.config.lock ? '' : html`<mwc-ripple id="ripple"></mwc-ripple>`}
@ -4432,7 +4505,7 @@ let ButtonCard = class ButtonCard extends LitElement {
if (!config) {
throw new Error('Invalid configuration');
}
this.config = Object.assign({ tap_action: { action: 'toggle' }, hold_action: { action: 'none' }, layout: 'vertical', size: '40%', color_type: 'icon', show_name: true, show_state: false, show_icon: true, show_units: true, show_label: false, show_entity_picture: false }, config);
this.config = Object.assign({ tap_action: { action: 'toggle' }, hold_action: { action: 'none' }, dbltap_action: { action: 'none' }, layout: 'vertical', size: '40%', color_type: 'icon', show_name: true, show_state: false, show_icon: true, show_units: true, show_label: false, show_entity_picture: false }, config);
this.config.default_color = 'var(--primary-text-color)';
if (this.config.color_type !== 'icon') {
this.config.color_off = 'var(--paper-card-background-color)';
@ -4476,7 +4549,7 @@ let ButtonCard = class ButtonCard extends LitElement {
return;
}
const config = ev.target.config;
handleClick(this, this.hass, config, false);
handleClick(this, this.hass, config, false, false);
}
_handleHold(ev) {
/* eslint no-alert: 0 */
@ -4484,7 +4557,15 @@ let ButtonCard = class ButtonCard extends LitElement {
return;
}
const config = ev.target.config;
handleClick(this, this.hass, config, true);
handleClick(this, this.hass, config, true, false);
}
_handleDblTap(ev) {
/* eslint no-alert: 0 */
if (this.config.confirmation && !window.confirm(this.config.confirmation)) {
return;
}
const config = ev.target.config;
handleClick(this, this.hass, config, false, true);
}
_handleLock(ev) {
ev.stopPropagation();

BIN
examples/color-variable.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

View File

@ -9,6 +9,7 @@ import {
} from 'lit-element';
import { styleMap, StyleInfo } from 'lit-html/directives/style-map';
import { unsafeHTML } from 'lit-html/directives/unsafe-html';
import { ifDefined } from 'lit-html/directives/if-defined';
import {
HassEntity,
} from 'home-assistant-js-websocket';
@ -128,6 +129,36 @@ class ButtonCard extends LitElement {
}
}
private _getColorForLightEntity(state: HassEntity | undefined): string {
let color: string = this.config!.default_color;
if (state) {
if (state.attributes.rgb_color) {
color = `rgb(${state.attributes.rgb_color.join(',')})`;
if (state.attributes.brightness) {
color = applyBrightnessToColor(color, (state.attributes.brightness + 245) / 5);
}
} else if (state.attributes.color_temp
&& state.attributes.min_mireds
&& state.attributes.max_mireds) {
color = getLightColorBasedOnTemperature(
state.attributes.color_temp,
state.attributes.min_mireds,
state.attributes.max_mireds,
);
if (state.attributes.brightness) {
color = applyBrightnessToColor(color, (state.attributes.brightness + 245) / 5);
}
} else if (state.attributes.brightness) {
color = applyBrightnessToColor(
this._getDefaultColorForState(state), (state.attributes.brightness + 245) / 5,
);
} else {
color = this._getDefaultColorForState(state);
}
}
return color;
}
private _buildCssColorAttribute(
state: HassEntity | undefined, configState: StateConfig | undefined,
): string {
@ -141,33 +172,7 @@ class ButtonCard extends LitElement {
colorValue = this.config!.color;
}
if (colorValue == 'auto') {
if (state) {
if (state.attributes.rgb_color) {
color = `rgb(${state.attributes.rgb_color.join(',')})`;
if (state.attributes.brightness) {
color = applyBrightnessToColor(color, (state.attributes.brightness + 245) / 5);
}
} else if (state.attributes.color_temp
&& state.attributes.min_mireds
&& state.attributes.max_mireds) {
color = getLightColorBasedOnTemperature(
state.attributes.color_temp,
state.attributes.min_mireds,
state.attributes.max_mireds,
);
if (state.attributes.brightness) {
color = applyBrightnessToColor(color, (state.attributes.brightness + 245) / 5);
}
} else if (state.attributes.brightness) {
color = applyBrightnessToColor(
this._getDefaultColorForState(state), (state.attributes.brightness + 245) / 5,
);
} else {
color = this._getDefaultColorForState(state);
}
} else {
color = this.config!.default_color;
}
color = this._getColorForLightEntity(state);
} else if (colorValue) {
color = colorValue;
} else if (state) {
@ -370,7 +375,8 @@ class ButtonCard extends LitElement {
const color = this._buildCssColorAttribute(state, configState);
let buttonColor = color;
let cardStyle: StyleInfo = {};
const lockStyle: StyleInfo = {};
let lockStyle: StyleInfo = {};
const lockStyleFromConfig = this._buildStyleGeneric(configState, 'lock');
const configCardStyle = this._buildStyleGeneric(configState, 'card');
if (configCardStyle.width) {
@ -394,9 +400,11 @@ class ButtonCard extends LitElement {
cardStyle = configCardStyle;
break;
}
this.style.setProperty('--button-card-light-color', this._getColorForLightEntity(state));
lockStyle = { ...lockStyle, ...lockStyleFromConfig };
return html`
<ha-card class="button-card-main ${this._isClickable(state) ? '' : 'disabled'}" style=${styleMap(cardStyle)} @ha-click="${this._handleTap}" @ha-hold="${this._handleHold}" .longpress="${longPress()}" .config="${this.config}">
<ha-card class="button-card-main ${this._isClickable(state) ? '' : 'disabled'}" style=${styleMap(cardStyle)} @ha-click="${this._handleTap}" @ha-hold="${this._handleHold}" @ha-dblclick=${this._handleDblTap} .hasDblClick=${this.config!.dbltap_action!.action !== 'none'} .repeat=${ifDefined(this.config!.hold_action!.repeat)} .longpress="${longPress()}" .config="${this.config}">
${this._getLock(lockStyle)}
${this._buttonContent(state, configState, buttonColor)}
${this.config!.lock ? '' : html`<mwc-ripple id="ripple"></mwc-ripple>`}
@ -510,6 +518,7 @@ class ButtonCard extends LitElement {
this.config = {
tap_action: { action: 'toggle' },
hold_action: { action: 'none' },
dbltap_action: { action: 'none' },
layout: 'vertical',
size: '40%',
color_type: 'icon',
@ -568,7 +577,7 @@ class ButtonCard extends LitElement {
return;
}
const config = ev.target.config;
handleClick(this, this.hass!, config, false);
handleClick(this, this.hass!, config, false, false);
}
private _handleHold(ev): void {
@ -578,7 +587,17 @@ class ButtonCard extends LitElement {
return;
}
const config = ev.target.config;
handleClick(this, this.hass!, config, true);
handleClick(this, this.hass!, config, true, false);
}
private _handleDblTap(ev): void {
/* eslint no-alert: 0 */
if (this.config!.confirmation
&& !window.confirm(this.config!.confirmation)) {
return;
}
const config = ev.target.config;
handleClick(this, this.hass!, config, false, true);
}
private _handleLock(ev): void {

View File

@ -12,12 +12,16 @@ export const handleClick = (
camera_image?: string;
hold_action?: ActionConfig;
tap_action?: ActionConfig;
dbltap_action?: ActionConfig;
},
hold: boolean
hold: boolean,
dblClick: boolean,
): void => {
let actionConfig: ActionConfig | undefined;
if (hold && config.hold_action) {
if (dblClick && config.dbltap_action) {
actionConfig = config.dbltap_action;
} else if (hold && config.hold_action) {
actionConfig = config.hold_action;
} else if (!hold && config.tap_action) {
actionConfig = config.tap_action;
@ -63,4 +67,4 @@ export const handleClick = (
if (actionConfig.haptic) forwardHaptic(node, actionConfig.haptic);
}
}
};
};

View File

@ -1,12 +1,11 @@
import { directive, PropertyPart } from "lit-html";
import { directive, PropertyPart } from 'lit-html';
// See https://github.com/home-assistant/home-assistant-polymer/pull/2457
// on how to undo mwc -> paper migration
// import "@material/mwc-ripple";
// import '@material/mwc-ripple';
const isTouch =
"ontouchstart" in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0;
const isTouch = 'ontouchstart' in window
|| navigator.maxTouchPoints > 0
|| navigator.msMaxTouchPoints > 0;
interface LongPress extends HTMLElement {
holdTime: number;
@ -14,49 +13,64 @@ interface LongPress extends HTMLElement {
}
interface LongPressElement extends Element {
longPress?: boolean;
repeat?: number | undefined;
isRepeating?: boolean | undefined;
hasDblClick?: boolean | undefined;
}
class LongPress extends HTMLElement implements LongPress {
public holdTime: number;
protected ripple: any;
protected timer: number | undefined;
protected held: boolean;
protected cooldownStart: boolean;
protected cooldownEnd: boolean;
private repeatTimeout: NodeJS.Timeout | undefined;
private dblClickTimeout: number | undefined;
private nbClicks: number;
constructor() {
super();
this.holdTime = 500;
this.ripple = document.createElement("paper-ripple");
this.ripple = document.createElement('paper-ripple');
this.timer = undefined;
this.held = false;
this.cooldownStart = false;
this.cooldownEnd = false;
this.nbClicks = 0;
}
public connectedCallback() {
Object.assign(this.style, {
borderRadius: "50%", // paper-ripple
position: "absolute",
width: isTouch ? "100px" : "50px",
height: isTouch ? "100px" : "50px",
transform: "translate(-50%, -50%)",
pointerEvents: "none",
borderRadius: '50%', // paper-ripple
position: 'absolute',
width: isTouch ? '100px' : '50px',
height: isTouch ? '100px' : '50px',
transform: 'translate(-50%, -50%)',
pointerEvents: 'none',
});
this.appendChild(this.ripple);
this.ripple.style.color = "#03a9f4"; // paper-ripple
this.ripple.style.color = "var(--primary-color)"; // paper-ripple
this.ripple.style.color = '#03a9f4'; // paper-ripple
this.ripple.style.color = 'var(--primary-color)'; // paper-ripple
// this.ripple.primary = true;
[
"touchcancel",
"mouseout",
"mouseup",
"touchmove",
"mousewheel",
"wheel",
"scroll",
'touchcancel',
'mouseout',
'mouseup',
'touchmove',
'mousewheel',
'wheel',
'scroll',
].forEach((ev) => {
document.addEventListener(
ev,
@ -65,18 +79,19 @@ class LongPress extends HTMLElement implements LongPress {
this.stopAnimation();
this.timer = undefined;
},
{ passive: true }
{ passive: true },
);
});
}
public bind(element: LongPressElement) {
/* eslint no-param-reassign: 0 */
if (element.longPress) {
return;
}
element.longPress = true;
element.addEventListener("contextmenu", (ev: Event) => {
element.addEventListener('contextmenu', (ev: Event) => {
const e = ev || window.event;
if (e.preventDefault) {
e.preventDefault();
@ -106,6 +121,12 @@ class LongPress extends HTMLElement implements LongPress {
this.timer = window.setTimeout(() => {
this.startAnimation(x, y);
this.held = true;
if (element.repeat && !element.isRepeating) {
element.isRepeating = true;
this.repeatTimeout = setInterval(() => {
element.dispatchEvent(new Event('ha-hold'));
}, element.repeat);
}
}, this.holdTime);
this.cooldownStart = true;
@ -114,29 +135,53 @@ class LongPress extends HTMLElement implements LongPress {
const clickEnd = (ev: Event) => {
if (
this.cooldownEnd ||
(["touchend", "touchcancel"].includes(ev.type) &&
this.timer === undefined)
this.cooldownEnd
|| (['touchend', 'touchcancel'].includes(ev.type)
&& this.timer === undefined)
) {
if (element.isRepeating && this.repeatTimeout) {
clearInterval(this.repeatTimeout);
element.isRepeating = false;
}
return;
}
clearTimeout(this.timer);
if (element.isRepeating && this.repeatTimeout) {
clearInterval(this.repeatTimeout);
}
element.isRepeating = false;
this.stopAnimation();
this.timer = undefined;
if (this.held) {
element.dispatchEvent(new Event("ha-hold"));
if (!element.repeat) {
element.dispatchEvent(new Event('ha-hold'));
}
} else if (element.hasDblClick) {
if (this.nbClicks === 0) {
this.nbClicks += 1;
this.dblClickTimeout = window.setTimeout(() => {
if (this.nbClicks === 1) {
this.nbClicks = 0;
element.dispatchEvent(new Event('ha-click'));
}
}, 250);
} else {
this.nbClicks = 0;
clearTimeout(this.dblClickTimeout);
element.dispatchEvent(new Event('ha-dblclick'));
}
} else {
element.dispatchEvent(new Event("ha-click"));
element.dispatchEvent(new Event('ha-click'));
}
this.cooldownEnd = true;
window.setTimeout(() => (this.cooldownEnd = false), 100);
};
element.addEventListener("touchstart", clickStart, { passive: true });
element.addEventListener("touchend", clickEnd);
element.addEventListener("touchcancel", clickEnd);
element.addEventListener("mousedown", clickStart, { passive: true });
element.addEventListener("click", clickEnd);
element.addEventListener('touchstart', clickStart, { passive: true });
element.addEventListener('touchend', clickEnd);
element.addEventListener('touchcancel', clickEnd);
element.addEventListener('mousedown', clickStart, { passive: true });
element.addEventListener('click', clickEnd);
}
private startAnimation(x: number, y: number) {
@ -156,19 +201,19 @@ class LongPress extends HTMLElement implements LongPress {
this.ripple.holdDown = false; // paper-ripple
// this.ripple.active = false;
// this.ripple.disabled = true;
this.style.display = "none";
this.style.display = 'none';
}
}
customElements.define("long-press-button-card", LongPress);
customElements.define('long-press-button-card', LongPress);
const getLongPress = (): LongPress => {
const body = document.body;
if (body.querySelector("long-press-button-card")) {
return body.querySelector("long-press-button-card") as LongPress;
if (body.querySelector('long-press-button-card')) {
return body.querySelector('long-press-button-card') as LongPress;
}
const longpress = document.createElement("long-press-button-card");
const longpress = document.createElement('long-press-button-card');
body.appendChild(longpress);
return longpress as LongPress;

View File

@ -34,22 +34,25 @@ export const styles = css`
overflow: hidden;
}
#overlay {
align-items: flex-start;
justify-content: flex-end;
padding: 8px 7px;
opacity: 0.5;
/* DO NOT override items below */
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
text-align: right;
z-index: 1;
display: flex;
}
#lock {
margin-top: 8px;
opacity: 0.5;
margin-right: 7px;
-webkit-animation-duration: 5s;
animation-duration: 5s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
margin: unset;
}
@keyframes fadeOut{
0% {opacity: 0.5;}
@ -323,7 +326,7 @@ export const styles = css`
#container.icon_state_name2nd #name {
align-self: center;
}
#container.icon_state_name2nd #state {
#container.icon_state_name2nd #label {
align-self: start;
}

View File

@ -19,6 +19,7 @@ export interface ButtonCardConfig {
lock: boolean;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
dbltap_action?: ActionConfig;
show_name?: boolean;
show_state?: boolean;
show_icon?: boolean;
@ -75,6 +76,7 @@ export interface StylesConfig {
label?: CssStyleConfig[];
grid?: CssStyleConfig[];
img_cell?: CssStyleConfig[];
lock?: CssStyleConfig[];
}
export interface CssStyleConfig {
@ -83,12 +85,14 @@ export interface CssStyleConfig {
export interface ToggleActionConfig {
action: 'toggle';
repeat?: number | undefined;
haptic?: HapticType;
}
export interface CallServiceActionConfig {
action: 'call-service';
haptic?: HapticType;
repeat?: number | undefined;
service: string;
service_data?: {
entity_id?: string | [string];
@ -99,21 +103,25 @@ export interface CallServiceActionConfig {
export interface NavigateActionConfig {
action: 'navigate';
haptic?: HapticType;
repeat?: number | undefined;
navigation_path: string;
}
export interface MoreInfoActionConfig {
action: 'more-info';
repeat?: number | undefined;
haptic?: HapticType;
}
export interface NoActionConfig {
action: 'none';
repeat?: number | undefined;
}
export interface UrlActionConfig {
action: 'url';
haptic?: HapticType;
repeat?: number | undefined;
url: string;
}