Skip to content

Commit

Permalink
Climate card new design + various fixes
Browse files Browse the repository at this point in the history
- [x] New more compact design for the climate card, this new design don’t use a sub-button for the temperature anymore. Important: If you’ve added climate cards before, just open these cards one by one in the editor, the previous temperature sub-button will be automatically deleted. Don’t forget to save after that.
- [x] Full support for climate entities with `target_temp_low` and `target_temp_high` #877
- [x] In heat_cool mode, the climate card color now change based on if it’s actually heating or cooling. #863
- [x] Actions are not triggered when scrolling anymore, finally! #892 and #839
- [x] Fixed an issue where it was impossible to scroll anymore in some cases (like in the editor).
- [x] Added the missing bubble-accent-color variable in the media player play/pause button.
- [x] Update climate specific css vars #908 PR from @flobiwankenobi
- [x] New element for pop-up background, breaking change for some users with custom styles. #895
- [x] Underscore removing from list select #890
- [x] Some pop-ups were not displayed displayed correctly, or only when leaving the editor/changing view, this is now fixed! #821
- [x] Fixed some more pop-up issues (like console errors). #840 and #791
- [x] Cover open and close buttons are now disabled when they should. #788
- [x] Slider for controlling thermostat/fan should now works correctly. #849, #838 and #768
  • Loading branch information
Clooos authored Nov 13, 2024
1 parent 83b4f0d commit a8e6bde
Show file tree
Hide file tree
Showing 21 changed files with 458 additions and 617 deletions.
74 changes: 37 additions & 37 deletions dist/bubble-card.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/bubble-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { createBubbleCardEditor } from './editor/bubble-card-editor.ts';

class BubbleCard extends HTMLElement {
editor = false;
isConnected = false;
isConnected;

connectedCallback() {
this.isConnected = true;
Expand Down
43 changes: 21 additions & 22 deletions src/cards/button/create.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { addActions, addFeedback } from "../../tools/tap-actions.ts";
import { createElement, toggleEntity, throttle, forwardHaptic, isEntityType } from "../../tools/utils.ts";
import { getButtonType, onSliderChange } from "./helpers.ts";
import { getButtonType, onSliderChange, updateEntity } from "./helpers.ts";
import styles from "./styles.ts";

export function createStructure(context, container = context.content, appendTo = container) {
const buttonType = getButtonType(context);

context.dragging = false;

if (!context.elements) context.elements = {};
Expand All @@ -28,8 +27,8 @@ export function createStructure(context, container = context.content, appendTo =

context.elements.iconContainer.appendChild(context.elements.icon);
context.elements.iconContainer.appendChild(context.elements.image);

context.elements.nameContainer.appendChild(context.elements.name);

if (buttonType !== "name") {
context.elements.nameContainer.appendChild(context.elements.state);
}
Expand Down Expand Up @@ -58,6 +57,7 @@ export function createStructure(context, container = context.content, appendTo =
context.cardType = `button-${buttonType}`;
}
}

export function createSwitchStructure(context) {
addActions(context.elements.iconContainer, context.config);

Expand All @@ -69,6 +69,7 @@ export function createSwitchStructure(context) {
addActions(context.elements.buttonBackground, context.config.button_action, context.config.entity, switchDefaultActions);
addFeedback(context.elements.buttonBackground, context.elements.feedback);
}

export function createNameStructure(context) {
const nameDefaultActions = {
tap_action: { action: "none" },
Expand All @@ -80,6 +81,7 @@ export function createNameStructure(context) {
addActions(context.elements.buttonBackground, context.config.button_action, context.config.entity, nameDefaultActions);
addFeedback(context.elements.buttonBackground, context.elements.feedback);
}

export function createStateStructure(context) {
const stateDefaultActions = {
tap_action: { action: "more-info" },
Expand Down Expand Up @@ -107,16 +109,11 @@ export function createSliderStructure(context) {

context.elements.buttonCardContainer.addEventListener('pointercancel', onPointerCancel);
context.elements.buttonCardContainer.addEventListener('pointerdown', (e) => {
// Vérifie si l'élément cliqué a la classe .bubble-action
if (e.target.closest('.bubble-action')) {
return;
}
if (e.target.closest('.bubble-action')) return;

context.elements.buttonCardContainer.setPointerCapture(e.pointerId);

if (context.card.classList.contains('is-unavailable')) {
return;
}
if (context.card.classList.contains('is-unavailable')) return;

context.dragging = true;
initialX = e.pageX || (e.touches ? e.touches[0].pageX : 0);
Expand All @@ -136,21 +133,19 @@ export function createSliderStructure(context) {
window.removeEventListener('pointerup', onPointerUp);
}

const throttledUpdateEntity = throttle(updateEntity, 200);

function onPointerMove(e) {
e.stopPropagation();

// Ignore les mouvements de pointeur si l'élément cliqué a la classe .bubble-action
if (e.target.closest('.bubble-action')) {
return;
}
if (e.target.closest('.bubble-action')) return;

const moveX = e.pageX || (e.touches ? e.touches[0].pageX : 0);
if (Math.abs(initialX - moveX) > 10) {
onSliderChange(context, moveX, true);
}

const rangedPercentage = onSliderChange(context, moveX);

if (!context.dragging) {
onSliderChange(context, moveX);
if (context.config.slider_live_update) {
throttledUpdateEntity(context, rangedPercentage);
}
}

Expand All @@ -164,12 +159,16 @@ export function createSliderStructure(context) {
}, 1400);

const moveX = e.pageX || (e.touches ? e.touches[0].pageX : 0);
onSliderChange(context, moveX);
const finalPercentage = onSliderChange(context, moveX);

if (!context.config.slider_live_update) {
updateEntity(context, finalPercentage);
}

forwardHaptic("selection");

context.elements.buttonCardContainer.classList.remove('is-dragging');
context.elements.buttonCardContainer.removeEventListener('pointermove', onPointerMove);
window.removeEventListener('pointerup', onPointerUp);
}
}

}
128 changes: 17 additions & 111 deletions src/cards/button/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,98 +16,9 @@ export function getButtonType(context) {
}
}

export function createStructure(context, container = context.content, appendTo = container) {
const buttonType = getButtonType(context);

context.dragging = false;
if (!context.elements) context.elements = {};

context.elements.buttonCardContainer = createElement('div', 'bubble-button-card-container button-container');
context.elements.buttonCard = createElement('div', 'bubble-button-card switch-button');
context.elements.buttonBackground = createElement('div', 'bubble-button-background');
context.elements.nameContainer = createElement('div', 'bubble-name-container name-container');
context.elements.iconContainer = createElement('div', 'bubble-icon-container icon-container');
context.elements.name = createElement('div', 'bubble-name name');
context.elements.state = createElement('div', 'bubble-state state');
context.elements.feedback = createElement('div', 'bubble-feedback-element feedback-element');
context.elements.icon = createElement('ha-icon', 'bubble-icon icon');
context.elements.image = createElement('div', 'bubble-entity-picture entity-picture');
context.elements.style = createElement('style');
context.elements.customStyle = createElement('style');

context.elements.feedback.style.display = 'none';
context.elements.style.innerText = styles;

context.elements.iconContainer.appendChild(context.elements.icon);
context.elements.iconContainer.appendChild(context.elements.image);

context.elements.nameContainer.appendChild(context.elements.name);
if (buttonType !== "name") {
context.elements.nameContainer.appendChild(context.elements.state);
}

context.elements.buttonCard.appendChild(context.elements.buttonBackground);
context.elements.buttonCard.appendChild(context.elements.iconContainer);
context.elements.buttonCard.appendChild(context.elements.nameContainer);
context.elements.buttonCard.appendChild(context.elements.feedback);
context.elements.buttonCardContainer.appendChild(context.elements.buttonCard);

container.innerHTML = '';

if (appendTo === container) {
container.appendChild(context.elements.buttonCardContainer);
}

container.appendChild(context.elements.style);
container.appendChild(context.elements.customStyle);

if (appendTo !== container) {
appendTo.innerHTML = '';
context.elements.buttonCardContainer.appendChild(container);
appendTo.appendChild(context.elements.buttonCardContainer);
context.buttonType = buttonType;
} else {
context.cardType = `button-${buttonType}`;
}
}

export function createSwitchStructure(context) {
addActions(context.elements.iconContainer, context.config);

const switchDefaultActions = {
tap_action: { action: "toggle" },
double_tap_action: { action: "toggle" },
hold_action: { action: "more-info" }
};
addActions(context.elements.buttonBackground, context.config.button_action, context.config.entity, switchDefaultActions);
addFeedback(context.elements.buttonBackground, context.elements.feedback);
}

export function createNameStructure(context) {
const nameDefaultActions = {
tap_action: { action: "none" },
double_tap_action: { action: "none" },
hold_action: { action: "none" }
};

addActions(context.elements.iconContainer, context.config, context.config.entity, nameDefaultActions);
addActions(context.elements.buttonBackground, context.config.button_action, context.config.entity, nameDefaultActions);
addFeedback(context.elements.buttonBackground, context.elements.feedback);
}

export function createStateStructure(context) {
const stateDefaultActions = {
tap_action: { action: "more-info" },
double_tap_action: { action: "more-info" },
hold_action: { action: "more-info" }
};

addActions(context.elements.buttonCardContainer, context.config);
addActions(context.elements.buttonBackground, context.config.button_action, context.config.entity, stateDefaultActions);
addFeedback(context.elements.buttonBackground, context.elements.feedback);
}

export function updateEntity(context, value) {
const state = context._hass.states[context.config.entity];

if (isEntityType(context, "light")) {
context._hass.callService('light', 'turn_on', {
entity_id: context.config.entity,
Expand All @@ -124,8 +35,8 @@ export function updateEntity(context, value) {
position: Math.round(value)
});
} else if (isEntityType(context, "input_number")) {
const minValue = getAttribute(context, "min") ?? 0;
const maxValue = getAttribute(context, "max") ?? 100;
const minValue = state.attributes.min ?? 0;
const maxValue = state.attributes.max ?? 100;
const step = getAttribute(context, "step") ?? 1;
let rawValue = (maxValue - minValue) * value / 100 + minValue;
let adjustedValue = Math.round(rawValue / step) * step;
Expand All @@ -134,16 +45,17 @@ export function updateEntity(context, value) {
value: adjustedValue
});
} else if (isEntityType(context, "fan")) {
const step = getAttribute(context, "percentage_step") ?? 1;
const step = state.attributes.percentage_step ?? 1;
let adjustedValue = Math.round(value / step) * step;
context._hass.callService('fan', 'set_percentage', {
entity_id: context.config.entity,
percentage: adjustedValue
});
} else if (isEntityType(context, "climate")) {
const minValue = getAttribute(context, "min_temp");
const maxValue = getAttribute(context, "max_temp");
const step = getAttribute(context, "target_temp_step") ?? 0.5;
const minValue = state.attributes.min_temp ?? 0;
const maxValue = state.attributes.max_temp ?? 10000;
const isCelcius = context._hass.config.unit_system.temperature === '°C';
const step = state.attributes.target_temp_step ? state.attributes.target_temp_step : isCelcius ? 0.5 : 1;
let rawValue = (maxValue - minValue) * value / 100 + minValue;
let adjustedValue = Math.round(rawValue / step) * step;
adjustedValue = parseFloat(adjustedValue.toFixed(1));
Expand All @@ -152,9 +64,9 @@ export function updateEntity(context, value) {
temperature: adjustedValue
});
} else if (isEntityType(context, "number")) {
const minValue = getAttribute(context, "min") ?? 0;
const maxValue = getAttribute(context, "max") ?? 100;
const step = getAttribute(context, "step") ?? 1;
const minValue = state.attributes.min ?? 0;
const maxValue = state.attributes.max ?? 100;
const step = state.attributes.step ?? 1;
let rawValue = (maxValue - minValue) * value / 100 + minValue;
let adjustedValue = Math.round(rawValue / step) * step;
context._hass.callService('number', 'set_value', {
Expand All @@ -164,18 +76,12 @@ export function updateEntity(context, value) {
}
}

export const throttledUpdateEntity = throttle(updateEntity, 100);

export function onSliderChange(context, leftDistance, throttle = false) {
export function onSliderChange(context, leftDistance) {
const rect = context.elements.rangeSlider.getBoundingClientRect();
const percentage = 100 * (leftDistance - rect.left) / rect.width;
const rangedPercentage = Math.min(100, Math.max(0, percentage));

context.elements.rangeFill.style.transform =`translateX(${rangedPercentage}%)`;
if (throttle) {
if (context.dragging && !context.config.slider_live_update) return;
throttledUpdateEntity(context, rangedPercentage);
} else {
updateEntity(context, rangedPercentage);
}
}
context.elements.rangeFill.style.transform = `translateX(${rangedPercentage}%)`;

return rangedPercentage;
}
45 changes: 41 additions & 4 deletions src/cards/climate/changes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { initializesubButtonIcon } from '../../tools/global-changes.ts';
import { getClimateColor, getHvacModeIcon, getFanModeIcon, createHaAttributeIcon } from './helpers.ts';
import {
getClimateColor,
getHvacModeIcon,
getFanModeIcon,
createHaAttributeIcon
} from './helpers.ts';
import {
applyScrollingEffect,
getIcon,
Expand All @@ -18,10 +23,11 @@ export function changeIcon(context) {
const isOn = isStateOn(context);
const icon = getIcon(context);
const state = getState(context);
const stateObj = context._hass.states[context.config.entity];

if (icon !== '') {
context.elements.icon.icon = icon;
context.elements.icon.style.color = isOn ? getClimateColor(state) : 'inherit';
context.elements.icon.style.color = isOn ? getClimateColor(stateObj) : 'inherit';
context.elements.icon.style.display = '';
} else {
context.elements.icon.style.display = 'none';
Expand Down Expand Up @@ -54,17 +60,48 @@ export function changeStatus(context) {
}
}

export function changeTemperature(context) {
const currentTemp = getAttribute(context, "temperature");
if (currentTemp !== context.previousTemp) {
context.previousTemp = currentTemp;
if (context.elements.tempDisplay) {
context.elements.tempDisplay.innerText = parseFloat(currentTemp).toFixed(1);
}
}
}

export function changeTargetTempLow(context) {
const targetTempLow = getAttribute(context, "target_temp_low");
if (targetTempLow !== context.previousTargetTempLow) {
context.previousTargetTempLow = targetTempLow;
if (context.elements.lowTempDisplay) {
context.elements.lowTempDisplay.innerText = parseFloat(targetTempLow).toFixed(1);
}
}
}

export function changeTargetTempHigh(context) {
const targetTempHigh = getAttribute(context, "target_temp_high");
if (targetTempHigh !== context.previousTargetTempHigh) {
context.previousTargetTempHigh = targetTempHigh;
if (context.elements.highTempDisplay) {
context.elements.highTempDisplay.innerText = parseFloat(targetTempHigh).toFixed(1);
}
}
}

export function changeStyle(context) {
initializesubButtonIcon(context);
setLayout(context);

const stateObj = context._hass.states[context.config.entity];
const state = getState(context);
const isOn = state !== "off" && state !== "unknown";

if (context.previousState !== state) {
context.previousState = state;
const element = context.elements.colorBackground;
element.style.backgroundColor = `var(--bubble-climate-background-color, ${getClimateColor(state)})`;
element.style.backgroundColor = `var(--bubble-climate-background-color, ${getClimateColor(stateObj)})`;
}

const cardLayout = context.config.card_layout;
Expand All @@ -86,4 +123,4 @@ export function changeStyle(context) {
if (context.elements.customStyle) {
context.elements.customStyle.innerText = customStyle;
}
}
}
Loading

0 comments on commit a8e6bde

Please sign in to comment.