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 !== '' ? `

${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 !== '' ? `

${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 = `Icon`; + } else { + iconContainer.innerHTML = ``; + } + + nameContainer.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 = `Icon`; - } else { - iconContainer.innerHTML = ``; + 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); + } } - nameContainer.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}

-
-
- ` - this.content.appendChild(separatorContainer); - this.separatorAdded = true; - - if (!this.styleAdded) { - const styleElement = document.createElement('style'); - const styles = ` - ${customStyles} - .separator-container { - display: inline-flex; - width: 100%; - } - .separator-container div:first-child { - display: inline-flex; - max-width: calc(100% - 38px); - } - .separator-container div ha-icon{ - display: inline-flex; - height: 24px; - width: 24px; - margin: 0 20px 0 8px; - transform: translateY(-2px); - } - .separator-container div h4{ - display: inline-flex; - margin: 0 20px 0 0; - font-size: 17px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + + addStyles(this, buttonStyles, customStyles, state, entityId); + break; + + // Initialize separator + case 'separator' : + if (!this.separatorAdded || this.editor) { + // Fix for editor mode + if (this.editor && this.separatorContainer) { + while (this.separatorContainer.firstChild) { + this.separatorContainer.removeChild(this.separatorContainer.firstChild); } - .separator-container div:last-child{ - display: inline-flex; - border-radius: 6px; - opacity: 0.3; - margin-left: 10px; - flex-grow: 1; - height: 6px; - align-self: center; - background-color: var(--background-color,var(--secondary-background-color)); - `; - - styleElement.innerHTML = styles; - this.content.appendChild(styleElement); - this.styleAdded = true; + } + // End of fix + + if (!this.separatorAdded) { + this.separatorContainer = document.createElement("div"); + this.separatorContainer.setAttribute("class", "separator-container"); + } + this.separatorContainer.innerHTML = ` +
+ +

${name}

+
+
+ ` + this.content.appendChild(this.separatorContainer); + this.separatorAdded = true; } - } - } - - // Initialize cover card - if (this.config.card_type === 'cover') { - const entity = this.config.entity; - const iconOpen = this.config.icon_open ? this.config.icon_open : 'mdi:window-shutter-open'; - const iconClosed = this.config.icon_close ? this.config.icon_close : 'mdi:window-shutter' - const icon = hass.states[this.config.entity].state === 'open' ? iconOpen : iconClosed; - const name = !this.config.name ? hass.states[entityId].attributes.friendly_name || '' : this.config.name; - const openCover = !this.config.open_service ? 'cover.open_cover' : this.config.open_service; - const closeCover = !this.config.close_service ? 'cover.close_cover' : this.config.close_service; - const stopCover = !this.config.stop_service ? 'cover.stop_cover' : this.config.stop_service; - - - if (!this.coverAdded) { - const coverContainer = document.createElement("div"); - coverContainer.setAttribute("class", "cover-container"); - coverContainer.innerHTML = ` -
-
-
-

${name}

-
-
- - - -
- ` - this.content.appendChild(coverContainer); - - const openButton = coverContainer.querySelector('.open'); - const stopButton = coverContainer.querySelector('.stop'); - const closeButton = coverContainer.querySelector('.close'); + + const separatorStyles = ` + .separator-container { + display: inline-flex; + width: 100%; + } + .separator-container div:first-child { + display: inline-flex; + max-width: calc(100% - 38px); + } + .separator-container div ha-icon{ + display: inline-flex; + height: 24px; + width: 24px; + margin: 0 20px 0 8px; + transform: translateY(-2px); + } + .separator-container div h4{ + display: inline-flex; + margin: 0 20px 0 0; + font-size: 17px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .separator-container div:last-child{ + display: inline-flex; + border-radius: 6px; + opacity: 0.3; + margin-left: 10px; + flex-grow: 1; + height: 6px; + align-self: center; + background-color: var(--background-color,var(--secondary-background-color)); + `; + + addStyles(this, separatorStyles, customStyles); + break; + + // Initialize cover card + case 'cover' : + const iconOpen = this.config.icon_open ? this.config.icon_open : 'mdi:window-shutter-open'; + const iconClosed = this.config.icon_close ? this.config.icon_close : 'mdi:window-shutter' + const openCover = !this.config.open_service ? 'cover.open_cover' : this.config.open_service; + const closeCover = !this.config.close_service ? 'cover.close_cover' : this.config.close_service; + const stopCover = !this.config.stop_service ? 'cover.stop_cover' : this.config.stop_service; + icon = hass.states[this.config.entity].state === 'open' ? iconOpen : iconClosed; + formatedState = this.config.entity ? hass.formatEntityState(hass.states[this.config.entity]) : ''; - openButton.addEventListener('click', () => { - hass.callService(openCover.split('.')[0], openCover.split('.')[1], { - entity_id: entity + + if (!this.coverAdded || this.editor) { + // Fix for editor mode + if (this.editor && this.coverContainer) { + while (this.coverContainer.firstChild) { + this.coverContainer.removeChild(this.coverContainer.firstChild); + } + } + // End of fix + + this.coverContainer = document.createElement("div"); + + this.coverContainer.setAttribute("class", "cover-container"); + this.coverContainer.innerHTML = ` +
+
+
+
+

${name}

+

${formatedState}

+
+
+
+ + + +
+ ` + this.content.appendChild(this.coverContainer); + + const openButton = this.coverContainer.querySelector('.open'); + const stopButton = this.coverContainer.querySelector('.stop'); + const closeButton = this.coverContainer.querySelector('.close'); + + openButton.addEventListener('click', () => { + hass.callService(openCover.split('.')[0], openCover.split('.')[1], { + entity_id: entityId + }); }); - }); - stopButton.addEventListener('click', () => { - hass.callService(stopCover.split('.')[0], stopCover.split('.')[1], { - entity_id: entity + stopButton.addEventListener('click', () => { + hass.callService(stopCover.split('.')[0], stopCover.split('.')[1], { + entity_id: entityId + }); }); - }); - closeButton.addEventListener('click', () => { - hass.callService(closeCover.split('.')[0], closeCover.split('.')[1], { - entity_id: entity + closeButton.addEventListener('click', () => { + hass.callService(closeCover.split('.')[0], closeCover.split('.')[1], { + entity_id: entityId + }); }); - }); - const iconContainer = this.content.querySelector('.icon-container'); - addActions(this, iconContainer); - - this.coverAdded = true; - } - - this.content.querySelector('.icon-container').innerHTML = ``; + + this.iconContainer = this.content.querySelector('.icon-container'); + addActions(this, this.iconContainer); - if (!this.styleAdded) { - const styleElement = document.createElement('style'); - const styles = ` - ${customStyles} + this.coverAdded = true; + } + + if (this.iconContainer) { + this.iconContainer.innerHTML = ``; + this.content.querySelector(".state").textContent = formatedState; + } + + const coverStyles = ` ha-card { margin-top: 0 !important; background: none !important; @@ -1280,12 +1432,6 @@ class BubbleCard extends HTMLElement { margin-bottom: 10px; } - .name { - display: inline-block; - margin-left: 10px; - font-weight: 600; - } - .cover-container { display: grid; } @@ -1296,7 +1442,7 @@ class BubbleCard extends HTMLElement { align-items: center; justify-content: center; cursor: pointer; - z-index: 2; + /*z-index: 1;*/ width: 48px; height: 48px; margin: 6px; @@ -1306,6 +1452,12 @@ class BubbleCard extends HTMLElement { box-sizing: border-box; } + .name-container { + font-weight: 600; + margin-left: 10px; + line-height: 4px; + } + .buttons-container { display: grid; align-self: center; @@ -1313,6 +1465,11 @@ class BubbleCard extends HTMLElement { grid-gap: 18px; } + .state { + font-size: 12px; + opacity: 0.7; + } + ha-icon { display: flex; height: 24px; @@ -1331,42 +1488,31 @@ class BubbleCard extends HTMLElement { border: none; } `; - - styleElement.innerHTML = styles; - this.content.appendChild(styleElement); - this.styleAdded = true; - } - } - - // Intitalize empty card - if (this.config.card_type === 'empty-column') { - if (!this.emptyCollumnAdded) { - const separatorContainer = document.createElement("div"); - separatorContainer.setAttribute("class", "empty-column"); - separatorContainer.innerHTML = ` -
-
- ` - this.content.appendChild(separatorContainer); - this.emptyColumnAdded = true; - } + + addStyles(this, coverStyles, customStyles, state, entityId); + break; + + // Intitalize empty card + case 'empty-column' : + if (!this.emptyCollumnAdded) { + const separatorContainer = document.createElement("div"); + separatorContainer.setAttribute("class", "empty-column"); + separatorContainer.innerHTML = ` +
+
+ ` + this.content.appendChild(separatorContainer); + this.emptyColumnAdded = true; + } + break; } } setConfig(config) { - // if (!config.card_type) { - // throw new Error("You need to define a card type (card_type: pop-up)"); - // } if (config.card_type === 'pop-up') { if (!config.hash) { throw new Error("You need to define an hash"); } - if (!config.icon) { - throw new Error("You need to define an icon"); - } - if (!config.name) { - throw new Error("You need to define a name"); - } } else if (config.card_type === 'horizontal-buttons-stack') { var definedLinks = {}; @@ -1394,17 +1540,6 @@ class BubbleCard extends HTMLElement { this.config = config; } - // static getStubConfig() { - // // const firstLight = Object.keys(this.hass.states).find((eid) => eid.substr(0, eid.indexOf(".")) === "light"); - // // console.log(firstLight); - // return { - // card_type: 'pop-up', - // name: 'Bubble Card', - // icon: 'mdi:heart', - // hash: '#######', - // } - // } - getCardSize() { return 1; } @@ -1482,13 +1617,21 @@ class BubbleCardEditor extends LitElement { return this._config.state || ''; } - get _state_unit() { - return this._config.state_unit || ''; + get _text() { + return this._config.text || ''; } get _hash() { return this._config.hash || '#pop-up-name'; } + + get _trigger_entity() { + return this._config.trigger_entity || ''; + } + + get _trigger_state() { + return this._config.trigger_state || ''; + } get _margin_top_mobile() { return this._config.margin_top_mobile || '0px'; @@ -1499,7 +1642,7 @@ class BubbleCardEditor extends LitElement { } get _width_desktop() { - return this._config.width_desktop || '600px'; + return this._config.width_desktop || '540px'; } get _is_sidebar_hidden() { @@ -1529,6 +1672,10 @@ class BubbleCardEditor extends LitElement { get _auto_order() { return this._config.auto_order || false; } + + get _show_state() { + return this._config.show_state || false; + } render() { if (!this.hass) { @@ -1609,7 +1756,8 @@ class BubbleCardEditor extends LitElement { return html`
${this.makeDropdown("Card type", "card_type", cardTypeList)} - This card allows you to convert any vertical-stack into a pop-up. Each pop-up can be opened by targeting its link (e.g. '#pop-up-name'), with navigation_path or with the horizontal buttons stack that is included.

It must be placed within a vertical-stack card at the top most position to function properly.
+

Pop-up

+ This card allows you to convert any vertical-stack into a pop-up. Each pop-up can be opened by targeting its link (e.g. '#pop-up-name'), with navigation_path or with the horizontal buttons stack that is included.

It must be placed within a vertical-stack card at the top most position to function properly. The pop-up will be hidden by default until you open it.
- ${this.makeDropdown("Icon", "icon")} - ${this.makeDropdown("Entity to toggle (e.g. room light group)", "entity", allEntitiesList)} - ${this.makeDropdown("Entity state to display (e.g. room temperature)", "state", allEntitiesList)} + ${this.makeDropdown("Optional - Icon", "icon")} + ${this.makeDropdown("Optional - Entity to toggle (e.g. room light group)", "entity", allEntitiesList)} + ${this.makeDropdown("Optional - Entity state to display (e.g. room temperature)", "state", allEntitiesList)} +

Pop-up trigger

+ This allows you to apen this pop-up based on the state of any entity, for example you can open a "Security" pop-up with a camera when a person is in front of your house. You can also create a toggle helper (input_boolean) and trigger its opening/closing in an automation. + ${this.makeDropdown("Optional - Entity to open the pop-up based on its state", "trigger_entity", allEntitiesList)} - +
- +
+

Styling options

+ + + +
+ +
+
${this.makeVersion()}
`; @@ -1673,17 +1843,29 @@ class BubbleCardEditor extends LitElement { return html`
${this.makeDropdown("Card type", "card_type", cardTypeList)} +

Button

This card can be a slider or a button, allowing you to toggle your entities, control the brightness of your lights and the volume of your media players. To access color / control of an entity, simply tap on the icon. ${this.makeDropdown(this._button_type !== 'slider' ? "Entity (toggle)" : "Entity (light or media_player)", "entity", allEntitiesList)} ${this.makeDropdown("Button type", "button_type", buttonTypeList)} - ${this.makeDropdown("Icon", "icon")} + ${this.makeDropdown("Optional - Icon", "icon")} + + +
+ +
+
${this.makeVersion()}
`; @@ -1691,6 +1873,7 @@ class BubbleCardEditor extends LitElement { return html`
${this.makeDropdown("Card type", "card_type", cardTypeList)} +

Separator

This card is a simple separator for dividing your pop-up into categories / sections. e.g. Lights, Devices, Covers, Settings, Automations... { this.buttonIndex++; - fireEvent(this, "config-changed", { - config: this._config - }); + + const originalOpacity = addButton.style.opacity; + const originalText = addButton.innerText; + + addButton.style.opacity = '0.6'; + addButton.style.transition = 'opacity 1s'; + addButton.innerText = "Loading..."; + + setTimeout(() => { + addButton.style.opacity = originalOpacity; + addButton.innerText = originalText; + }, 5000); }); this.buttonAdded = true; } - fireEvent(this, "config-changed", { - config: this._config - }); return html`
${this.makeDropdown("Card type", "card_type", cardTypeList)} +

Horizontal buttons stack

This card is the companion to the pop-up card, allowing you to open the corresponding pop-ups. It also allows you to open any page of your dashboard. In addition, you can add your motion sensors so that the order of the buttons adapts according to the room you just entered. This card is scrollable, remains visible and acts as a footer.

Please note that this card may take some time to load in edit mode.
- +
${this.makeButton()}
+

Styling options

+ + + +
+ +
+
${this.makeVersion()}
`; @@ -1753,38 +1961,39 @@ class BubbleCardEditor extends LitElement { return html`
${this.makeDropdown("Card type", "card_type", cardTypeList)} +

Cover

This card allows you to control your covers. ${this.makeDropdown("Entity", "entity", coverList)} - ${this.makeDropdown("Open icon", "icon_open")} - ${this.makeDropdown("Closed icon", "icon_close")} + ${this.makeDropdown("Optional - Open icon", "icon_open")} + ${this.makeDropdown("Optional - Closed icon", "icon_close")} ${this.makeVersion()}
`; @@ -1792,6 +2001,7 @@ class BubbleCardEditor extends LitElement { return html`
${this.makeDropdown("Card type", "card_type", cardTypeList)} +

Empty column

Just an empty card to fill any empty column. ${this.makeVersion()}
@@ -1800,6 +2010,7 @@ class BubbleCardEditor extends LitElement { return html`
${this.makeDropdown("Card type", "card_type", cardTypeList)} +

Bubble Card

You need to add a card type first. ${this.makeVersion()}
@@ -1810,7 +2021,7 @@ class BubbleCardEditor extends LitElement { makeDropdown(label, configValue, items) { const hass = this.hass; - if (label === 'Icon' || label === 'Open icon' || label === 'Closed icon') { + if (label.includes('icon') || label.includes('Icon')) { return html`
eid.substr(0, eid.indexOf(".")) === "binary_sensor" - // ); for (let i = 1; i <= this.buttonIndex; i++) { buttons.push(html` @@ -1853,37 +2060,37 @@ class BubbleCardEditor extends LitElement { Button ${i}
+ - `); } - fireEvent(this, "config-changed", { - config: this._config - }); return buttons; } @@ -1997,5 +2201,5 @@ window.customCards.push({ type: "bubble-card", name: "Bubble Card", preview: false, - description: "A minimalist card collection with a nice pop-up touch.", + description: "A minimalist card collection with a nice pop-up touch." });