diff --git a/src/bubble-card.js b/src/bubble-card.js
index ba2011b4..3a69a293 100644
--- a/src/bubble-card.js
+++ b/src/bubble-card.js
@@ -1,6 +1,46 @@
-const version = 'v0.0.1-beta.8';
+var version = 'v0.0.1-beta.9';
class BubbleCard extends HTMLElement {
+ constructor() {
+ super();
+ // 'urlChanged' custom event
+ const pushState = history.pushState;
+ history.pushState = function () {
+ pushState.apply(history, arguments);
+ window.dispatchEvent(new Event('pushstate'));
+ };
+
+ const replaceState = history.replaceState;
+ history.replaceState = function () {
+ replaceState.apply(history, arguments);
+ window.dispatchEvent(new Event('replacestate'));
+ };
+
+ ['popstate', 'pushstate', 'replacestate', 'mousedown', 'touchstart'].forEach((eventType) => {
+ window.addEventListener(eventType, urlChanged);
+ });
+
+ const event = new Event('urlChanged');
+
+ function urlChanged() {
+ const newUrl = window.location.href;
+ if (newUrl !== this.currentUrl) {
+ window.dispatchEvent(event);
+ this.currentUrl = newUrl;
+ }
+ }
+
+ // Check url when pop-ups are initialized
+ const popUpInitialized = () => {
+ window.dispatchEvent(event);
+ setTimeout(() => {
+ window.removeEventListener('popUpInitialized', popUpInitialized);
+ }, 1000);
+ };
+
+ window.addEventListener('popUpInitialized', popUpInitialized);
+ }
+
set hass(hass) {
// Initialize the content if it's not there yet.
if (!this.content) {
@@ -17,22 +57,41 @@ class BubbleCard extends HTMLElement {
this.content = this.shadowRoot.querySelector("div");
}
- editorMode(this);
-
- function editorMode(t) {
- if (window.location.search !== "?edit=1") {
- t.editor = false;
- } else {
- t.editor = true;
- }
+ if (window.location.search !== "?edit=1") {
+ this.editor = false;
+ } else {
+ this.editor = true;
}
- function toggleEntity(entity) {
+ function toggleEntity(entityId) {
hass.callService('homeassistant', 'toggle', {
- entity_id: entity
+ entity_id: entityId
});
}
+ const addStyles = function(context, styles, customStyles, state, entityId, path = '', element = context.content) {
+ const customStylesEval = customStyles ? eval('`' + customStyles + '`') : '';
+ let styleAddedKey = styles + 'Added'; // Add 'Added' at the end of the styles value
+
+ // Check if the style has changed
+ if (!context[styleAddedKey] || context.previousStyle !== customStylesEval) {
+ if (!context[styleAddedKey]) {
+ // Check if the style element already exists
+ context.styleElement = context.content.querySelector('style');
+ if (!context.styleElement) {
+ // If not, create a new style element
+ context.styleElement = document.createElement('style');
+ const parentElement = path ? context.content.querySelector(path) : element;
+ parentElement.appendChild(context.styleElement);
+ }
+ context[styleAddedKey] = true;
+ }
+ // Update the content of the existing style element
+ context.styleElement.innerHTML = customStylesEval + styles;
+ context.previousStyle = customStylesEval; // Store the current style
+ }
+ }
+
const forwardHaptic = hapticType => {
fireEvent(window, "haptic", hapticType)
}
@@ -172,509 +231,572 @@ class BubbleCard extends HTMLElement {
}
const customStyles = !this.config.styles ? '' : this.config.styles;
-
- // Initialize pop-up card
- if (this.config.card_type === 'pop-up' && !this.getRootNode().host) {
- // Hide vertical stack content before initialization
- if (this.editor !== true) {
- this.card.style.marginTop = '2000px';
- }
- } else if (this.config.card_type === 'pop-up') {
- if (!this.popUp) {
- this.card.style.marginTop = '0';
- this.popUp = this.getRootNode().querySelector('#root');
- }
-
- const popUpHash = this.config.hash;
- const popUp = this.popUp;
- const entityId = this.config.entity || '';
- const icon = this.config.icon || '';
- const name = this.config.name || '';
- const stateUnit = this.config.state_unit || '';
- const state = this.config.state ? hass.states[this.config.state].state + stateUnit : '';
- const marginTopMobile = this.config.margin_top_mobile
- ? (this.config.margin_top_mobile !== '0' ? this.config.margin_top_mobile : '0px')
- : '0px';
- const marginTopDesktop = this.config.margin_top_desktop
- ? (this.config.margin_top_desktop !== '0' ? this.config.margin_top_desktop : '0px')
- : '0px';
- const widthDesktop = this.config.width_desktop || '600px';
- const isSidebarHidden = this.config.is_sidebar_hidden || 'false';
- const widthDesktopDivided = widthDesktop.match(/(\d+)(\D+)/);
- const displayPowerButton = this.config.entity ? 'flex' : 'none';
-
- if (!this.headerAdded) {
- const headerContainer = document.createElement("div");
- headerContainer.setAttribute("id", "header-container");
-
- const div = document.createElement("div");
- headerContainer.appendChild(div);
-
- const haIcon1 = document.createElement("ha-icon");
- haIcon1.setAttribute("class", "header-icon");
- haIcon1.setAttribute("icon", icon);
- div.appendChild(haIcon1);
- addActions(this, haIcon1);
-
- const h2 = document.createElement("h2");
- h2.textContent = name;
- div.appendChild(h2);
-
- const p = document.createElement("p");
- p.textContent = state;
- div.appendChild(p);
-
- const haIcon2 = document.createElement("ha-icon");
- haIcon2.setAttribute("class", "power-button");
- haIcon2.setAttribute("icon", "mdi:power");
- haIcon2.setAttribute("style", `display: ${displayPowerButton};`);
- div.appendChild(haIcon2);
-
- const button = document.createElement("button");
- button.setAttribute("class", "close-pop-up");
- button.setAttribute("onclick", "history.replaceState(null, null, location.href.split('#')[0]);");
- headerContainer.appendChild(button);
-
- const haIcon3 = document.createElement("ha-icon");
- haIcon3.setAttribute("icon", "mdi:close");
- button.appendChild(haIcon3);
-
- this.content.appendChild(headerContainer);
- this.header = div;
-
- this.headerAdded = true;
- }
-
- if (this.headerAdded) {
- const haIcon1 = this.content.querySelector("#header-container .header-icon");
- haIcon1.setAttribute("icon", icon);
- this.haIcon1 = haIcon1;
-
- const h2 = this.content.querySelector("#header-container h2");
- h2.textContent = name;
-
- const p = this.content.querySelector("#header-container p");
- p.textContent = state;
-
- const haIcon2 = this.content.querySelector("#header-container .power-button");
- haIcon2.setAttribute("style", `display: ${displayPowerButton};`);
- }
-
- if (!this.eventAdded) {
- ['click', 'touchend', 'popstate'].forEach((eventType) => {
- if (window['checkHashRef_' + popUpHash]) {
- window.removeEventListener(eventType, window['checkHashRef_' + popUpHash]);
- }
- window['checkHashRef_' + popUpHash] = checkHash;
- window.addEventListener(eventType, window['checkHashRef_' + popUpHash]);
- });
-
- this.content.querySelector('.power-button').addEventListener('click', () => {
- toggleEntity(entityId);
- });
-
- document.addEventListener('mousedown', function(e) {
- if (location.hash === popUpHash &&
- !e.composedPath().some(el => el.nodeName === 'HA-MORE-INFO-DIALOG') &&
- !e.composedPath().some(el => el.id === 'root' && !el.classList.contains('close-pop-up'))) {
- history.replaceState(null, null, location.href.split('#')[0]);
- }
- });
-
- window.addEventListener('keydown', function(event) {
- if (event.key === 'Escape') {
- history.replaceState(null, null, location.href.split('#')[0]);
+
+ let entityId = this.config.entity ? this.config.entity : '';
+ let icon = !this.config.icon && this.config.entity ? hass.states[entityId].attributes.icon || hass.states[entityId].attributes.entity_picture || '' : this.config.icon || '';
+ let name = this.config.name ? this.config.name : this.config.entity ? hass.states[entityId].attributes.friendly_name : '';
+ let widthDesktop = this.config.width_desktop || '540px';
+ let widthDesktopDivided = widthDesktop ? widthDesktop.match(/(\d+)(\D+)/) : '';
+ let isSidebarHidden = this.config.is_sidebar_hidden || false;
+ let state = this.config.state ? hass.states[this.config.state].state : '';
+ let formatedState;
+
+ switch (this.config.card_type) {
+ // Initialize pop-up card
+ case 'pop-up':
+ if (!this.getRootNode().host) {
+ // Hide vertical stack content before initialization
+ if (this.editor !== true) {
+ this.card.style.marginTop = '2000px';
}
- });
-
- // Slide down to close pop-up
-
- let startTouchY;
- let lastTouchY;
-
- popUp.addEventListener('touchstart', function(event) {
- // Record the Y position of the finger at the start of the touch
- startTouchY = event.touches[0].clientY;
- lastTouchY = startTouchY;
- });
-
- popUp.addEventListener('touchmove', function(event) {
- // Calculate the distance the finger has traveled
- let touchMoveDistance = event.touches[0].clientY - startTouchY;
-
- // If the distance is positive (i.e., the finger is moving downward) and exceeds a certain threshold, close the pop-up
- if (touchMoveDistance > 300 && event.touches[0].clientY > lastTouchY) {
- history.replaceState(null, null, location.href.split('#')[0]);
+ } else {
+ if (!this.popUp) {
+ this.card.style.marginTop = '0';
+ this.popUp = this.getRootNode().querySelector('#root');
+ const event = new Event('popUpInitialized');
+ window.dispatchEvent(event);
}
-
- // Update the Y position of the last touch
- lastTouchY = event.touches[0].clientY;
- });
- this.eventAdded = true;
- }
-
- if (entityId !== '') {
- const rgbColor = hass.states[entityId].attributes.rgb_color;
- const rgbColorOpacity = rgbColor
- ? `rgba(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]}, 0.5)`
- : hass.states[entityId].state !== ('off' || 'closed' || 'paused' || 'false')
- ? `var(--accent-color)`
- : `var(--background-color,var(--secondary-background-color))`;
- this.header.style.backgroundColor = rgbColorOpacity;
- }
-
- function checkHash(event) {
- return new Promise((resolve, reject) => {
- let attempts = 0;
- const intervalId = setInterval(() => {
- attempts += 1;
-
- // Open on hash change
- let hash = location.hash.split('?')[0];
-
- if (hash === popUpHash) {
- clearInterval(intervalId);
- resolve(openPopUp());
- // Close on back button from browser
- } else if (popUp.classList.contains('open-pop-up')) {
- clearInterval(intervalId);
- resolve(closePopUp());
+ const popUpHash = this.config.hash;
+ const popUp = this.popUp;
+ const text = this.config.text || '';
+ const triggerEntity = this.config.trigger_entity ? this.config.trigger_entity : '';
+ const triggerState = this.config.trigger_state ? this.config.trigger_state : '';
+ const triggerClose = this.config.trigger_close ? this.config.trigger_close : false;
+ formatedState = this.config.state ? hass.formatEntityState(hass.states[this.config.state]) + ' ' + text : text;
+ const marginTopMobile = this.config.margin_top_mobile
+ ? (this.config.margin_top_mobile !== '0' ? this.config.margin_top_mobile : '0px')
+ : '0px';
+ const marginTopDesktop = this.config.margin_top_desktop
+ ? (this.config.margin_top_desktop !== '0' ? this.config.margin_top_desktop : '0px')
+ : '0px';
+ const displayPowerButton = this.config.entity ? 'flex' : 'none';
+
+ if (!this.headerAdded) {
+ const headerContainer = document.createElement("div");
+ headerContainer.setAttribute("id", "header-container");
+
+ const div = document.createElement("div");
+ headerContainer.appendChild(div);
+
+ const iconContainer = document.createElement("div");
+ iconContainer.setAttribute("class", "header-icon");
+ div.appendChild(iconContainer);
+
+ if (hass && hass.states && hass.states[entityId] && hass.states[entityId].attributes.entity_picture && !this.config.icon) {
+ const img = document.createElement("img");
+ img.setAttribute("src", hass.states[entityId].attributes.entity_picture);
+ img.setAttribute("class", "entity-picture");
+ img.setAttribute("alt", "Icon");
+ iconContainer.appendChild(img);
+ } else {
+ const haIcon = document.createElement("ha-icon");
+ haIcon.setAttribute("icon", icon);
+ haIcon.setAttribute("class", "icon");
+ iconContainer.appendChild(haIcon);
}
-
- // Stop checking after 0.5 seconds
- if (attempts >= 10) {
- clearInterval(intervalId);
+ addActions(this, iconContainer);
+
+ const h2 = document.createElement("h2");
+ h2.textContent = name;
+ div.appendChild(h2);
+
+ const p = document.createElement("p");
+ p.textContent = formatedState;
+ div.appendChild(p);
+
+ const haIcon2 = document.createElement("ha-icon");
+ haIcon2.setAttribute("class", "power-button");
+ haIcon2.setAttribute("icon", "mdi:power");
+ haIcon2.setAttribute("style", `display: ${displayPowerButton};`);
+ div.appendChild(haIcon2);
+
+ const button = document.createElement("button");
+ button.setAttribute("class", "close-pop-up");
+ button.onclick = function() { history.replaceState(null, null, location.href.split('#')[0]); localStorage.setItem('isManuallyClosed_' + popUpHash, true); };
+ headerContainer.appendChild(button);
+
+ const haIcon3 = document.createElement("ha-icon");
+ haIcon3.setAttribute("icon", "mdi:close");
+ button.appendChild(haIcon3);
+
+ this.content.appendChild(headerContainer);
+ this.header = div;
+
+ this.headerAdded = true;
+ } else if (this.headerAdded) {
+ const iconContainer = this.content.querySelector("#header-container .header-icon");
+ iconContainer.innerHTML = ''; // Clear the container
+
+ if (hass && hass.states && hass.states[entityId] && hass.states[entityId].attributes.entity_picture && !this.config.icon) {
+ const img = document.createElement("img");
+ img.setAttribute("src", hass.states[entityId].attributes.entity_picture);
+ img.setAttribute("class", "entity-picture");
+ img.setAttribute("alt", "Icon");
+ iconContainer.appendChild(img);
+ } else {
+ const haIcon = document.createElement("ha-icon");
+ haIcon.setAttribute("icon", icon);
+ haIcon.setAttribute("class", "icon");
+ iconContainer.appendChild(haIcon);
}
- }, 50); // Check every 50ms for a total of 0.5 seconds
- });
- };
-
- checkHash();
-
- function openPopUp() {
- return new Promise((resolve, reject) => {
- popUp.classList.remove('close-pop-up');
- popUp.classList.add('open-pop-up');
-
- resolve();
- });
- }
-
- function closePopUp() {
- return new Promise((resolve, reject) => {
- popUp.classList.remove('open-pop-up');
- popUp.classList.add('close-pop-up');
-
- resolve();
- });
- }
-
- const headerStyles = `
- ${customStyles}
- #header-container {
- display: inline-flex;
- width: 100%;
- margin: 0;
- padding: 0;
- }
- #header-container > div {
- display: inline-flex;
- align-items: center;
- position: relative;
- padding: 6px;
- z-index: 2;
- flex-grow: 1;
- background-color: var(--background-color,var(--secondary-background-color));
- transition: background 1s;
- border-radius: 25px;
- margin-right: 14px;
- }
- .header-icon {
- display: inline-flex;
- width: 22px;
- height: 22px;
- padding: 8px;
- background-color: var(--card-background-color,var(--ha-card-background));
- border-radius: 100%;
- margin: 0 10px 0 0;
- cursor: ${!this.config.entity && !this.config.double_tap_action && !this.config.tap_action && !this.config.hold_action ? 'default' : 'pointer'};
- }
- #header-container h2 {
- display: inline-flex;
- margin: 0 18px 0 0;
- /*line-height: 0px;*/
- z-index: 100;
- font-size: 20px;
- }
- #header-container p {
- display: inline-flex;
- line-height: 0px;
- font-size: 16px;
- }
- .power-button {
- cursor: pointer;
- flex-grow: inherit;
- width: 24px;
- height: 24px;
- border-radius: 12px;
- margin: 0 10px;
- background: none !important;
- justify-content: flex-end;
- background-color: var(--background-color,var(--secondary-background-color));
- }
- .close-pop-up {
- height: 50px;
- width: 50px;
- border: none;
- border-radius: 50%;
- z-index: 2;
- background: var(--background-color,var(--secondary-background-color));
- color: var(--primary-text-color);
- flex-shrink: 0;
- cursor: pointer;
- }
- `;
-
- if (!this.styleAdded && this.editor !== true) {
- const styleElement = document.createElement('style');
- const headerStyleElement = document.createElement('style');
-
- const styles = `
- ${customStyles}
- ha-card {
- margin-top: 0 !important;
- background: none !important;
- border: none !important;
- }
- .card-content {
- width: 100% !important;
- padding: 0 !important;
- }
- #root {
- position: fixed !important;
- margin: 0 -7px;
- width: calc(100% - 38px);
- background-color: var(--ha-card-background,var(--card-background-color));
- border-radius: 42px;
- top: calc(100% + ${marginTopMobile} + 54px); /*136px*/
- grid-gap: 12px !important;
- gap: 12px !important;
- grid-auto-rows: min-content;
- padding: 18px 18px 220px 18px !important;
- height: calc(100% - 240px) !important;
- -ms-overflow-style: none; /* for Internet Explorer, Edge */
- scrollbar-width: none; /* for Firefox */
- overflow-y: auto;
- overflow-x: hidden;
- z-index: 1 !important; /* Higher value hide the more-info panel */
- /* For older Safari but not working with Firefox */
- /* display: grid !important; */
- }
- #root > bubble-card:first-child::after {
- content: '';
- display: block;
- position: sticky;
- top: 0;
- left: -50px;
- margin: -70px 0 -35px 0;
- width: 200%;
- height: 100px;
- background: linear-gradient(0deg, rgba(79, 69, 87, 0) 0%, var(--ha-card-background,var(--card-background-color)) 80%);
- z-index: 0;
- }
- #root::-webkit-scrollbar {
- display: none; /* for Chrome, Safari, and Opera */
+
+ const h2 = this.content.querySelector("#header-container h2");
+ h2.textContent = name;
+
+ const p = this.content.querySelector("#header-container p");
+ p.textContent = formatedState;
+
+ const haIcon2 = this.content.querySelector("#header-container .power-button");
+ haIcon2.setAttribute("style", `display: ${displayPowerButton};`);
}
- #root > bubble-card:first-child {
- position: sticky;
- top: 0;
- z-index: 5;
- background: none !important;
+
+ if (!this.eventAdded) {
+ window['checkHashRef_' + popUpHash] = checkHash;
+ window.addEventListener('urlChanged', window['checkHashRef_' + popUpHash]);
+
+ this.content.querySelector('.power-button').addEventListener('click', () => {
+ toggleEntity(entityId);
+ });
+
+ window.addEventListener('mousedown', function(e) {
+ if (location.hash === popUpHash &&
+ !e.composedPath().some(el => el.nodeName === 'HA-MORE-INFO-DIALOG') &&
+ !e.composedPath().some(el => el.id === 'root' && !el.classList.contains('close-pop-up'))) {
+ history.replaceState(null, null, location.href.split('#')[0]);
+ localStorage.setItem('isManuallyClosed_' + popUpHash, true)
+ }
+ });
+
+ window.addEventListener('keydown', function(event) {
+ if (event.key === 'Escape') {
+ history.replaceState(null, null, location.href.split('#')[0]);
+ localStorage.setItem('isManuallyClosed_' + popUpHash, true)
+ }
+ });
+
+ // Slide down to close pop-up
+
+ let startTouchY;
+ let lastTouchY;
+
+ popUp.addEventListener('touchstart', function(event) {
+ // Record the Y position of the finger at the start of the touch
+ startTouchY = event.touches[0].clientY;
+ lastTouchY = startTouchY;
+ });
+
+ popUp.addEventListener('touchmove', function(event) {
+ // Calculate the distance the finger has traveled
+ let touchMoveDistance = event.touches[0].clientY - startTouchY;
+
+ // If the distance is positive (i.e., the finger is moving downward) and exceeds a certain threshold, close the pop-up
+ if (touchMoveDistance > 300 && event.touches[0].clientY > lastTouchY) {
+ history.replaceState(null, null, location.href.split('#')[0]);
+ localStorage.setItem('isManuallyClosed_' + popUpHash, true)
+ }
+
+ // Update the Y position of the last touch
+ lastTouchY = event.touches[0].clientY;
+ });
+
+ this.eventAdded = true;
}
- #root.open-pop-up {
- transform: translateY(-100%);
- transition: transform .4s !important;
+
+ if (triggerEntity) {
+ let previousTriggerState = localStorage.getItem('previousTriggerState_' + popUpHash);
+ let isManuallyClosed = localStorage.getItem('isManuallyClosed_' + popUpHash) === 'true';
+ let isTriggered = localStorage.getItem('isTriggered_' + popUpHash) === 'true';
+
+ if (hass.states[triggerEntity].state === triggerState && previousTriggerState === null || !isTriggered) {
+ navigate('', popUpHash);
+ isTriggered = true;
+ localStorage.setItem('isTriggered_' + popUpHash, isTriggered);
+ }
+
+ if (hass.states[triggerEntity].state !== previousTriggerState) {
+ isManuallyClosed = false;
+ localStorage.setItem('previousTriggerState_' + popUpHash, hass.states[triggerEntity].state);
+ localStorage.setItem('isManuallyClosed_' + popUpHash, isManuallyClosed);
+ }
+
+ if (hass.states[triggerEntity].state === triggerState && !isManuallyClosed) {
+ navigate('', popUpHash);
+ isTriggered = true;
+ localStorage.setItem('isTriggered_' + popUpHash, isTriggered);
+ } else if (triggerClose && popUp.classList.contains('open-pop-up') && isTriggered && !isManuallyClosed) {
+ history.replaceState(null, null, location.href.split('#')[0]);
+ isTriggered = false;
+ isManuallyClosed = true;
+ localStorage.setItem('isManuallyClosed_' + popUpHash, isManuallyClosed);
+ localStorage.setItem('isTriggered_' + popUpHash, isTriggered);
+ }
}
- #root.open-pop-up > * {
- /* Block child items to overflow and if they do clip them */
- /*max-width: calc(100vw - 38px);*/
- max-width: 100% !important;
- overflow-x: clip;
+
+ if (entityId !== '') {
+ const rgbColor = hass.states[entityId].attributes.rgb_color;
+ const rgbColorOpacity = rgbColor
+ ? `rgba(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]}, 0.5)`
+ : hass.states[entityId].state !== ('off' || 'closed' || 'paused' || 'false')
+ ? `var(--accent-color)`
+ : `var(--background-color,var(--secondary-background-color))`;
+ this.header.style.backgroundColor = rgbColorOpacity;
+ }
+
+ function checkHash() {
+ if (!this.editor) {
+ let hash = location.hash.split('?')[0];
+
+ // Open on hash change
+ if (hash === popUpHash) {
+ openPopUp();
+ // Close on back button from browser
+ } else if (popUp.classList.contains('open-pop-up')) {
+ closePopUp();
+ }
+ }
+ };
+
+ function openPopUp() {
+ popUp.classList.remove('close-pop-up');
+ popUp.classList.add('open-pop-up');
}
- #root.close-pop-up {
- transform: translateY(0%);
- transition: transform .4s !important;
- /* animation: hide 1s forwards; */
+
+ function closePopUp() {
+ popUp.classList.remove('open-pop-up');
+ popUp.classList.add('close-pop-up');
}
- @media only screen and (min-width: 768px) {
- #root {
- top: calc(100% + ${marginTopDesktop} + 54px);
- width: calc(${widthDesktop} - 36px) !important;
- left: calc(50% - ${widthDesktopDivided[1] / 2}${widthDesktopDivided[2]});
- margin: 0 !important;
+
+ const popUpStyles = `
+ ha-card {
+ margin-top: 0 !important;
+ background: none !important;
+ border: none !important;
+ }
+ .card-content {
+ width: 100% !important;
+ padding: 0 !important;
}
- }
- @media only screen and (min-width: 870px) {
#root {
- top: calc(100% + ${marginTopDesktop} + 54px);
- width: calc(${widthDesktop} - ${isSidebarHidden === true ? '0px' : '92px'}) !important;
- left: calc(50% - ${widthDesktopDivided[1] / 2}${widthDesktopDivided[2]} + ${isSidebarHidden === true ? '0px' : '56px'});
- margin: 0 !important;
+ position: fixed !important;
+ margin: 0 -7px;
+ width: 100%;
+ background-color: var(--ha-card-background,var(--card-background-color));
+ border-radius: 42px;
+ box-sizing: border-box;
+ top: calc(100% + ${marginTopMobile} + var(--header-height));
+ grid-gap: 12px !important;
+ gap: 12px !important;
+ grid-auto-rows: min-content;
+ padding: 18px 18px 220px 18px !important;
+ height: 100% !important;
+ -ms-overflow-style: none; /* for Internet Explorer, Edge */
+ scrollbar-width: none; /* for Firefox */
+ overflow-y: auto;
+ overflow-x: hidden;
+ z-index: 1 !important; /* Higher value hide the more-info panel */
+ /* For older Safari but not working with Firefox */
+ /* display: grid !important; */
}
- }
- `;
-
- styleElement.innerHTML = styles;
- headerStyleElement.innerHTML = headerStyles;
- popUp.appendChild(styleElement);
- this.content.appendChild(headerStyleElement);
- this.styleAdded = true;
- } else if (this.editor === true) {
- const styleElement = this.getRootNode().querySelector('#root').querySelector("style");
- if (styleElement) {
- popUp.removeChild(styleElement);
- }
- popUp.style.backgroundColor = 'var(--ha-card-background,var(--card-background-color))';
- popUp.style.padding = '16px';
- popUp.style.borderRadius = '42px';
- popUp.style.gridGap = '12px';
- popUp.style.gap = '12px';
- const headerStyleElement = document.createElement('style');
- headerStyleElement.innerHTML = headerStyles;
- this.content.appendChild(headerStyleElement);
- this.styleAdded = false;
- }
- }
-
- // Initialize horizontal buttons stack
- if (this.config.card_type === 'horizontal-buttons-stack') {
- const createButton = (button, link, icon) => {
- const buttonElement = document.createElement("button");
- buttonElement.onclick = function() { navigate('', link); };
- buttonElement.setAttribute("class", "button");
- buttonElement.innerHTML = `
- ${icon !== '' ? `
${button}
` : ''} - `; - return buttonElement; - }; - - const updateButtonStyle = (buttonElement, lightEntity) => { - if (hass.states[lightEntity].attributes.rgb_color) { - const rgbColor = hass.states[lightEntity].attributes.rgb_color; - const rgbColorOpacity = `rgba(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]}, 0.5)`; - buttonElement.style.backgroundColor = rgbColorOpacity; - buttonElement.style.border = '1px solid rgba(0,0,0,0)'; - } else if (!hass.states[lightEntity].attributes.rgb_color && hass.states[lightEntity].state == 'on') { - buttonElement.style.backgroundColor = 'rgba(255,255,255,0.5)'; - buttonElement.style.border = '1px solid rgba(0,0,0,0)'; - } else { - buttonElement.style.backgroundColor = 'rgba(0,0,0,0)'; - buttonElement.style.border = '1px solid var(--primary-text-color)'; + #root > bubble-card:first-child::after { + content: ''; + display: block; + position: sticky; + top: 0; + left: -50px; + margin: -70px 0 -35px 0; + width: 200%; + height: 100px; + background: linear-gradient(0deg, rgba(79, 69, 87, 0) 0%, var(--ha-card-background,var(--card-background-color)) 80%); + z-index: 0; + } + #root::-webkit-scrollbar { + display: none; /* for Chrome, Safari, and Opera */ + } + #root > bubble-card:first-child { + position: sticky; + top: 0; + z-index: 1; + background: none !important; + } + #root.open-pop-up { + transform: translateY(-100%); + transition: transform .4s !important; + } + #root.open-pop-up > * { + /* Block child items to overflow and if they do clip them */ + /*max-width: calc(100vw - 38px);*/ + max-width: 100% !important; + overflow-x: clip; + } + #root.close-pop-up { + transform: translateY(0%); + transition: transform .4s !important; + /* animation: hide 1s forwards; */ + } + @media only screen and (min-width: 768px) { + #root { + top: calc(100% + ${marginTopDesktop} + var(--header-height)); + left: calc(50% - ${widthDesktopDivided[1] / 2}${widthDesktopDivided[2]}); + margin: 0 !important; + } + } + @media only screen and (min-width: 870px) { + #root { + top: calc(100% + ${marginTopDesktop} + var(--header-height)); + width: calc(${widthDesktop}${widthDesktopDivided[2] === '%' ? ' - var(--mdc-drawer-width)' : ''}) !important; + left: calc(50% - ${widthDesktopDivided[1] / 2}${widthDesktopDivided[2]} + ${isSidebarHidden === true ? '0px' : `var(--mdc-drawer-width) ${widthDesktopDivided[2] === '%' ? '' : '/ 2'}`}); + margin: 0 !important; + } + } + #root.editor { + position: inherit !important; + width: 100% !important; + padding: 18px !important; + } + `; + + const headerStyles = ` + #header-container { + display: inline-flex; + ${!icon && !name && !entityId && !state && !text ? 'flex-direction: row-reverse;' : ''} + width: 100%; + margin: 0; + padding: 0; + } + #header-container > div { + display: ${!icon && !name && !entityId && !state && !text ? 'none' : 'inline-flex'}; + align-items: center; + position: relative; + padding: 6px; + z-index: 1; + flex-grow: 1; + background-color: var(--background-color,var(--secondary-background-color)); + transition: background 1s; + border-radius: 25px; + margin-right: 14px; + } + .header-icon { + display: inline-flex; + width: 22px; + height: 22px; + padding: 8px; + background-color: var(--card-background-color,var(--ha-card-background)); + border-radius: 100%; + margin: 0 10px 0 0; + cursor: ${!this.config.entity && !this.config.double_tap_action && !this.config.tap_action && !this.config.hold_action ? 'default' : 'pointer'}; + flex-wrap: wrap; + align-content: center; + justify-content: center; + overflow: hidden; + } + .entity-picture { + height: calc(100% + 16px); + width: calc(100% + 16px); + } + #header-container h2 { + display: inline-flex; + margin: 0 18px 0 0; + /*line-height: 0px;*/ + z-index: 1; + font-size: 20px; + } + #header-container p { + display: inline-flex; + line-height: 0px; + font-size: 16px; + } + .power-button { + cursor: pointer; + flex-grow: inherit; + width: 24px; + height: 24px; + border-radius: 12px; + margin: 0 10px; + background: none !important; + justify-content: flex-end; + background-color: var(--background-color,var(--secondary-background-color)); + } + .close-pop-up { + height: 50px; + width: 50px; + border: none; + border-radius: 50%; + z-index: 1; + background: var(--background-color,var(--secondary-background-color)); + color: var(--primary-text-color); + flex-shrink: 0; + cursor: pointer; + } + `; + + addStyles(this, popUpStyles, customStyles, state, entityId, '', popUp); + addStyles(this, headerStyles, customStyles, state, entityId); + + if (this.editor === true) { + popUp.classList.add('editor'); + popUp.classList.remove('open-pop-up'); + popUp.classList.remove('close-pop-up'); + } else { + popUp.classList.remove('editor'); + } } - }; - - if (!this.buttonsAdded) { - const buttonsContainer = document.createElement("div"); - buttonsContainer.setAttribute("class", "horizontal-buttons-stack-container"); - this.content.appendChild(buttonsContainer); - } - - const updateButtonsOrder = () => { - let buttonsList = []; - let i = 1; - while (this.config[i + '_link']) { - const prefix = i + '_'; - const button = this.config[prefix + 'name'] || ''; - const pirSensor = this.config[prefix + 'pir_sensor']; - const icon = this.config[prefix + 'icon'] || ''; - const link = this.config[prefix + 'link']; - const lightEntity = this.config[prefix + 'entity']; - buttonsList.push({ - button, - pirSensor, - icon, - link, - lightEntity - }); - i++; + break; + + // Initialize horizontal buttons stack + case 'horizontal-buttons-stack' : + const createButton = (button, link, icon) => { + const buttonElement = document.createElement("button"); + buttonElement.onclick = function() { navigate('', link); }; + buttonElement.setAttribute("class", "button"); + buttonElement.innerHTML = ` + ${icon !== '' ? `${button}
` : ''} + `; + return buttonElement; + }; + + const updateButtonStyle = (buttonElement, lightEntity) => { + if (hass.states[lightEntity].attributes.rgb_color) { + const rgbColor = hass.states[lightEntity].attributes.rgb_color; + const rgbColorOpacity = `rgba(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]}, 0.5)`; + buttonElement.style.backgroundColor = rgbColorOpacity; + buttonElement.style.border = '1px solid rgba(0,0,0,0)'; + } else if (!hass.states[lightEntity].attributes.rgb_color && hass.states[lightEntity].state == 'on') { + buttonElement.style.backgroundColor = 'rgba(255,255,255,0.5)'; + buttonElement.style.border = '1px solid rgba(0,0,0,0)'; + } else { + buttonElement.style.backgroundColor = 'rgba(0,0,0,0)'; + buttonElement.style.border = '1px solid var(--primary-text-color)'; + } + }; + + if (!this.buttonsAdded) { + const buttonsContainer = document.createElement("div"); + buttonsContainer.setAttribute("class", "horizontal-buttons-stack-container"); + this.content.appendChild(buttonsContainer); + this.buttonsContainer = buttonsContainer; } - - if (this.config.auto_order) { - buttonsList.sort((a, b) => { - // Check if both PIR sensors are defined - if (a.pirSensor && b.pirSensor) { - // Check if the PIR sensor state is "on" for both buttons - if (hass.states[a.pirSensor].state === "on" && hass.states[b.pirSensor].state === "on") { + + const updateButtonsOrder = () => { + let buttonsList = []; + let i = 1; + while (this.config[i + '_link']) { + const prefix = i + '_'; + const button = this.config[prefix + 'name'] || ''; + const pirSensor = this.config[prefix + 'pir_sensor']; + icon = this.config[prefix + 'icon'] || ''; + const link = this.config[prefix + 'link']; + const lightEntity = this.config[prefix + 'entity']; + buttonsList.push({ + button, + pirSensor, + icon, + link, + lightEntity + }); + i++; + } + + if (this.config.auto_order) { + buttonsList.sort((a, b) => { + // Check if both PIR sensors are defined + if (a.pirSensor && b.pirSensor) { + // Check if the PIR sensor state is "on" for both buttons + if (hass.states[a.pirSensor].state === "on" && hass.states[b.pirSensor].state === "on") { + let aTime = hass.states[a.pirSensor].last_updated; + let bTime = hass.states[b.pirSensor].last_updated; + return aTime < bTime ? 1 : -1; + } + // If only a.pirSensor is "on", place a before b + else if (hass.states[a.pirSensor].state === "on") { + return -1; + } + // If only b.pirSensor is "on", place b before a + else if (hass.states[b.pirSensor].state === "on") { + return 1; + } + // If neither PIR sensor is "on", arrangement based only on the state of last updated even if off let aTime = hass.states[a.pirSensor].last_updated; let bTime = hass.states[b.pirSensor].last_updated; return aTime < bTime ? 1 : -1; } - // If only a.pirSensor is "on", place a before b - else if (hass.states[a.pirSensor].state === "on") { + // If a.pirSensor is not defined, place a after b + else if (!a.pirSensor) { + return 1; + } + // If b.pirSensor is not defined, place b after a + else if (!b.pirSensor) { return -1; } - // If only b.pirSensor is "on", place b before a - else if (hass.states[b.pirSensor].state === "on") { - return 1; + }); + } + + if (!this.buttonsAdded || this.editor) { + this.card.classList.add('horizontal-buttons-stack'); + + // Fix for editor mode + if (this.editor && this.buttonsContainer) { + while (this.buttonsContainer.firstChild) { + this.buttonsContainer.removeChild(this.buttonsContainer.firstChild); } - // If neither PIR sensor is "on", arrangement based only on the state of last updated even if off - let aTime = hass.states[a.pirSensor].last_updated; - let bTime = hass.states[b.pirSensor].last_updated; - return aTime < bTime ? 1 : -1; + localStorage.setItem('editorMode', true); + } else { + localStorage.setItem('editorMode', false); } - // If a.pirSensor is not defined, place a after b - else if (!a.pirSensor) { - return 1; + // End of fix + + const buttons = {}; + buttonsList.forEach(button => { + const buttonElement = createButton(button.button, button.link, button.icon); + // Store the button element using its link as key + buttons[button.link] = buttonElement; + this.buttonsContainer.appendChild(buttonElement); + }); + this.buttonsAdded = true; + this.buttons = buttons; + } + + if (this.editor) { + localStorage.setItem('justExitedEditor', false); + } else if (localStorage.getItem('justExitedEditor') === 'false') { + localStorage.setItem('justExitedEditor', true); + } + + let currentPosition = 0; + let margin = 12; + const justExitedEditor = localStorage.getItem('editorMode') === 'true' && !this.editor; + + buttonsList.forEach((button, index) => { + let buttonElement = this.buttons[button.link]; + if (buttonElement) { + let buttonWidth = localStorage.getItem(`buttonWidth-${button.link}`); + let buttonContent = localStorage.getItem(`buttonContent-${button.link}`); + if (!buttonWidth || buttonWidth === '0' || buttonContent !== buttonElement.innerHTML || this.editor || justExitedEditor) { + buttonWidth = buttonElement.offsetWidth; + localStorage.setItem(`buttonWidth-${button.link}`, buttonWidth); + localStorage.setItem(`buttonContent-${button.link}`, buttonElement.innerHTML); + if (this.editor) { + margin = 36; // Recalculate margin for editor mode + } else if (justExitedEditor) { + margin = 12; // Recalculate margin for regular mode + } + } + buttonElement.style.transform = `translateX(${currentPosition}px)`; + currentPosition += parseInt(buttonWidth) + margin; } - // If b.pirSensor is not defined, place b after a - else if (!b.pirSensor) { - return -1; + if (button.lightEntity) { + updateButtonStyle(buttonElement, button.lightEntity); } }); } - - if (!this.buttonsAdded) { - const buttonsContainer = this.content.querySelector(".horizontal-buttons-stack-container"); - const buttons = {}; - buttonsList.forEach(button => { - const buttonElement = createButton(button.button, button.link, button.icon); - // Store the button element using its link as key - buttons[button.link] = buttonElement; - buttonsContainer.appendChild(buttonElement); - }); - this.buttonsAdded = true; - this.buttons = buttons; - } - - let currentPosition = 0; - const margin = 12; - buttonsList.forEach((button, index) => { - let buttonElement = this.buttons[button.link]; - if (buttonElement) { - let buttonWidth = localStorage.getItem(`buttonWidth-${button.link}`); - let buttonContent = localStorage.getItem(`buttonContent-${button.link}`); - if (!buttonWidth || buttonWidth === '0' || buttonContent !== buttonElement.innerHTML || this.editor === true) { - buttonWidth = buttonElement.offsetWidth; - localStorage.setItem(`buttonWidth-${button.link}`, buttonWidth); - localStorage.setItem(`buttonContent-${button.link}`, buttonElement.innerHTML); - } - buttonElement.style.transform = `translateX(${currentPosition}px)`; - currentPosition += parseInt(buttonWidth) + margin; - } - if (button.lightEntity) { - updateButtonStyle(buttonElement, button.lightEntity); + + updateButtonsOrder(); + + const horizontalButtonsStackStyles = ` + ha-card { + border-radius: 0; } - }); - } - - updateButtonsOrder(); - - if (!this.styleAdded) { - const styleElement = document.createElement('style'); - const styles = ` - ${customStyles} .horizontal-buttons-stack { width: 100%; margin-top: 0 !important; @@ -682,6 +804,7 @@ class BubbleCard extends HTMLElement { position: fixed; height: 51px; bottom: 16px; + left: 7px; /* transform: translateY(200px); */ /* animation: from-bottom 1.3s forwards; */ z-index: 1 !important; /* Higher value hide the more-info panel */ @@ -703,14 +826,14 @@ class BubbleCard extends HTMLElement { .button { display: flex; position: absolute; - box-sizing: border-box; + /* box-sizing: border-box; */ border: 1px solid var(--primary-text-color); align-items: center; height: 50px; white-space: nowrap; width: auto; border-radius: 25px; - z-index: 2; + z-index: 1; padding: 16px; background: none; transition: background-color 1s, border 1s, transform 1s; @@ -720,15 +843,16 @@ class BubbleCard extends HTMLElement { height: 24px; } .card-content { - width: 100%; + width: calc(100% + 18px); box-sizing: border-box; margin: 0 -36px !important; padding: 0 36px !important; overflow: scroll !important; -ms-overflow-style: none; scrollbar-width: none; - mask-image: linear-gradient(90deg, transparent 2%, rgba(0, 0, 0, 1) 6%, rgba(0, 0, 0, 1) 96%, transparent 100%); - -webkit-mask-image: linear-gradient(90deg, transparent 2%, rgba(0, 0, 0, 1) 6%, rgba(0, 0, 0, 1) 96%, transparent 100%); + -webkit-mask-image: linear-gradient(90deg, transparent 0%, rgba(0, 0, 0, 1) calc(0% + 28px), rgba(0, 0, 0, 1) calc(100% - 28px), transparent 100%); + /* mask-image: linear-gradient(90deg, transparent 2%, rgba(0, 0, 0, 1) 6%, rgba(0, 0, 0, 1) 96%, transparent 100%); */ + /* -webkit-mask-image: linear-gradient(90deg, transparent 2%, rgba(0, 0, 0, 1) 6%, rgba(0, 0, 0, 1) 96%, transparent 100%); */ } .horizontal-buttons-stack::before { content: ''; @@ -746,289 +870,305 @@ class BubbleCard extends HTMLElement { @media only screen and (min-width: 768px) { .card-content { position: fixed; - width: 538px !important; - left: calc(50% - 246px); + width: ${widthDesktop} !important; + left: calc(50% - ${widthDesktopDivided[1] / 2}${widthDesktopDivided[2]}); + margin-left: -13px !important; padding: 0 26px !important; } } @media only screen and (min-width: 870px) { .card-content { position: fixed; - width: 538px !important; - left: calc(50% - 218px); + width: calc(${widthDesktop}${widthDesktopDivided[2] === '%' ? ' - var(--mdc-drawer-width)' : ''}) !important; + left: calc(50% - ${widthDesktopDivided[1] / 2}${widthDesktopDivided[2]} + ${isSidebarHidden === true ? '0px' : `var(--mdc-drawer-width) ${widthDesktopDivided[2] === '%' ? '' : '/ 2'}`}); + margin-left: -13px !important; padding: 0 26px !important; } } + + .horizontal-buttons-stack.editor { + position: relative; + bottom: 0; + left: 0; + overflow: hidden; + } + + .horizontal-buttons-stack.editor::before { + top: -32px; + left: -100%; + background: none; + width: 100%; + height: 0; + } + + .horizontal-buttons-stack-container.editor > .button { + transition: background-color 0s, border 0s, transform 0s; + } + + .horizontal-buttons-stack-container.editor { + margin-left: 1px; + } + + .horizontal-buttons-stack.editor > .card-content { + position: relative; + width: calc(100% + 26px) !important; + left: -26px; + margin: 0 !important; + padding: 0; + } `; - - styleElement.innerHTML = styles; - this.card.classList.add('horizontal-buttons-stack'); - this.content.querySelector(".horizontal-buttons-stack-container").appendChild(styleElement); - this.styleAdded = true; - } - - if (this.editor === true) { - if (!this.editorStyleAdded) { - const styleElement = document.createElement('style'); - const styles = ` - ${customStyles} - .horizontal-buttons-stack { - position: relative; - height: 51px; - bottom: 0px; - overflow: hidden; - } - .horizontal-buttons-stack::before { - top: -32px; - left: -100%; - background: none; - width: 100%; - height: 0; - } - .card-content { - position: relative; - mask-image: linear-gradient(90deg, transparent 2%, rgba(0, 0, 0, 1) 6%, rgba(0, 0, 0, 1) 96%, transparent 100%); - -webkit-mask-image: linear-gradient(90deg, transparent 2%, rgba(0, 0, 0, 1) 6%, rgba(0, 0, 0, 1) 96%, transparent 100%); - } - @media only screen and (min-width: 870px) { - .card-content { - left: calc(50% - 230px); - } + + addStyles(this, horizontalButtonsStackStyles, customStyles); + + if (this.editor) { + this.buttonsContainer.classList.add('editor'); + this.card.classList.add('editor'); + } else { + this.buttonsContainer.classList.remove('editor'); + this.card.classList.remove('editor'); + } + break; + + // Initialize button + case 'button' : + if (!this.buttonAdded) { + const buttonContainer = document.createElement("div"); + buttonContainer.setAttribute("class", "button-container"); + this.content.appendChild(buttonContainer); + } + + //const name = !this.config.name ? hass.states[entityId].attributes.friendly_name || '' : this.config.name; + const buttonType = this.config.button_type || 'switch'; + formatedState = hass.formatEntityState(hass.states[entityId]); + const showState = !this.config.show_state ? false : this.config.show_state; + let currentBrightness = !entityId ? '' : hass.states[entityId].attributes.brightness || 0; + let currentVolume = !entityId ? '' : hass.states[entityId].attributes.volume_level || 0; + let isDragging = false; + let brightness = currentBrightness; + let volume = currentVolume; + let startX = 0; + let startY = 0; + let startValue = 0; + let movingVertically = false; + let timeoutId = null; + + const iconContainer = document.createElement('div'); + iconContainer.setAttribute('class', 'icon-container'); + this.iconContainer = iconContainer; + + const nameContainer = document.createElement('div'); + nameContainer.setAttribute('class', 'name-container'); + + const switchButton = document.createElement('div'); + switchButton.setAttribute('class', 'switch-button'); + + const rangeSlider = document.createElement('div'); + rangeSlider.setAttribute('class', 'range-slider'); + + const rangeFill = document.createElement('div'); + + rangeFill.setAttribute('class', 'range-fill'); + if (entityId && entityId.startsWith("light.") && buttonType === 'slider') { + rangeFill.style.transform = `translateX(${(currentBrightness / 255) * 100}%)`; + } else if (entityId && entityId.startsWith("media_player.") && buttonType === 'slider') { + rangeFill.style.transform = `translateX(${currentVolume * 100}%)`; + } + + if (!this.buttonContainer || this.editor) { + // Fix for editor mode + if (this.editor && this.buttonContainer) { + while (this.buttonContainer.firstChild) { + this.buttonContainer.removeChild(this.buttonContainer.firstChild); } - `; - styleElement.innerHTML = styles; - this.card.appendChild(styleElement); - this.editorStyleAdded = true; + this.eventAdded = false; + } + // End of fix + + this.buttonContainer = this.content.querySelector(".button-container"); + + if (buttonType === 'slider' && (!this.buttonAdded || this.editor)) { + this.buttonContainer.appendChild(rangeSlider); + rangeSlider.appendChild(iconContainer); + rangeSlider.appendChild(nameContainer); + rangeSlider.appendChild(rangeFill); + this.rangeFill = this.content.querySelector(".range-fill"); + } else if (buttonType === 'switch' || buttonType === 'custom' || this.editor) { + this.buttonContainer.appendChild(switchButton); + switchButton.appendChild(iconContainer); + switchButton.appendChild(nameContainer); + this.switchButton = this.content.querySelector(".switch-button"); + } + + if ((hass.states[entityId].attributes.entity_picture || icon.startsWith("/api/image/")) && !this.config.icon) { + iconContainer.innerHTML = ``; + } else { + iconContainer.innerHTML = `${name}
+ ${!showState ? '' : `${formatedState}
`} + `; + + this.buttonAdded = true; } - } else if (this.card.querySelector("ha-card > style")) { - const styleElement = this.card.querySelector("ha-card > style"); - this.card.removeChild(styleElement); - } - } - - // Initialize button - if (this.config.card_type === 'button') { - if (!this.buttonAdded) { - const buttonContainer = document.createElement("div"); - buttonContainer.setAttribute("class", "button-container"); - this.content.appendChild(buttonContainer); - } - - const entityId = this.config.entity; - const icon = !this.config.icon ? hass.states[entityId].attributes.icon || hass.states[entityId].attributes.entity_picture || '' : this.config.icon; - const name = !this.config.name ? hass.states[entityId].attributes.friendly_name || '' : this.config.name; - const buttonType = this.config.button_type || 'switch'; - const state = !entityId ? '' : hass.states[entityId].state; - let currentBrightness = !entityId ? '' : hass.states[entityId].attributes.brightness || 0; - let currentVolume = !entityId ? '' : hass.states[entityId].attributes.volume_level || 0; - let isDragging = false; - let brightness = currentBrightness; - let volume = currentVolume; - let startX = 0; - let startY = 0; - let startValue = 0; - let movingVertically = false; - let timeoutId = null; - - const iconContainer = document.createElement('div'); - iconContainer.setAttribute('class', 'icon-container'); - this.iconContainer = iconContainer; - - const nameContainer = document.createElement('div'); - nameContainer.setAttribute('class', 'nameContainer'); - - const switchButton = document.createElement('div'); - switchButton.setAttribute('class', 'switch-button'); - - const rangeSlider = document.createElement('div'); - rangeSlider.setAttribute('class', 'range-slider'); - - const rangeFill = document.createElement('div'); - - rangeFill.setAttribute('class', 'range-fill'); - if (entityId && entityId.startsWith("light.") && buttonType === 'slider') { - rangeFill.style.transform = `translateX(${(currentBrightness / 255) * 100}%)`; - } else if (entityId && entityId.startsWith("media_player.") && buttonType === 'slider') { - rangeFill.style.transform = `translateX(${currentVolume * 100}%)`; - } - - if (!this.buttonContainer) { - this.buttonContainer = this.content.querySelector(".button-container"); - - if (buttonType === 'slider' && !this.buttonAdded) { - this.buttonContainer.appendChild(rangeSlider); - rangeSlider.appendChild(iconContainer); - rangeSlider.appendChild(nameContainer); - rangeSlider.appendChild(rangeFill); - this.rangeFill = this.content.querySelector(".range-fill"); - } else if (buttonType === 'switch' || buttonType === 'custom') { - this.buttonContainer.appendChild(switchButton); - switchButton.appendChild(iconContainer); - switchButton.appendChild(nameContainer); - this.switchButton = this.content.querySelector(".switch-button"); + + if (showState) { + this.content.querySelector(".state").textContent = formatedState; + } + + function tapFeedback(content) { + let feedbackElement = content.querySelector('.feedback-element'); + if (!feedbackElement) { + feedbackElement = document.createElement('div'); + feedbackElement.setAttribute('class', 'feedback-element'); + content.appendChild(feedbackElement); + } + + feedbackElement.style.animation = 'tap-feedback .5s'; + setTimeout(() => { + feedbackElement.style.animation = 'none'; + content.removeChild(feedbackElement); + }, 500); } - if (hass.states[entityId].attributes.entity_picture || icon.startsWith("/api/image/")) { - iconContainer.innerHTML = ``; - } else { - iconContainer.innerHTML = `${name}
`; - - this.buttonAdded = true; - } - - function tapFeedback(content) { - let feedbackElement = content.querySelector('.feedback-element'); - if (!feedbackElement) { - feedbackElement = document.createElement('div'); - feedbackElement.setAttribute('class', 'feedback-element'); - content.appendChild(feedbackElement); + + function checkVerticalScroll(e) { + const x = e.pageX || (e.touches ? e.touches[0].pageX : 0); + const y = e.pageY || (e.touches ? e.touches[0].pageY : 0); + if (Math.abs(y - startY) > Math.abs(x - startX)) { + clearTimeout(timeoutId); // Cancel the activation of the slider if vertical scrolling is detected + handleEnd(); + } else { + document.removeEventListener('mousemove', checkVerticalScroll); + document.removeEventListener('touchmove', checkVerticalScroll); + document.addEventListener('mousemove', handleMove); + document.addEventListener('touchmove', handleMove); + } } - - feedbackElement.style.animation = 'tap-feedback .5s'; - setTimeout(() => { - feedbackElement.style.animation = 'none'; - content.removeChild(feedbackElement); - }, 500); - } - - function handleStart(e) { - startX = e.pageX || (e.touches ? e.touches[0].pageX : 0); - startY = e.pageY || (e.touches ? e.touches[0].pageY : 0); - startValue = rangeSlider.value; - if (e.target !== iconContainer.querySelector('ha-icon')) { - isDragging = true; - document.addEventListener('mouseup', handleEnd); - document.addEventListener('touchend', handleEnd); - document.addEventListener('mousemove', checkVerticalScroll); - document.addEventListener('touchmove', checkVerticalScroll); - - // Add a delay before activating the slider - timeoutId = setTimeout(() => { - updateRange(e.pageX || e.touches[0].pageX); - updateEntity(); - timeoutId = null; // Reset timeoutId once the delay has elapsed - }, 200); + + function handleEnd() { + isDragging = false; + movingVertically = false; + updateEntity(); + document.removeEventListener('mouseup', handleEnd); + document.removeEventListener('touchend', handleEnd); + document.removeEventListener('mousemove', handleMove); + document.removeEventListener('touchmove', handleMove); } - } - - function checkVerticalScroll(e) { - const x = e.pageX || (e.touches ? e.touches[0].pageX : 0); - const y = e.pageY || (e.touches ? e.touches[0].pageY : 0); - if (Math.abs(y - startY) > Math.abs(x - startX)) { - clearTimeout(timeoutId); // Cancel the activation of the slider if vertical scrolling is detected - handleEnd(); - } else { - document.removeEventListener('mousemove', checkVerticalScroll); - document.removeEventListener('touchmove', checkVerticalScroll); - document.addEventListener('mousemove', handleMove); - document.addEventListener('touchmove', handleMove); + + function updateEntity() { + if (entityId.startsWith("light.")) { + currentBrightness = brightness; + hass.callService('light', 'turn_on', { + entity_id: entityId, + brightness: currentBrightness + }); + } else if (currentVolume !== volume && entityId.startsWith("media_player.")) { + currentVolume = volume; + hass.callService('media_player', 'volume_set', { + entity_id: entityId, + volume_level: currentVolume + }); + } } - } - - function handleEnd() { - isDragging = false; - movingVertically = false; - updateEntity(); - document.removeEventListener('mouseup', handleEnd); - document.removeEventListener('touchend', handleEnd); - document.removeEventListener('mousemove', handleMove); - document.removeEventListener('touchmove', handleMove); - } - - function updateEntity() { - if (entityId.startsWith("light.")) { - currentBrightness = brightness; - hass.callService('light', 'turn_on', { - entity_id: entityId, - brightness: currentBrightness - }); - } else if (currentVolume !== volume && entityId.startsWith("media_player.")) { - currentVolume = volume; - hass.callService('media_player', 'volume_set', { - entity_id: entityId, - volume_level: currentVolume + + function handleMove(e) { + const x = e.pageX || (e.touches ? e.touches[0].pageX : 0); + const y = e.pageY || (e.touches ? e.touches[0].pageY : 0); + // Check if the movement is large enough to be considered a slide + if (isDragging && Math.abs(x - startX) > 10) { + updateRange(x); + } else if (isDragging && Math.abs(y - startY) > 10) { // If the movement is primarily vertical + isDragging = false; // Stop the slide + rangeSlider.value = startValue; // Reset to initial value + } + } + + if (!this.eventAdded && buttonType === 'switch') { + switchButton.addEventListener('click', () => tapFeedback(this.switchButton)); + switchButton.addEventListener('click', function(e) { + if (!e.target.closest('ha-icon')) { + toggleEntity(entityId); + } }); + addActions(this, this.iconContainer); + this.eventAdded = true; + } else if (!this.eventAdded && buttonType === 'slider') { + rangeSlider.addEventListener('mousedown', handleStart); + rangeSlider.addEventListener('touchstart', handleStart); + addActions(this, this.iconContainer); + this.eventAdded = true; + } else if (!this.eventAdded && buttonType === 'custom') { + switchButton.addEventListener('click', () => tapFeedback(this.switchButton)); + addActions(this, this.switchButton); + this.eventAdded = true; } - } - - function handleMove(e) { - const x = e.pageX || (e.touches ? e.touches[0].pageX : 0); - const y = e.pageY || (e.touches ? e.touches[0].pageY : 0); - // Check if the movement is large enough to be considered a slide - if (isDragging && Math.abs(x - startX) > 10) { - updateRange(x); - } else if (isDragging && Math.abs(y - startY) > 10) { // If the movement is primarily vertical - isDragging = false; // Stop the slide - rangeSlider.value = startValue; // Reset to initial value + + if (!this.isDragging && buttonType === 'slider') { + this.rangeFill.style.transition = 'all .2s'; + if (entityId.startsWith("light.")) { + this.rangeFill.style.transform = `translateX(${(currentBrightness / 255) * 100}%)`; + } else if (entityId.startsWith("media_player.")) { + this.rangeFill.style.transform = `translateX(${currentVolume * 100}%)`; + } } - } - - if (!this.eventAdded && buttonType === 'switch') { - switchButton.addEventListener('click', () => tapFeedback(this.switchButton)); - switchButton.addEventListener('click', function(e) { - if (!e.target.closest('ha-icon')) { - toggleEntity(entityId); + + function updateStyle(state, content) { + content.buttonContainer.style.opacity = state !== 'unavailable' ? '1' : '0.5'; + if (['switch', 'custom'].includes(buttonType)) { + const backgroundColor = ['on', 'open', 'cleaning', 'true', 'home', 'playing'].includes(state) ? 'var(--accent-color)' : 'rgba(0,0,0,0)'; + content.switchButton.style.backgroundColor = backgroundColor; } - }); - addActions(this, this.iconContainer); - this.eventAdded = true; - } else if (!this.eventAdded && buttonType === 'slider') { - rangeSlider.addEventListener('mousedown', handleStart); - rangeSlider.addEventListener('touchstart', handleStart); - addActions(this, this.iconContainer); - this.eventAdded = true; - } else if (!this.eventAdded && buttonType === 'custom') { - switchButton.addEventListener('click', () => tapFeedback(this.switchButton)); - addActions(this, this.switchButton); - this.eventAdded = true; - } - - if (!this.isDragging && buttonType === 'slider') { - this.rangeFill.style.transition = 'all .2s'; - if (entityId.startsWith("light.")) { - this.rangeFill.style.transform = `translateX(${(currentBrightness / 255) * 100}%)`; - } else if (entityId.startsWith("media_player.")) { - this.rangeFill.style.transform = `translateX(${currentVolume * 100}%)`; } - } - - function updateButtonStyle(state, content) { - content.buttonContainer.style.opacity = state !== 'unavailable' ? '1' : '0.5'; - if (['switch', 'custom'].includes(buttonType)) { - const backgroundColor = ['on', 'open', 'cleaning', 'true', 'home', 'playing'].includes(state) ? 'var(--accent-color)' : 'rgba(0,0,0,0)'; - content.switchButton.style.backgroundColor = backgroundColor; + + updateStyle(state, this); + + function updateRange(x) { + const rect = rangeSlider.getBoundingClientRect(); + const position = Math.min(Math.max(x - rect.left, 0), rect.width); + const percentage = position / rect.width; + if (entityId.startsWith("light.")) { + brightness = Math.round(percentage * 255); + } else if (entityId.startsWith("media_player.")) { + volume = percentage; + } + + rangeFill.style.transition = 'none'; + rangeFill.style.transform = `translateX(${percentage * 100}%)`; } - } - - updateButtonStyle(state, this); - - function updateRange(x) { - const rect = rangeSlider.getBoundingClientRect(); - const position = Math.min(Math.max(x - rect.left, 0), rect.width); - const percentage = position / rect.width; - if (entityId.startsWith("light.")) { - brightness = Math.round(percentage * 255); - } else if (entityId.startsWith("media_player.")) { - volume = percentage; + + if (buttonType === 'slider') { + if (entityId.startsWith("light.")) { + const rgbColor = hass.states[entityId].attributes.rgb_color; + const rgbColorOpacity = rgbColor ? `rgba(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]}, 0.5)` : `rgba(255, 255, 255, 0.5)`; + //rangeFill.style.backgroundColor = rgbColorOpacity; + this.rangeFill.style.backgroundColor = rgbColorOpacity; + } else { + this.rangeFill.style.backgroundColor = `var(--accent-color)`; + } } - - rangeFill.style.transition = 'none'; - rangeFill.style.transform = `translateX(${percentage * 100}%)`; - } - - if (buttonType === 'slider') { - const rgbColor = hass.states[entityId].attributes.rgb_color; - const rgbColorOpacity = rgbColor ? `rgba(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]}, 0.5)` : `rgba(255, 255, 255, 0.5)`; - rangeFill.style.backgroundColor = rgbColorOpacity; - this.rangeFill.style.backgroundColor = rgbColorOpacity; - } - - if (!this.styleAdded) { - const styleElement = document.createElement('style'); - const styles = ` - ${customStyles} + + const buttonStyles = ` ha-card { margin-top: 0 !important; background: none !important; @@ -1074,14 +1214,14 @@ class BubbleCard extends HTMLElement { } .range-fill { - z-index: 1; + z-index: 0; width: 100%; left: -100%; } .icon-container { position: absolute; - z-index: 2; + z-index: 1; width: 38px; height: 38px; margin: 6px; @@ -1104,17 +1244,19 @@ class BubbleCard extends HTMLElement { border-radius: 100%; } - .nameContainer { + .name-container { position: relative; - display: inline-flex; + display: ${!showState ? 'inline-flex' : 'block'}; margin-left: 58px; - z-index: 2; + z-index: 1; font-weight: 600; align-items: center; + line-height: ${!showState ? '16px' : '4px'}; } - .nameContainer p { - display: inline-flex; + .state { + font-size: 12px; + opacity: 0.7; } .feedback-element { @@ -1133,142 +1275,152 @@ class BubbleCard extends HTMLElement { 100% {transform: translateX(100%); opacity: 0;} } `; - - styleElement.innerHTML = styles; - this.content.appendChild(styleElement); - this.styleAdded = true; - } - } - - // Initialize separator - if (this.config.card_type === 'separator') { - const icon = !this.config.icon ? '' : this.config.icon; - const name = !this.config.name ? '' : this.config.name; - - if (!this.separatorAdded) { - const separatorContainer = document.createElement("div"); - separatorContainer.setAttribute("class", "separator-container"); - separatorContainer.innerHTML = ` -${name}
-${name}
+${formatedState}
+