diff --git a/dist/bubble-card.js b/dist/bubble-card.js index 100e3059..97113498 100644 --- a/dist/bubble-card.js +++ b/dist/bubble-card.js @@ -1 +1,483 @@ -var version="v1.5.3";let editor,entityStates={},lastCall={entityId:null,stateChanged:null,timestamp:null};async function addResource(t){let e=(await t.callWS({type:"lovelace/resources"})).find((t=>t.url.includes("bubble-pop-up.js")));e&&await t.callWS({type:"lovelace/resources/delete",resource_id:e.id})}class BubbleCard extends HTMLElement{constructor(){if(super(),!window.eventAdded){const t=history.pushState;window.popUpInitialized=!1,history.pushState=function(){t.apply(history,arguments),window.dispatchEvent(new Event("pushstate"))};const e=history.replaceState;history.replaceState=function(){e.apply(history,arguments),window.dispatchEvent(new Event("replacestate"))},["pushstate","replacestate","click","popstate","mousedown","touchstart"].forEach((t=>{window.addEventListener(t,i)}),{passive:!0});const n=new Event("urlChanged");function i(){const t=window.location.href;t!==this.currentUrl&&(window.dispatchEvent(n),this.currentUrl=t)}const o=()=>{window.dispatchEvent(n),window.addEventListener("popstate",i,{passive:!0})};window.addEventListener("popUpInitialized",o,{passive:!0}),window.eventAdded=!0}}set hass(hass){if(window.resourceCleared||(addResource(hass),window.resourceCleared=!0),!this.content){this.attachShadow({mode:"open"}),this.shadowRoot.innerHTML='\n \n
\n
\n
\n ',this.card=this.shadowRoot.querySelector("ha-card"),this.content=this.shadowRoot.querySelector("div");const t=new Promise((t=>{t(document.querySelector("body > home-assistant").shadowRoot.querySelector("home-assistant-main").shadowRoot.querySelector("ha-drawer > partial-panel-resolver > ha-panel-lovelace").shadowRoot.querySelector("hui-root").shadowRoot.querySelector("div"))}));t.then((t=>{this.editorElement=t}))}let customStyles=this.config.styles?this.config.styles:"",entityId=this.config.entity&&hass.states[this.config.entity]?this.config.entity:"",icon=!this.config.icon&&this.config.entity?hass.states[entityId].attributes.icon||hass.states[entityId].attributes.entity_picture||"":this.config.icon||"",name=this.config.name?this.config.name:this.config.entity?hass.states[entityId].attributes.friendly_name:"",widthDesktop=this.config.width_desktop||"540px",widthDesktopDivided=widthDesktop?widthDesktop.match(/(\d+)(\D+)/):"",shadowOpacity=void 0!==this.config.shadow_opacity?this.config.shadow_opacity:"0",bgBlur=void 0!==this.config.bg_blur?this.config.bg_blur:"10",isSidebarHidden=this.config.is_sidebar_hidden||!1,state=entityId?hass.states[entityId].state:"",stateOn=["on","open","cleaning","true","home","playing"].includes(state)||0!==Number(state)&&!isNaN(Number(state)),formatedState,autoClose=this.config.auto_close||!1,riseAnimation=void 0===this.config.rise_animation||this.config.rise_animation,marginCenter=this.config.margin?"0"!==this.config.margin?this.config.margin:"0px":"7px",popUpHash=this.config.hash,popUpOpen,startTouchY,lastTouchY;function toggleEntity(t){hass.callService("homeassistant","toggle",{entity_id:t})}function stateChanged(t){let e=Date.now();if(lastCall.entityId===t&&e-lastCall.timestamp<100)return lastCall.stateChanged;if(!hass.states[t]||!hass.states[t].state)return!1;let n=hass.states[t].state,i=hass.states[t].attributes.rgb_color;entityStates[t]||(entityStates[t]={prevState:null,prevColor:null});let o=entityStates[t].prevState!==n||entityStates[t].prevColor!==i;return entityStates[t].prevState=n,entityStates[t].prevColor=i,lastCall={entityId:t,stateChanged:o,timestamp:e},o}this.editorElement&&(editor=this.editorElement.classList.contains("edit-mode"));const addStyles=function(context,styles,customStyles,state,entityId,stateChangedVar,path="",element=context.content){const customStylesEval=customStyles?eval("`"+customStyles+"`"):"";let styleAddedKey=styles+"Added";if(!context[styleAddedKey]||context.previousStyle!==customStylesEval||stateChangedVar||context.previousConfig!==context.config){if(!context[styleAddedKey]){if(context.styleElement=element.querySelector("style"),!context.styleElement){context.styleElement=document.createElement("style");const t=path?element.querySelector(path):element;t?.appendChild(context.styleElement)}context[styleAddedKey]=!0}context.styleElement.innerHTML!==customStylesEval+styles&&(context.styleElement.innerHTML=customStylesEval+styles),context.previousStyle=customStylesEval,context.previousConfig=context.config}},forwardHaptic=t=>{fireEvent(window,"haptic",t)},navigate=(t,e,n=!1)=>{n?history.replaceState(null,"",e):history.pushState(null,"",e),fireEvent(window,"location-changed",{replace:n})},handleActionConfig=(t,e,n,i)=>{if(!i.confirmation||i.confirmation.exemptions&&i.confirmation.exemptions.some((t=>t.user===e.user.id))||(forwardHaptic("warning"),confirm(i.confirmation.text||`Are you sure you want to ${i.action}?`)))switch(i.action){case"more-info":(this.config.entity||this.config.camera_image)&&fireEvent(t,"hass-more-info",{entityId:this.config.entity?this.config.entity:this.config.camera_image});break;case"navigate":i.navigation_path&&navigate(t,i.navigation_path);break;case"url":i.url_path&&window.open(i.url_path);break;case"toggle":this.config.entity&&(toggleEntity(this.config.entity),forwardHaptic("success"));break;case"call-service":{if(!i.service)return void forwardHaptic("failure");const[t,n]=i.service.split(".",2);e.callService(t,n,i.service_data,i.target),forwardHaptic("success");break}case"fire-dom-event":fireEvent(t,"ll-custom",i)}},handleAction=(t,e,n,i)=>{let o;"double_tap"===i&&this.config.double_tap_action?o=this.config.double_tap_action:"hold"===i&&this.config.hold_action?o=this.config.hold_action:"tap"===i&&this.config.tap_action?o=this.config.tap_action:"double_tap"!==i||this.config.double_tap_action?("hold"!==i||this.config.hold_action)&&("tap"!==i||this.config.tap_action)||(o={action:"more-info"}):o={action:"toggle"},handleActionConfig(t,e,n,o)},addAction=function(){let t,e;return function(n,i,o,a){o.addEventListener(n,(()=>{const i=(new Date).getTime();"click"===n?i-(e||0)<250?(clearTimeout(t),handleAction(a,hass,{},"double_tap")):t=setTimeout((()=>{handleAction(a,hass,{},"tap")}),250):handleAction(a,hass,{},"hold"),e=i}),{passive:!0})}}();function addActions(t,e){addAction("click","tap",e,t),addAction("contextmenu","hold",e,t)}if(entityId){const e=!!hass.states[entityId].attributes&&hass.states[entityId].attributes;this.newPictureUrl=!!e.entity_picture&&e.entity_picture}function createIcon(t,e,n,i,o){updateIcon(t,e,n,i,o),editor||e.connection.subscribeEvents((a=>{a.data.entity_id===n&&t.newPictureUrl!==t.currentPictureUrl&&(t.currentPictureUrl=t.newPictureUrl,updateIcon(t,e,n,i,o))}),"state_changed")}function updateIcon(t,e,n,i,o){for(;o.firstChild;)o.removeChild(o.firstChild);if(t.newPictureUrl&&!t.config.icon){const e=document.createElement("img");e.setAttribute("src",t.newPictureUrl),e.setAttribute("class","entity-picture"),e.setAttribute("alt","Icon"),o&&o.appendChild(e)}else{const t=document.createElement("ha-icon");t.setAttribute("icon",i),t.setAttribute("class","icon"),o&&o.appendChild(t)}}function isColorCloseToWhite(t){let e=[220,220,190];for(let n=0;n<3;n++)if(t[n]{if(this.host&&this.host===this.getRootNode().host){if(!this.popUp&&(this.verticalStack=this.getRootNode(),this.popUp=this.verticalStack.querySelector("#root"),!window.popUpInitialized&&this.popUp)){this.config.back_open||!1?localStorage.setItem("backOpen",!0):localStorage.setItem("backOpen",!1);if("true"===localStorage.getItem("backOpen")){window.backOpen=!0;const _=new Event("popUpInitialized");setTimeout((()=>{window.dispatchEvent(_)}),0)}else window.backOpen=!1,popUpOpen=popUpHash+!1,history.replaceState(null,null,location.href.split("#")[0]);window.popUpInitialized=!0}const t=this.popUp,e=this.config.text||"",n=this.config.state;formatedState=n?hass.formatEntityState(hass.states[n])+" "+e:e;const i=this.config.margin_top_mobile&&"0"!==this.config.margin_top_mobile?this.config.margin_top_mobile:"0px",o=this.config.margin_top_desktop&&"0"!==this.config.margin_top_desktop?this.config.margin_top_desktop:"0px",a=this.config.entity?"flex":"none";let s,r;if(state=n?hass.states[n].state:"",this.headerAdded){if(entityId){const w=this.content.querySelector("#header-container .header-icon"),x=this.content.querySelector("#header-container h2"),k=this.content.querySelector("#header-container p"),C=this.content.querySelector("#header-container .power-button");w.innerHTML="",createIcon(this,hass,entityId,icon,w),x.textContent=name,k.textContent=formatedState,C.setAttribute("style",`display: ${a};`)}}else{const $=document.createElement("div");$.setAttribute("id","header-container");const S=document.createElement("div");$.appendChild(S);const E=document.createElement("div");E.setAttribute("class","header-icon"),S.appendChild(E),createIcon(this,hass,entityId,icon,E),addActions(this,E);const I=document.createElement("h2");I.textContent=name,S.appendChild(I);const O=document.createElement("p");O.textContent=formatedState,S.appendChild(O);const A=document.createElement("ha-icon");A.setAttribute("class","power-button"),A.setAttribute("icon","mdi:power"),A.setAttribute("style",`display: ${a};`),S.appendChild(A);const L=document.createElement("button");L.setAttribute("class","close-pop-up"),L.onclick=function(){history.replaceState(null,null,location.href.split("#")[0]),localStorage.setItem("isManuallyClosed_"+popUpHash,!0)},$.appendChild(L);const T=document.createElement("ha-icon");T.setAttribute("icon","mdi:close"),L.appendChild(T),this.content.appendChild($),this.header=S,this.headerAdded=!0}function l(){toggleEntity(entityId)}function c(t){"Escape"===t.key&&(popUpOpen=popUpHash+!1,history.replaceState(null,null,location.href.split("#")[0]),localStorage.setItem("isManuallyClosed_"+popUpHash,!0))}function d(t){window.hash===popUpHash&&m(),startTouchY=t.touches[0].clientY,lastTouchY=startTouchY}function h(t){t.touches[0].clientY-startTouchY>300&&t.touches[0].clientY>lastTouchY&&(popUpOpen=popUpHash+!1,history.replaceState(null,null,location.href.split("#")[0]),popUpOpen=popUpHash+!1,localStorage.setItem("isManuallyClosed_"+popUpHash,!0)),lastTouchY=t.touches[0].clientY}if(this.eventAdded||editor||(window["checkHashRef_"+popUpHash]=p,window.addEventListener("urlChanged",window["checkHashRef_"+popUpHash],{passive:!0}),window.addEventListener("click",(function(t){if(location.hash===popUpHash&&m(),!window.justOpened)return;const e=t.composedPath();!e||e.some((t=>"HA-MORE-INFO-DIALOG"===t.nodeName))||e.some((t=>"root"===t.id&&!t.classList.contains("close-pop-up")))||popUpOpen!==popUpHash+!0||(popUpOpen=popUpHash+!1,history.replaceState(null,null,location.href.split("#")[0]),localStorage.setItem("isManuallyClosed_"+popUpHash,!0))}),{passive:!0}),this.eventAdded=!0),entityId){const U=hass.states[entityId].attributes.rgb_color;this.rgbColor=U?isColorCloseToWhite(U)?"rgb(255,220,200)":`rgb(${U})`:stateOn?entityId.startsWith("light.")?"rgba(255,220,200, 0.5)":"var(--accent-color)":"rgba(255, 255, 255, 1",this.rgbColorOpacity=U?isColorCloseToWhite(U)?"rgba(255,220,200, 0.5)":`rgba(${U}, 0.5)`:entityId&&stateOn?entityId.startsWith("light.")?"rgba(255,220,200, 0.5)":"var(--accent-color)":"var(--background-color,var(--secondary-background-color))",r=convertToRGBA(color,0),this.iconFilter=U?isColorCloseToWhite(U)?"none":"brightness(1.1)":"none"}function p(){editor||(window.hash=location.hash.split("?")[0],window.hash===popUpHash?g():t.classList.contains("open-pop-up")&&f())}const u=this.content;function g(){t.classList.remove("close-pop-up"),t.classList.add("open-pop-up"),u.querySelector(".power-button").addEventListener("click",l,{passive:!0}),window.addEventListener("keydown",c,{passive:!0}),t.addEventListener("touchstart",d,{passive:!0}),t.addEventListener("touchmove",h,{passive:!0}),popUpOpen=popUpHash+!0,setTimeout((()=>{window.justOpened=!0}),10),m()}function f(){t.classList.remove("open-pop-up"),t.classList.add("close-pop-up"),u.querySelector(".power-button").removeEventListener("click",l),window.removeEventListener("keydown",c),t.removeEventListener("touchstart",d),t.removeEventListener("touchmove",h),popUpOpen=popUpHash+!1,window.justOpened=!1,clearTimeout(s)}function m(){clearTimeout(s),autoClose>0&&(s=setTimeout(b,autoClose))}function b(){history.replaceState(null,null,location.href.split("#")[0])}const y=`\n ha-card {\n margin-top: 0 !important;\n background: none !important;\n border: none !important;\n }\n .card-content {\n width: 100% !important;\n padding: 0 !important;\n }\n #root {\n transition: all 1s !important;\n position: fixed !important;\n margin: 0 -${marginCenter}; /* 7px */\n width: 100%;\n background-color: ${rgbaColor};\n box-shadow: 0px 0px 50px rgba(0,0,0,${shadowOpacity/100});\n backdrop-filter: blur(${bgBlur}px);\n -webkit-backdrop-filter: blur(${bgBlur}px);\n border-radius: 42px;\n box-sizing: border-box;\n top: calc(120% + ${i} + var(--header-height));\n grid-gap: 12px !important;\n gap: 12px !important;\n grid-auto-rows: min-content;\n padding: 18px 18px 220px 18px !important;\n height: 100% !important;\n -ms-overflow-style: none; /* for Internet Explorer, Edge */\n scrollbar-width: none; /* for Firefox */\n overflow-y: auto; \n overflow-x: hidden; \n z-index: 1 !important; /* Higher value hide the more-info panel */\n /* For older Safari but not working with Firefox */\n /* display: grid !important; */ \n }\n #root > bubble-card:first-child::after {\n content: '';\n display: block;\n position: sticky;\n top: 0;\n left: -50px;\n margin: -70px 0 -36px -36px;\n overflow: visible;\n width: 200%;\n height: 100px;\n background: linear-gradient(0deg, ${r} 0%, ${rgbaColor} 80%);\n z-index: 0;\n } \n #root::-webkit-scrollbar {\n display: none; /* for Chrome, Safari, and Opera */\n }\n #root > bubble-card:first-child {\n position: sticky;\n top: 0;\n z-index: 1;\n background: none !important;\n overflow: visible;\n }\n #root.open-pop-up {\n /*will-change: transform;*/\n transform: translateY(-120%);\n transition: transform .4s !important;\n }\n #root.open-pop-up > * {\n /* Block child items to overflow and if they do clip them */\n /*max-width: calc(100vw - 38px);*/\n max-width: 100% !important;\n overflow-x: clip;\n }\n #root.close-pop-up { \n transform: translateY(-20%);\n transition: transform .4s !important;\n box-shadow: none;\n }\n @media only screen and (min-width: 768px) {\n #root {\n top: calc(120% + ${o} + var(--header-height));\n width: calc(${widthDesktop}${"%"!==widthDesktopDivided[2]||isSidebarHidden?"":" - var(--mdc-drawer-width)"}) !important;\n left: calc(50% - ${widthDesktopDivided[1]/2}${widthDesktopDivided[2]});\n margin: 0 !important;\n }\n } \n @media only screen and (min-width: 870px) {\n #root {\n left: calc(50% - ${widthDesktopDivided[1]/2}${widthDesktopDivided[2]} + ${isSidebarHidden?"0px":"var(--mdc-drawer-width) "+("%"===widthDesktopDivided[2]?"":"/ 2")});\n }\n } \n #root.editor {\n position: inherit !important;\n width: 100% !important;\n padding: 18px !important;\n }\n `,v=`\n ha-card {\n margin-top: 0 !important;\n }\n #header-container {\n display: inline-flex;\n ${icon||name||entityId||state||e?"":"flex-direction: row-reverse;"}\n width: 100%;\n margin: 0;\n padding: 0;\n }\n #header-container > div {\n display: ${icon||name||entityId||state||e?"inline-flex":"none"};\n align-items: center;\n position: relative;\n padding: 6px;\n z-index: 1;\n flex-grow: 1;\n background-color: ${entityId?this.rgbColorOpacity:"var(--background-color,var(--secondary-background-color))"};\n transition: background 1s;\n border-radius: 25px;\n margin-right: 14px;\n backdrop-filter: blur(14px);\n -webkit-backdrop-filter: blur(14px);\n }\n .header-icon {\n display: inline-flex;\n width: 38px;\n height: 38px;\n background-color: var(--card-background-color,var(--ha-card-background));\n border-radius: 100%;\n margin: 0 10px 0 0;\n cursor: ${this.config.entity||this.config.double_tap_action||this.config.tap_action||this.config.hold_action?"pointer":"default"}; \n flex-wrap: wrap;\n align-content: center;\n justify-content: center;\n overflow: hidden;\n }\n .header-icon > ha-icon {\n color: ${stateOn?this.rgbColor?this.rgbColor:"var(--accent-color)":"inherit"};\n opacity: ${stateOn?"1":"0.6"};\n filter: ${this.iconFilter};\n }\n .header-icon::after {\n content: '';\n position: absolute;\n width: 38px;\n height: 38px;\n display: block;\n opacity: 0.2;\n transition: background-color 1s;\n border-radius: 50%;\n background-color: ${stateOn?this.rgbColor?this.rgbColor:"var(--accent-color)":"var(--card-background-color,var(--ha-card-background))"};\n }\n .entity-picture {\n height: calc(100% + 16px);\n width: calc(100% + 16px);\n }\n #header-container h2 {\n display: inline-flex;\n margin: 0 18px 0 0;\n /*line-height: 0px;*/\n z-index: 1;\n font-size: 20px;\n }\n #header-container p {\n display: inline-flex;\n line-height: 0px;\n font-size: 16px;\n }\n .power-button {\n cursor: pointer; \n flex-grow: inherit; \n width: 24px;\n height: 24px;\n border-radius: 12px;\n margin: 0 10px;\n background: none !important;\n justify-content: flex-end;\n background-color: var(--background-color,var(--secondary-background-color));\n }\n .close-pop-up {\n height: 50px;\n width: 50px;\n border: none;\n border-radius: 50%;\n z-index: 1;\n background: var(--background-color,var(--secondary-background-color));\n color: var(--primary-text-color);\n flex-shrink: 0;\n cursor: pointer;\n }\n `;addStyles(this,y,customStyles,state,entityId,"","",t),addStyles(this,v,customStyles,state,entityId,stateChanged(entityId)),editor?(t.classList.add("editor"),t.classList.remove("open-pop-up"),t.classList.remove("close-pop-up")):t.classList.remove("editor")}else this.host=this.getRootNode().host},i=this.config.trigger_entity?this.config.trigger_entity:"",o=this.config.trigger_state?this.config.trigger_state:"",a=!!this.config.trigger_close&&this.config.trigger_close,s=this.config.state;if(this.popUp!==this.getRootNode().querySelector("#root")){let K=setInterval((()=>{n(),this.popUp&&clearInterval(K)}),20);setTimeout((()=>{if(!this.popUp)throw this.errorTriggered=!0,clearInterval(K),new Error("Pop-up card must be placed inside a vertical_stack! If it's already the case, please ignore this error 🍻")}),4e3)}else!editor&&this.wasEditing?(n(),this.wasEditing=!1):(popUpHash===window.hash&&(stateChanged(entityId)||stateChanged(s))||editor)&&(n(),editor&&(this.wasEditing=!0));if(this.popUp&&stateChanged(i)&&hass.states[i]){null===localStorage.getItem("previousTriggerState_"+popUpHash)&&localStorage.setItem("previousTriggerState_"+popUpHash,""),null===localStorage.getItem("isManuallyClosed_"+popUpHash)&&localStorage.setItem("isManuallyClosed_"+popUpHash,"false"),null===localStorage.getItem("isTriggered_"+popUpHash)&&localStorage.setItem("isTriggered_"+popUpHash,"false");let Z=localStorage.getItem("previousTriggerState_"+popUpHash),J="true"===localStorage.getItem("isManuallyClosed_"+popUpHash),Q="true"===localStorage.getItem("isTriggered_"+popUpHash);hass.states[i].state!==o||null!==Z||Q||(navigate("",popUpHash),Q=!0,localStorage.setItem("isTriggered_"+popUpHash,Q)),hass.states[i].state!==Z&&(J=!1,localStorage.setItem("previousTriggerState_"+popUpHash,hass.states[i].state),localStorage.setItem("isManuallyClosed_"+popUpHash,J)),hass.states[i].state!==o||J?hass.states[i].state!==o&&a&&this.popUp.classList.contains("open-pop-up")&&Q&&!J&&(history.replaceState(null,null,location.href.split("#")[0]),popUpOpen=popUpHash+!1,Q=!1,J=!0,localStorage.setItem("isManuallyClosed_"+popUpHash,J),localStorage.setItem("isTriggered_"+popUpHash,Q)):(navigate("",popUpHash),Q=!0,localStorage.setItem("isTriggered_"+popUpHash,Q))}break;case"horizontal-buttons-stack":const r=(t,e,n)=>{const i=document.createElement("button");return i.setAttribute("class",`button ${e.substring(1)}`),i.innerHTML=`\n ${""!==n?``:""}\n ${""!==t?`

${t}

`:""}\n `,i.hasListener||(i.addEventListener("click",(t=>{t.stopPropagation(),popUpOpen=location.hash+!0;localStorage.getItem("isManuallyClosed_"+e);popUpOpen!==e+!0?(navigate("",e),popUpOpen=e+!0):(history.replaceState(null,null,location.href.split("#")[0]),popUpOpen=e+!1)}),{passive:!0}),i.hasListener=!0),i};if(!this.buttonsAdded){const tt=document.createElement("div");tt.setAttribute("class","horizontal-buttons-stack-container"),this.content.appendChild(tt),this.buttonsContainer=tt}const l=(t,e)=>{if(hass.states[e].attributes.rgb_color){const n=hass.states[e].attributes.rgb_color,i=isColorCloseToWhite(n)?"rgba(255,220,200, 0.5)":`rgba(${n}, 0.5)`;t.style.backgroundColor=i,t.style.border="1px solid rgba(0,0,0,0)"}else hass.states[e].attributes.rgb_color||"on"!=hass.states[e].state?(t.style.backgroundColor="rgba(0,0,0,0)",t.style.border="1px solid var(--primary-text-color)"):(t.style.backgroundColor="rgba(255,255,255,0.5)",t.style.border="1px solid rgba(0,0,0,0)")};let c=[],d=1;for(;this.config[d+"_link"];){const et=d+"_",nt=this.config[et+"name"]||"",it=this.config[et+"pir_sensor"];icon=this.config[et+"icon"]||"";const ot=this.config[et+"link"],at=this.config[et+"entity"];c.push({button:nt,pirSensor:it,icon:icon,link:ot,lightEntity:at}),d++}if(this.config.auto_order&&c.sort(((t,e)=>{if(t.pirSensor&&e.pirSensor){if("on"===hass.states[t.pirSensor].state&&"on"===hass.states[e.pirSensor].state){return hass.states[t.pirSensor].last_updated{const e=r(t.button,t.link,t.icon);st[t.link]=e,this.buttonsContainer.appendChild(e)})),this.buttonsAdded=!0,this.buttons=st}let h=0,p=12;async function u(){let t=[];for(let e of c){this.buttons[e.link]&&(t.push(localStorage.getItem(`buttonWidth-${e.link}`)),t.push(localStorage.getItem(`buttonContent-${e.link}`)))}let e=await Promise.all(t),n=0;for(let t of c){let i=this.buttons[t.link];if(i){let o=e[n],a=e[n+1];n+=2,o&&"0"!==o&&a===i.innerHTML&&this.previousConfig===this.config||(o=i.offsetWidth,await localStorage.setItem(`buttonWidth-${t.link}`,o),await localStorage.setItem(`buttonContent-${t.link}`,i.innerHTML),this.previousConfig=this.config),i.style.transform=`translateX(${h}px)`,h+=parseInt(o)+p}t.lightEntity&&l(i,t.lightEntity)}}u.call(this);const g=`\n ha-card {\n border-radius: 0;\n }\n .horizontal-buttons-stack {\n width: 100%;\n margin-top: 0 !important;\n background: none !important;\n position: fixed;\n height: 51px;\n bottom: 16px;\n left: ${marginCenter};\n z-index: 1 !important; /* Higher value hide the more-info panel */\n }\n @keyframes from-bottom {\n 0% {transform: translateY(200px);}\n 20% {transform: translateY(200px);}\n 46% {transform: translateY(-8px);}\n 56% {transform: translateY(1px);}\n 62% {transform: translateY(-2px);}\n 70% {transform: translateY(0);}\n 100% {transform: translateY(0);}\n }\n .horizontal-buttons-stack-container {\n width: max-content;\n position: relative;\n height: 51px;\n }\n .button {\n display: flex;\n position: absolute;\n box-sizing: border-box !important;\n border: 1px solid var(--primary-text-color);\n align-items: center;\n height: 50px;\n white-space: nowrap;\n width: auto;\n border-radius: 25px;\n z-index: 1;\n padding: 16px;\n background: none;\n transition: background-color 1s, border 1s, transform 1s;\n color: var(--primary-text-color);\n }\n .icon {\n height: 24px;\n }\n .card-content {\n width: calc(100% + 18px);\n box-sizing: border-box !important;\n margin: 0 -36px !important;\n padding: 0 36px !important;\n overflow: scroll !important;\n -ms-overflow-style: none;\n scrollbar-width: none;\n -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%);\n /* mask-image: linear-gradient(90deg, transparent 2%, rgba(0, 0, 0, 1) 6%, rgba(0, 0, 0, 1) 96%, transparent 100%); */\n /* -webkit-mask-image: linear-gradient(90deg, transparent 2%, rgba(0, 0, 0, 1) 6%, rgba(0, 0, 0, 1) 96%, transparent 100%); */\n }\n .horizontal-buttons-stack::before {\n content: '';\n position: absolute;\n top: -32px;\n left: -100%;\n display: block;\n background: linear-gradient(0deg, var(--background-color, var(--primary-background-color)) 50%, rgba(79, 69, 87, 0));\n width: 200%;\n height: 100px;\n }\n .card-content::-webkit-scrollbar {\n display: none;\n }\n @media only screen and (min-width: 768px) {\n .card-content {\n position: fixed;\n width: ${widthDesktop} !important;\n left: calc(50% - ${widthDesktopDivided[1]/2}${widthDesktopDivided[2]});\n margin-left: -13px !important;\n padding: 0 26px !important;\n }\n }\n @media only screen and (min-width: 870px) {\n .card-content {\n position: fixed;\n width: calc(${widthDesktop}${"%"!==widthDesktopDivided[2]||isSidebarHidden?"":" - var(--mdc-drawer-width)"}) !important;\n left: calc(50% - ${widthDesktopDivided[1]/2}${widthDesktopDivided[2]} + ${!0===isSidebarHidden?"0px":"var(--mdc-drawer-width) "+("%"===widthDesktopDivided[2]?"":"/ 2")});\n margin-left: -13px !important;\n padding: 0 26px !important;\n }\n }\n .horizontal-buttons-stack.editor {\n position: relative;\n bottom: 0;\n left: 0;\n overflow: hidden;\n }\n \n .horizontal-buttons-stack.editor::before {\n top: -32px;\n left: -100%;\n background: none;\n width: 100%;\n height: 0;\n }\n \n .horizontal-buttons-stack-container.editor > .button {\n transition: background-color 0s, border 0s, transform 0s;\n }\n \n .horizontal-buttons-stack-container.editor {\n margin-left: 1px;\n }\n \n .horizontal-buttons-stack.editor > .card-content {\n position: relative;\n width: calc(100% + 26px) !important;\n left: -26px;\n margin: 0 !important;\n padding: 0;\n }\n `;!window.hasAnimated&&riseAnimation&&(this.content.style.animation="from-bottom 1.3s forwards",window.hasAnimated=!0,setTimeout((()=>{this.content.style.animation="none"}),1500)),addStyles(this,g,customStyles),editor?(this.buttonsContainer.classList.add("editor"),this.card.classList.add("editor")):(this.buttonsContainer.classList.remove("editor"),this.card.classList.remove("editor"));break;case"button":if(!this.buttonAdded){const rt=document.createElement("div");rt.setAttribute("class","button-container"),this.content.appendChild(rt)}const f=this.config.button_type||"switch";formatedState=hass.formatEntityState(hass.states[entityId]);const m=!!this.config.show_state&&this.config.show_state;let b=entityId?hass.states[entityId].attributes.brightness||0:"",y=entityId?hass.states[entityId].attributes.volume_level||0:"",v=!1,_=b,w=y,x=0,k=0,C=0,$=!1,S=null;const E=stateChanged(entityId),I=document.createElement("div");I.setAttribute("class","icon-container"),this.iconContainer=I;const O=document.createElement("div");O.setAttribute("class","name-container");const A=document.createElement("div");A.setAttribute("class","switch-button");const L=document.createElement("div");L.setAttribute("class","range-slider");const T=document.createElement("div");if(T.setAttribute("class","range-fill"),!this.buttonContainer||editor){if(editor&&this.buttonContainer){for(;this.buttonContainer.firstChild;)this.buttonContainer.removeChild(this.buttonContainer.firstChild);this.eventAdded=!1,this.wasEditing=!0}this.buttonContainer=this.content.querySelector(".button-container"),"slider"!==f||this.buttonAdded&&!editor?("switch"===f||"custom"===f||editor)&&(this.buttonContainer.appendChild(A),A.appendChild(I),A.appendChild(O),this.switchButton=this.content.querySelector(".switch-button")):(this.buttonContainer.appendChild(L),L.appendChild(I),L.appendChild(O),L.appendChild(T),this.rangeFill=this.content.querySelector(".range-fill")),createIcon(this,hass,entityId,icon,this.iconContainer),O.innerHTML=`\n

${name}

\n ${m?`

${formatedState}

`:""}\n `,this.buttonAdded=!0}function U(t){let e=t.querySelector(".feedback-element");e||(e=document.createElement("div"),e.setAttribute("class","feedback-element"),t.appendChild(e)),e.style.animation="tap-feedback .5s",setTimeout((()=>{e.style.animation="none",t.removeChild(e)}),500)}function H(t){x=t.pageX||(t.touches?t.touches[0].pageX:0),k=t.pageY||(t.touches?t.touches[0].pageY:0),C=L.value,t.target!==I&&t.target!==I.querySelector("ha-icon")&&(v=!0,document.addEventListener("mouseup",V,{passive:!0}),document.addEventListener("touchend",V,{passive:!0}),document.addEventListener("mousemove",D,{passive:!0}),document.addEventListener("touchmove",D,{passive:!0}),S=setTimeout((()=>{Y(t.pageX||t.touches[0].pageX),z(),S=null}),200))}function D(t){const e=t.pageX||(t.touches?t.touches[0].pageX:0),n=t.pageY||(t.touches?t.touches[0].pageY:0);Math.abs(n-k)>Math.abs(e-x)?(clearTimeout(S),V()):(document.removeEventListener("mousemove",D),document.removeEventListener("touchmove",D),document.addEventListener("mousemove",B,{passive:!0}),document.addEventListener("touchmove",B,{passive:!0}))}function V(){v=!1,$=!1,z(),document.removeEventListener("mouseup",V),document.removeEventListener("touchend",V),document.removeEventListener("mousemove",B),document.removeEventListener("touchmove",B)}function z(){entityId.startsWith("light.")?(b=_,hass.callService("light","turn_on",{entity_id:entityId,brightness:b})):entityId.startsWith("media_player.")&&(y=w,hass.callService("media_player","volume_set",{entity_id:entityId,volume_level:y}))}function B(t){const e=t.pageX||(t.touches?t.touches[0].pageX:0),n=t.pageY||(t.touches?t.touches[0].pageY:0);v&&Math.abs(e-x)>10?Y(e):v&&Math.abs(n-k)>10&&(v=!1,L.value=C)}function Y(t){const e=L.getBoundingClientRect(),n=Math.min(Math.max(t-e.left,0),e.width)/e.width;entityId.startsWith("light.")?_=Math.round(255*n):entityId.startsWith("media_player.")&&(w=n),T.style.transition="none",T.style.transform=`translateX(${100*n}%)`}if(m&&formatedState&&(this.content.querySelector(".state").textContent=formatedState),this.eventAdded||"switch"!==f?this.eventAdded||"slider"!==f?this.eventAdded||"custom"!==f||(A.addEventListener("click",(()=>U(this.switchButton)),{passive:!0}),addActions(this,this.switchButton),this.eventAdded=!0):(L.addEventListener("mousedown",H,{passive:!0}),L.addEventListener("touchstart",H,{passive:!0}),addActions(this,this.iconContainer),this.eventAdded=!0):(A.addEventListener("click",(()=>U(this.switchButton)),{passive:!0}),A.addEventListener("click",(function(t){t.target!==I&&t.target!==I.querySelector("ha-icon")&&toggleEntity(entityId)}),{passive:!0}),addActions(this,this.iconContainer),this.eventAdded=!0),this.isDragging||"slider"!==f||(this.rangeFill.style.transition="all .3s",entityId.startsWith("light.")?this.rangeFill.style.transform=`translateX(${b/255*100}%)`:entityId.startsWith("media_player.")&&(this.rangeFill.style.transform=`translateX(${100*y}%)`)),"slider"===f&&(!this.colorAdded||E||this.wasEditing)){if(entityId.startsWith("light.")){const lt=hass.states[entityId].attributes.rgb_color;this.rgbColorOpacity=lt?isColorCloseToWhite(lt)?"rgba(255,220,200,0.5)":`rgba(${lt}, 0.5)`:stateOn?"rgba(255,220,200, 0.5)":"rgba(255, 255, 255, 0.5)",this.rgbColor=lt?isColorCloseToWhite(lt)?"rgb(255,220,200)":`rgb(${lt})`:stateOn?"rgba(255,220,200, 1)":"rgba(255, 255, 255, 1)",this.iconFilter=lt?isColorCloseToWhite(lt)?"none":"brightness(1.1)":"none"}else this.rgbColorOpacity="var(--accent-color)",this.iconFilter="brightness(1.1)";this.colorAdded=!0,this.wasEditing=!1}const M=`\n ha-card {\n margin-top: 0 !important;\n background: none !important;\n opacity: ${"unavailable"!==state?"1":"0.5"};\n }\n \n .button-container {\n position: relative;\n width: 100%;\n height: 50px;\n z-index: 0;\n background-color: var(--background-color-2,var(--secondary-background-color));\n border-radius: 25px;\n mask-image: radial-gradient(white, black);\n -webkit-mask-image: radial-gradient(white, black);\n -webkit-backface-visibility: hidden;\n -moz-backface-visibility: hidden;\n -webkit-transform: translateZ(0);\n overflow: hidden;\n }\n \n .switch-button,\n .range-slider {\n display: inline-flex;\n position: absolute;\n height: 100%;\n width: 100%;\n transition: background-color 1.5s;\n background-color: ${stateOn&&["switch","custom"].includes(f)?"var(--accent-color)":"rgba(0,0,0,0)"};\n }\n \n .range-fill {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n background-color: ${this.rgbColorOpacity};\n }\n \n .switch-button {\n cursor: pointer !important;\n }\n \n .range-slider {\n cursor: ew-resize;\n }\n \n .range-fill {\n z-index: 0;\n width: 100%;\n left: -100%;\n }\n \n .icon-container {\n position: absolute;\n display: flex;\n z-index: 1;\n width: 38px;\n height: 38px;\n margin: 6px;\n border-radius: 50%;\n cursor: pointer !important;\n background-color: var(--card-background-color,var(--ha-card-background));\n }\n \n .icon-container::after {\n content: '';\n position: absolute;\n display: block;\n opacity: ${entityId.startsWith("light.")?"0.2":"0"};\n width: 100%;\n height: 100%;\n transition: all 1s;\n border-radius: 50%;\n background-color: ${stateOn?this.rgbColor?this.rgbColor:"var(--accent-color)":"var(--card-background-color,var(--ha-card-background))"};\n }\n \n ha-icon {\n display: flex;\n position: absolute;\n margin: inherit;\n padding: 1px 2px;\n width: 22px; \n height: 22px;\n color: ${stateOn?this.rgbColor?this.rgbColor:"var(--accent-color)":"inherit"};\n opacity: ${stateOn?"1":"0.6"};\n filter: ${stateOn?this.rgbColor?this.iconFilter:"brightness(1.1)":"inherit"};\n }\n \n .entity-picture {\n display: flex;\n height: 38px;\n width: 38px;\n border-radius: 100%;\n }\n \n .name-container {\n position: relative;\n display: ${m?"block":"inline-flex"};\n margin-left: 58px;\n z-index: 1;\n font-weight: 600;\n align-items: center;\n line-height: ${m?"4px":"16px"};\n padding-right: 16px;\n }\n \n .state {\n font-size: 12px;\n opacity: 0.7;\n }\n \n .feedback-element {\n position: absolute;\n top: 0;\n left: 0;\n opacity: 0;\n width: 100%;\n height: 100%;\n background-color: rgb(0,0,0);\n }\n \n @keyframes tap-feedback {\n 0% {transform: translateX(-100%); opacity: 0;}\n 64% {transform: translateX(0); opacity: 0.1;}\n 100% {transform: translateX(100%); opacity: 0;}\n }\n `;addStyles(this,M,customStyles,state,entityId,E);break;case"separator":if(!this.separatorAdded||editor){if(editor&&this.separatorContainer)for(;this.separatorContainer.firstChild;)this.separatorContainer.removeChild(this.separatorContainer.firstChild);this.separatorAdded||(this.separatorContainer=document.createElement("div"),this.separatorContainer.setAttribute("class","separator-container")),this.separatorContainer.innerHTML=`\n
\n \n

${name}

\n
\n
\n `,this.content.appendChild(this.separatorContainer),this.separatorAdded=!0}const q="\n .separator-container {\n display: inline-flex;\n width: 100%;\n margin-top: 12px;\n }\n .separator-container div:first-child {\n display: inline-flex;\n max-width: calc(100% - 38px);\n }\n .separator-container div ha-icon{\n display: inline-flex;\n height: 24px;\n width: 24px;\n margin: 0 20px 0 8px;\n transform: translateY(-2px);\n }\n .separator-container div h4{\n display: inline-flex;\n margin: 0 20px 0 0;\n font-size: 17px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n .separator-container div:last-child{\n display: inline-flex; \n border-radius: 6px; \n opacity: 0.3; \n margin-left: 10px; \n flex-grow: 1; \n height: 6px; \n align-self: center; \n background-color: var(--background-color,var(--secondary-background-color));\n ";addStyles(this,q,customStyles);break;case"cover":const R=this.config.icon_open?this.config.icon_open:"mdi:window-shutter-open",W=this.config.icon_close?this.config.icon_close:"mdi:window-shutter",P=this.config.open_service?this.config.open_service:"cover.open_cover",F=this.config.close_service?this.config.close_service:"cover.close_cover",j=this.config.stop_service?this.config.stop_service:"cover.stop_cover",N=this.config.icon_up?this.config.icon_up:"mdi:arrow-up",X=this.config.icon_down?this.config.icon_down:"mdi:arrow-down";if(icon="open"===hass.states[this.config.entity].state?R:W,formatedState=this.config.entity?hass.formatEntityState(hass.states[this.config.entity]):"",!this.coverAdded||editor){if(editor&&this.coverContainer)for(;this.coverContainer.firstChild;)this.coverContainer.removeChild(this.coverContainer.firstChild);this.coverContainer=document.createElement("div"),this.coverContainer.setAttribute("class","cover-container"),this.coverContainer.innerHTML=`\n
\n
\n
\n
\n

${name}

\n

${formatedState}

\n
\n
\n
\n \n \n \n
\n `,this.content.appendChild(this.coverContainer);const ct=this.coverContainer.querySelector(".open"),dt=this.coverContainer.querySelector(".stop"),ht=this.coverContainer.querySelector(".close");ct.addEventListener("click",(()=>{hass.callService(P.split(".")[0],P.split(".")[1],{entity_id:entityId})}),{passive:!0}),dt.addEventListener("click",(()=>{hass.callService(j.split(".")[0],j.split(".")[1],{entity_id:entityId})}),{passive:!0}),ht.addEventListener("click",(()=>{hass.callService(F.split(".")[0],F.split(".")[1],{entity_id:entityId})}),{passive:!0}),this.iconContainer=this.content.querySelector(".icon-container"),addActions(this,this.iconContainer),this.coverAdded=!0}this.iconContainer&&(this.iconContainer.innerHTML=``,this.content.querySelector(".state").textContent=formatedState);const G="\n ha-card {\n margin-top: 0 !important;\n background: none !important;\n }\n \n .header-container {\n display: flex;\n align-items: center;\n margin-bottom: 10px;\n }\n \n .cover-container {\n display: grid;\n }\n \n .icon-container {\n display: flex;\n margin: 0 !important;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n /*z-index: 1;*/\n width: 48px;\n height: 48px;\n margin: 6px;\n border-radius: 50%;\n background-color: var(--card-background-color,var(--ha-card-background));\n border: 6px solid var(--background-color-2,var(--secondary-background-color));\n box-sizing: border-box;\n }\n \n .name-container {\n font-weight: 600;\n margin-left: 10px;\n line-height: 4px;\n }\n \n .buttons-container {\n display: grid;\n align-self: center;\n grid-auto-flow: column;\n grid-gap: 18px; \n }\n \n .state {\n font-size: 12px;\n opacity: 0.7;\n }\n \n ha-icon {\n display: flex; \n height: 24px; \n width: 24px; \n color: var(--primary-text-color);\n }\n \n .button {\n display: flex;\n background: var(--background-color-2,var(--secondary-background-color));\n height: 42px;\n border-radius: 32px;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n border: none;\n }\n ";addStyles(this,G,customStyles,state,entityId);break;case"empty-column":if(!this.emptyCollumnAdded){const pt=document.createElement("div");pt.setAttribute("class","empty-column"),pt.innerHTML='\n
\n ',this.content.appendChild(pt),this.emptyColumnAdded=!0}}}setConfig(t){if("pop-up"===t.card_type){if(!t.hash)throw new Error("You need to define an hash. Please note that this card must be placed inside a vertical_stack to work as a pop-up.")}else if("horizontal-buttons-stack"===t.card_type){var e={};for(var n in t)if(n.match(/^\d+_icon$/)){var i=n.replace("_icon","_link");if(void 0===t[i])throw new Error("You need to define "+i);if(e[t[i]])throw new Error("You can't use "+t[i]+" twice");e[t[i]]=!0}}else if(("button"===t.card_type||"cover"===t.card_type)&&!t.entity)throw new Error("You need to define an entity");this.config=t}getCardSize(){return 0}static getConfigElement(){return document.createElement("bubble-card-editor")}}console.info(`%c Bubble Card %c ${version} `,"background-color: #555;color: #fff;padding: 3px 2px 3px 3px;border-radius: 14px 0 0 14px;font-family: DejaVu Sans,Verdana,Geneva,sans-serif;text-shadow: 0 1px 0 rgba(1, 1, 1, 0.3)","background-color: #506eac;color: #fff;padding: 3px 3px 3px 2px;border-radius: 0 14px 14px 0;font-family: DejaVu Sans,Verdana,Geneva,sans-serif;text-shadow: 0 1px 0 rgba(1, 1, 1, 0.3)"),customElements.define("bubble-card",BubbleCard);const fireEvent=(t,e,n,i)=>{i=i||{},n=null==n?{}:n;const o=new Event(e,{bubbles:void 0===i.bubbles||i.bubbles,cancelable:Boolean(i.cancelable),composed:void 0===i.composed||i.composed});return o.detail=n,t.dispatchEvent(o),o};customElements.get("ha-switch");const LitElement=Object.getPrototypeOf(customElements.get("ha-panel-lovelace")),html=LitElement.prototype.html,css=LitElement.prototype.css;class BubbleCardEditor extends LitElement{setConfig(t){this._config={...t}}static get properties(){return{hass:{},_config:{}}}get _card_type(){return this._config.card_type||""}get _button_type(){return this._config.button_type||"switch"}get _entity(){return this._config.entity||""}get _name(){return this._config.name||""}get _icon(){return this._config.icon||""}get _state(){return this._config.state||""}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 _trigger_close(){return this._config.trigger_close||!1}get _margin(){return this._config.margin||"7px"}get _margin_top_mobile(){return this._config.margin_top_mobile||"0px"}get _margin_top_desktop(){return this._config.margin_top_desktop||"0px"}get _width_desktop(){return this._config.width_desktop||"540px"}get _bg_color(){return this._config.bg_color||window.color}get _bg_opacity(){return void 0!==this._config.bg_opacity?this._config.bg_opacity:"88"}get _bg_blur(){return void 0!==this._config.bg_blur?this._config.bg_blur:"14"}get _shadow_opacity(){return void 0!==this._config.shadow_opacity?this._config.shadow_opacity:"0"}get _is_sidebar_hidden(){return this._config.is_sidebar_hidden||!1}get _rise_animation(){return void 0===this._config.rise_animation||this._config.rise_animation}get _auto_close(){return this._config.auto_close||""}get _back_open(){return this._config.back_open||!1}get _icon_open(){return this._config.icon_open||""}get _icon_close(){return this._config.icon_close||""}get _open_service(){return this._config.open_service||"cover.open_cover"}get _close_service(){return this._config.open_service||"cover.close_cover"}get _stop_service(){return this._config.open_service||"cover.stop_cover"}get _auto_order(){return this._config.auto_order||!1}get _show_state(){return this._config.show_state||!1}render(){if(!this.hass)return html``;if(!this.listsUpdated){const t=t=>({label:t,value:t});this.allEntitiesList=Object.keys(this.hass.states).map(t),this.lightList=Object.keys(this.hass.states).filter((t=>"light"===t.substr(0,t.indexOf(".")))).map(t),this.sensorList=Object.keys(this.hass.states).filter((t=>"sensor"===t.substr(0,t.indexOf(".")))).map(t),this.binarySensorList=Object.keys(this.hass.states).filter((t=>"binary_sensor"===t.substr(0,t.indexOf(".")))).map(t),this.coverList=Object.keys(this.hass.states).filter((t=>"cover"===t.substr(0,t.indexOf(".")))).map(t),this.cardTypeList=[{label:"Button",value:"button"},{label:"Cover",value:"cover"},{label:"Empty column",value:"empty-column"},{label:"Horizontal buttons stack",value:"horizontal-buttons-stack"},{label:"Pop-up",value:"pop-up"},{label:"Separator",value:"separator"}],this.buttonTypeList=[{label:"Switch",value:"switch"},{label:"Slider",value:"slider"}],this.listsUpdated=!0}const t=this.allEntitiesList,e=(this.lightList,this.sensorList,this.coverList),n=this.cardTypeList,i=this.buttonTypeList;if("pop-up"===this._config.card_type)return html`
${this.makeDropdown("Card type","card_type",n)}

Pop-up Regular mode

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.

How to get the optimized mode?
${this.makeDropdown("Optional - Icon","icon")} ${this.makeDropdown("Optional - Entity to toggle (e.g. room light group)","entity",t)} ${this.makeDropdown("Optional - Entity state to display (e.g. room temperature)","state",t)}

Pop-up trigger

This allows you to open 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",t)}

Styling options

You can't set a value to 0 with the sliders for now, just change it to 0 in the text field if you need to.

Advanced settings

Back button/event support : This allow you to navigate through your pop-ups history when you press the back button of your browser. This setting can be applied only once, you don't need to change it in all pop-ups. If it's not working just turn it on for each pop-ups. ${this.makeVersion()}
`;if("button"===this._config.card_type)return html`
${this.makeDropdown("Card type","card_type",n)}

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("slider"!==this._button_type?"Entity (toggle)":"Entity (light or media_player)","entity",t)} ${this.makeDropdown("Button type","button_type",i)} ${this.makeDropdown("Optional - Icon","icon")}
${this.makeVersion()}
`;if("separator"===this._config.card_type)return html`
${this.makeDropdown("Card type","card_type",n)}

Separator

This card is a simple separator for dividing your pop-up into categories / sections. e.g. Lights, Devices, Covers, Settings, Automations... ${this.makeDropdown("Icon","icon")} ${this.makeVersion()}
`;if("horizontal-buttons-stack"===this._config.card_type){if(!this.buttonAdded&&this.shadowRoot.querySelector("#add-button")){const t=this.shadowRoot.querySelector("#add-button");for(this.buttonIndex=0;this._config[this.buttonIndex+1+"_link"];)this.buttonIndex++;t.addEventListener("click",(()=>{this.buttonIndex++;const e=t.style.opacity,n=t.innerText;t.style.opacity="0.6",t.style.transition="opacity 1s",t.innerText="Loading...",setTimeout((()=>{t.style.opacity=e,t.innerText=n}),5e3)}),{passive:!0}),this.buttonAdded=!0}return html`
${this.makeDropdown("Card type","card_type",n)}

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()}
`}return"cover"===this._config.card_type?html`
${this.makeDropdown("Card type","card_type",n)}

Cover

This card allows you to control your covers. ${this.makeDropdown("Entity","entity",e)} ${this.makeDropdown("Optional - Open icon","icon_open")} ${this.makeDropdown("Optional - Closed icon","icon_close")}

Styling options

${this.makeDropdown("Optional - Arrow down icon","icon_down")} ${this.makeDropdown("Optional - Arrow up icon","icon_up")} ${this.makeVersion()}
`:"empty-column"===this._config.card_type?html`
${this.makeDropdown("Card type","card_type",n)}

Empty column

Just an empty card to fill any empty column. ${this.makeVersion()}
`:this._config.card_type?void 0:html`
${this.makeDropdown("Card type","card_type",n)} You need to add a card type first.

Almost everything is available in the GUI editor, but in the YAML editor you can add your own custom styles, create custom buttons or modify the tap actions of all cards. You can find more details on my GitHub page.

And if you like my project and want to support me, please consider making a donation. Any amount is welcome and very much appreciated! 🍻

${this.makeVersion()}
`}makeDropdown(t,e,n){this.hass;return t.includes("icon")||t.includes("Icon")?html`
`:html`
`}makeButton(){let t=[];for(let e=1;e<=this.buttonIndex;e++)t.push(html`
this.removeButton(e)}> Button ${e}
`);return t}makeVersion(){return html`

Bubble Card ${version}

`}removeButton(t){delete this._config[t+"_name"],delete this._config[t+"_icon"],delete this._config[t+"_link"],delete this._config[t+"_entity"],delete this._config[t+"_pir_sensor"];for(let e=t;e{"use strict";var __webpack_modules__={946:(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{__webpack_require__.d(__webpack_exports__,{G:()=>getIconStyles,IU:()=>createIcon,L2:()=>addStyles,_k:()=>convertToRGBA,mk:()=>getIconColor,wW:()=>isColorCloseToWhite});const addStyles=function(hass,context,styles,customStyles,state,entityId,stateChanged,path="",element=context.content){const customStylesEval=customStyles?eval("`"+customStyles+"`"):"";let styleAddedKey=styles+"Added";if(!context[styleAddedKey]||context.previousStyle!==customStylesEval||stateChanged||context.previousConfig!==context.config){if(!context[styleAddedKey]){if(context.styleElement=element.querySelector("style"),!context.styleElement){context.styleElement=document.createElement("style");const t=path?element.querySelector(path):element;t?.appendChild(context.styleElement)}context[styleAddedKey]=!0}context.styleElement.innerHTML!==customStylesEval+styles&&(context.styleElement.innerHTML=customStylesEval+styles),context.previousStyle=customStylesEval,context.previousConfig=context.config}};function createIcon(t,e,n,o,i,a){let r=!(!n||!e.states[n].attributes)&&e.states[n].attributes;t.imageUrl=!!r.entity_picture&&r.entity_picture,updateIcon(t,e,n,o,i),a||e.connection.subscribeEvents((a=>{a.data.entity_id===n&&a.data.old_state&&a.data.old_state.attributes.entity_picture!==a.data.new_state.attributes.entity_picture&&(t.imageUrl=a.data.new_state.attributes.entity_picture,updateIcon(t,e,n,o,i))}),"state_changed")}function updateIcon(t,e,n,o,i){for(;i.firstChild;)i.removeChild(i.firstChild);let a=t.config.icon&&t.config.icon.includes("/")?t.config.icon:t.imageUrl?t.imageUrl:"";if(a&&(r=e.states[n].state,n.startsWith("media_player.")&&!["off","unknown","idle",void 0].includes(r)||!n.startsWith("media_player."))){const t=document.createElement("div");t.setAttribute("class","entity-picture"),t.setAttribute("alt","Icon"),i&&(i.appendChild(t),i.style.background="center / cover no-repeat url("+a+"), var(--card-background-color,var(--ha-card-background))")}else{const t=document.createElement("ha-icon");t.setAttribute("icon",o),t.setAttribute("class","icon"),i&&i.appendChild(t)}var r}function isColorCloseToWhite(t){let e=[220,220,190];for(let n=0;n<3;n++)if(t[n]{for(var n in e)__webpack_require__.o(e,n)&&!__webpack_require__.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},__webpack_require__.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var __webpack_exports__={};(()=>{var t="v1.6.0-beta.1",e=__webpack_require__(946);const n=(t,e,n,o)=>{o=o||{},n=null==n?{}:n;const i=new Event(e,{bubbles:void 0===o.bubbles||o.bubbles,cancelable:Boolean(o.cancelable),composed:void 0===o.composed||o.composed});return i.detail=n,t.dispatchEvent(i),i},o=t=>{n(window,"haptic",t)},i=(t,e,o=!1)=>{o?history.replaceState(null,"",e):history.pushState(null,"",e),n(window,"location-changed",{replace:o})};function a(t,e){t.callService("homeassistant","toggle",{entity_id:e})}function r(t,e,n){const o={entity:e.entity,tap_action:{action:"more-info"},double_tap_action:{action:"toggle"},hold_action:{action:"toggle"}},i=new Event("hass-action",{bubbles:!0,composed:!0});i.detail={config:o,action:n},t.dispatchEvent(i)}function s(t,e,n,o){e.tap_action,e.double_tap_action,e.hold_action;let i,a=0,s=0,l=0;t.addEventListener("mousedown",(()=>{s=Date.now(),i=setTimeout((()=>{r(t,e,"hold")}),300)})),t.addEventListener("mouseup",(()=>{clearTimeout(i),l=Date.now(),l-s<300&&(a++,1===a&&setTimeout((()=>{1===a?r(t,e,"tap"):(r(t,e,"double_tap"),o("success")),a=0}),300)),s=0,l=0})),t.addEventListener("touchstart",(n=>{o("light"),s=Date.now(),i=setTimeout((()=>{r(t,e,"hold")}),300),n.preventDefault()})),t.addEventListener("touchend",(n=>{clearTimeout(i),l=Date.now(),l-s<300&&(a++,1===a&&setTimeout((()=>{r(t,e,1===a?"tap":"double_tap"),a=0}),300)),s=0,l=0,n.preventDefault()})),t.addEventListener("mouseout",(()=>{clearTimeout(i)})),t.addEventListener("touchcancel",(()=>{clearTimeout(i)}))}let l,c,d,h;function p(t,n,o,i){let a=n.styles?n.styles:"",r=n.entity&&o.states[n.entity]?n.entity:"",s=!n.icon&&n.entity?o.states[r].attributes.icon||o.states[r].attributes.entity_picture||"":n.icon||"",p=n.name?n.name:n.entity?o.states[r].attributes.friendly_name:"",u=n.width_desktop||"540px",g=u?u.match(/(\d+)(\D+)/):"",m=n.is_sidebar_hidden||!1,b=r?o.states[r].state:"";!function(t,e,n){t.hasState=e.states[n],t.hasState&&(t.newState=[t.hasState.state,t.hasState.attributes.rgb_color],t.oldState&&t.newState[0]===t.oldState[0]&&t.newState[1]===t.oldState[1]?t.stateChanged=!1:(t.oldState=t.newState,t.stateChanged=!0),t.stateChanged)}(t,o,r);let f=t.stateChanged,_=["on","open","cleaning","true","home","playing"].includes(b)||0!==Number(b)&&!isNaN(Number(b)),v=void 0===n.rise_animation||n.rise_animation,y=n.margin?"0"!==n.margin?n.margin:"0px":"7px",w=void 0!==n.bg_opacity?n.bg_opacity:"88",x=void 0!==n.shadow_opacity?n.shadow_opacity:"0",k=void 0!==n.bg_blur?n.bg_blur:"10",{iconColorOpacity:C,iconColor:$,iconFilter:S}=(0,e.mk)(o,r,_,e.wW),E=(0,e.G)(r,_,$,S),O=getComputedStyle(document.body),L=O.getPropertyValue("--ha-card-background")||O.getPropertyValue("--card-background-color"),A=n.bg_color?n.bg_color:L;if(A&&(!t.color||A!==t.color)){const n=1.02;c=(0,e._k)(A,w/100,n),t.color=A,window.color=A}return{customStyles:a,entityId:r,icon:s,name:p,widthDesktop:u,widthDesktopDivided:g,isSidebarHidden:m,state:b,stateChanged:f,stateOn:_,formatedState:h,riseAnimation:v,marginCenter:y,popUpOpen:l,rgbaColor:c,rgbColor:d,bgOpacity:w,shadowOpacity:x,bgBlur:k,iconColorOpacity:C,iconColor:$,iconFilter:S,iconStyles:E,haStyle:O,themeBgColor:L,color:A}}const u=Object.getPrototypeOf(customElements.get("ha-panel-lovelace")),g=u.prototype.html,m=u.prototype.css;let b;!function(){if(!window.eventAdded){const t=new Event("urlChanged");function e(){let e=0;window.dispatchEvent(t);const n=setInterval((()=>{e<10?(window.dispatchEvent(t),e++):clearInterval(n)}),1e3)}window.popUpInitialized=!1,["click","mousedown","touchstart","focus","location-changed","connection-status"].forEach((t=>{window.addEventListener(t,e)}),{passive:!0});const n=()=>{window.dispatchEvent(t),window.addEventListener("popstate",e,{passive:!0})};window.addEventListener("popUpInitialized",n,{passive:!0}),window.eventAdded=!0}}();class f extends HTMLElement{set hass(t){var n;switch(this._hass=t,this.editor=b,(n=this).content||(n.attachShadow({mode:"open"}),n.shadowRoot.innerHTML='\n \n
\n
\n
\n ',n.card=n.shadowRoot.querySelector("ha-card"),n.content=n.shadowRoot.querySelector("div")),async function(t){if(window.editorElement)t=window.editorElement.classList.contains("edit-mode");else{const t=new Promise((t=>{t(document.querySelector("body > home-assistant").shadowRoot.querySelector("home-assistant-main").shadowRoot.querySelector("ha-drawer > partial-panel-resolver > ha-panel-lovelace").shadowRoot.querySelector("hui-root").shadowRoot.querySelector("div"))}));window.editorElement=await t}return t}(b).then((t=>{b=t})),this.config.card_type){case"pop-up":!function(t){const n=t._hass,r=t.editor,l=t.config;if(!n)return;let c,d,{customStyles:h,entityId:u,icon:g,name:m,widthDesktop:b,widthDesktopDivided:f,isSidebarHidden:_,state:v,stateChanged:y,stateOn:w,formatedState:x,riseAnimation:k,marginCenter:C,popUpOpen:$,rgbaColor:S,rgbColor:E,bgOpacity:O,shadowOpacity:L,bgBlur:A,iconColorOpacity:I,iconColor:T,iconFilter:V,iconStyles:D,haStyle:z,themeBgColor:M,color:B}=p(t,l,n),q=l.auto_close||!1,U=l.hash,H=l.trigger_entity?l.trigger_entity:"",Y=l.trigger_state?l.trigger_state:"",F=!!l.trigger_close&&l.trigger_close;if(t.errorTriggered)return;t.initStyleAdded||t.host||r||(t.card.style.marginTop="4000px",t.initStyleAdded=!0);const R=()=>{if(t.host){if(!t.popUp&&(t.verticalStack=t.getRootNode(),t.popUp=t.verticalStack.querySelector("#root"),!window.popUpInitialized&&t.popUp)){if(l.back_open?localStorage.setItem("backOpen",!0):localStorage.setItem("backOpen",!1),"true"===localStorage.getItem("backOpen")){window.backOpen=!0;const G=new Event("popUpInitialized");setTimeout((()=>{window.dispatchEvent(G)}),0)}else window.backOpen=!1,$=U+!1,history.replaceState(null,null,location.href.split("#")[0]);window.popUpInitialized=!0}const i=t.popUp,p=(t.verticalStack,l.text||""),k=l.state;x=k?n.formatEntityState(n.states[k]):x||"";const E=l.margin_top_mobile&&"0"!==l.margin_top_mobile?l.margin_top_mobile:"0px",O=l.margin_top_desktop&&"0"!==l.margin_top_desktop?l.margin_top_desktop:"0px",I=l.entity?"flex":"none";let T,V;if(v=k?n.states[k].state:"",t.headerAdded){if(u){const K=t.content.querySelector("#header-container .icon-container"),Z=t.content.querySelector("#header-container h2"),J=t.content.querySelector("#header-container p"),Q=t.content.querySelector("#header-container .power-button");K.innerHTML="",(0,e.IU)(t,n,u,g,K,r),Z.textContent=m,J.textContent=x,Q.setAttribute("style",`display: ${I};`)}}else{const tt=document.createElement("div");tt.setAttribute("id","header-container");const et=document.createElement("div");tt.appendChild(et);const nt=document.createElement("div");nt.setAttribute("class","icon-container"),et.appendChild(nt),(0,e.IU)(t,n,u,g,nt,r),s(nt,l,0,o);const ot=document.createElement("h2");ot.textContent=m,et.appendChild(ot);const it=document.createElement("p");it.textContent=x,et.appendChild(it);const at=document.createElement("ha-icon");at.setAttribute("class","power-button"),at.setAttribute("icon","mdi:power"),at.setAttribute("style",`display: ${I};`),et.appendChild(at);const rt=document.createElement("button");rt.setAttribute("class","close-pop-up"),rt.onclick=function(){history.replaceState(null,null,location.href.split("#")[0]),localStorage.setItem("isManuallyClosed_"+U,!0)},tt.appendChild(rt);const st=document.createElement("ha-icon");st.setAttribute("icon","mdi:close"),rt.appendChild(st),t.content.appendChild(tt),t.header=et,t.headerAdded=!0}function z(){a(n,u)}function M(t){"Escape"===t.key&&($=U+!1,history.replaceState(null,null,location.href.split("#")[0]),localStorage.setItem("isManuallyClosed_"+U,!0))}function H(t){window.hash===U&&j(),c=t.touches[0].clientY,d=c}function Y(t){t.touches[0].clientY-c>300&&t.touches[0].clientY>d&&($=U+!1,history.replaceState(null,null,location.href.split("#")[0]),$=U+!1,localStorage.setItem("isManuallyClosed_"+U,!0)),d=t.touches[0].clientY}if(t.eventAdded||r||(window["checkHashRef_"+U]=F,window.addEventListener("urlChanged",window["checkHashRef_"+U],{passive:!0}),window.addEventListener("click",(function(t){if(location.hash===U&&j(),!window.justOpened)return;const e=t.composedPath();!e||e.some((t=>"HA-MORE-INFO-DIALOG"===t.nodeName))||e.some((t=>"root"===t.id&&!t.classList.contains("close-pop-up")))||$!==U+!0||setTimeout((function(){location.hash===U&&($=U+!1,history.replaceState(null,null,location.href.split("#")[0]),localStorage.setItem("isManuallyClosed_"+U,!0))}),2)}),{passive:!0}),t.eventAdded=!0),u){const lt=n.states[u].attributes.rgb_color;t.rgbColor=lt?(0,e.wW)(lt)?"rgb(255,220,200)":`rgb(${lt})`:w?u.startsWith("light.")?"rgba(255,220,200, 0.5)":"var(--accent-color)":"rgba(255, 255, 255, 1",t.rgbColorOpacity=lt?(0,e.wW)(lt)?"rgba(255,220,200, 0.5)":`rgba(${lt}, 0.5)`:u&&w?u.startsWith("light.")?"rgba(255,220,200, 0.5)":"var(--accent-color)":"var(--background-color,var(--secondary-background-color))",V=(0,e._k)(B,0),t.iconFilter=lt?(0,e.wW)(lt)?"none":"brightness(1.1)":"none"}else V=(0,e._k)(B,0);function F(){r||(window.hash=location.hash.split("?")[0],window.hash===U?(setTimeout((function(){i.classList.remove("close-pop-up"),i.classList.add("open-pop-up"),R.querySelector(".power-button").addEventListener("click",z,{passive:!0}),window.addEventListener("keydown",M,{passive:!0}),i.addEventListener("touchstart",H,{passive:!0}),i.addEventListener("touchmove",Y,{passive:!0}),$=U+!0,document.body.style.overflow="hidden",setTimeout((()=>{window.justOpened=!0}),10),j()}),0),setTimeout((function(){W(i,!1)}),0)):i.classList.contains("open-pop-up")&&(setTimeout((function(){i.classList.remove("open-pop-up"),i.classList.add("close-pop-up"),R.querySelector(".power-button").removeEventListener("click",z),window.removeEventListener("keydown",M),i.removeEventListener("touchstart",H),i.removeEventListener("touchmove",Y),$=U+!1,document.body.style.overflow="",window.justOpened=!1,clearTimeout(T)}),0),setTimeout((function(){W(i,!0)}),320)))}let R=t.content;function W(t,e){for(var n=t.querySelectorAll("video"),o=0;o0&&!n[o].paused&&!n[o].ended&&n[o].readyState>n[o].HAVE_CURRENT_DATA;e&&i?n[o].pause():e||i||(n[o].play(),n[o].currentTime>0&&(n[o].currentTime=1e4))}var a=t.querySelectorAll("*");for(o=0;o0&&(T=setTimeout(P,q))}function P(){history.replaceState(null,null,location.href.split("#")[0])}r&&!t.editorModeAdded&&(console.log(U),i.classList.add("editor"),i.classList.remove("open-pop-up"),i.classList.remove("close-pop-up"),t.editorModeAdded=!0);const X=` \n\t ha-card {\n\t margin-top: 0 !important;\n\t background: none !important;\n\t border: none !important;\n\t }\n\t .card-content {\n\t width: 100% !important;\n\t padding: 0 !important;\n\t }\n\t #root {\n\t transition: all 1s !important;\n\t position: fixed !important;\n\t margin: 0 -${C}; /* 7px */\n\t width: 100%;\n\t background-color: ${S};\n\t box-shadow: 0px 0px 50px rgba(0,0,0,${L/100});\n\t backdrop-filter: blur(${A}px);\n\t -webkit-backdrop-filter: blur(${A}px);\n\t border-radius: 42px;\n\t box-sizing: border-box;\n\t top: calc(120% + ${E} + var(--header-height));\n\t grid-gap: 12px !important;\n\t gap: 12px !important;\n\t grid-auto-rows: min-content;\n\t padding: 18px 18px 220px 18px !important;\n\t height: 100% !important;\n\t -ms-overflow-style: none; /* for Internet Explorer, Edge */\n\t scrollbar-width: none; /* for Firefox */\n\t overflow-y: auto; \n\t overflow-x: hidden; \n\t z-index: 1 !important; /* Higher value hide the more-info panel */\n\t /* For older Safari but not working with Firefox */\n\t /* display: grid !important; */ \n\t }\n\t #root.hidden {\n\t \tdisplay: none !important;\n\t }\n\t #root > :first-child::after {\n\t content: '';\n\t display: block;\n\t position: sticky;\n\t top: 0;\n\t left: -50px;\n\t margin: -70px 0 -36px -36px;\n\t overflow: visible;\n\t width: 200%;\n\t height: 100px;\n\t background: linear-gradient(0deg, ${V} 0%, ${S} 80%);\n\t z-index: 0;\n\t } \n\t #root::-webkit-scrollbar {\n\t display: none; /* for Chrome, Safari, and Opera */\n\t }\n\t #root > :first-child {\n\t position: sticky;\n\t top: 0;\n\t z-index: 1;\n\t background: none !important;\n\t overflow: visible;\n\t }\n\t #root.open-pop-up {\n\t /*will-change: transform;*/\n\t transform: translateY(-120%);\n\t transition: transform .36s !important;\n\t }\n\t #root.open-pop-up > * {\n\t /* Block child items to overflow and if they do clip them */\n\t /*max-width: calc(100vw - 38px);*/\n\t max-width: 100% !important;\n\t /*overflow-x: clip;*/\n\t }\n\t #root.close-pop-up { \n\t transform: translateY(-20%);\n\t transition: transform .4s !important;\n\t box-shadow: none;\n\t }\n\t @media only screen and (min-width: 600px) {\n\t #root {\n\t top: calc(120% + ${O} + var(--header-height));\n\t width: calc(${b}${"%"!==f[2]||_?"":" - var(--mdc-drawer-width)"}) !important;\n\t left: calc(50% - ${f[1]/2}${f[2]});\n\t margin: 0 !important;\n\t }\n\t } \n\t @media only screen and (min-width: 870px) {\n\t #root {\n\t left: calc(50% - ${f[1]/2}${f[2]} + ${_?"0px":"var(--mdc-drawer-width) "+("%"===f[2]?"":"/ 2")});\n\t }\n\t } \n\t #root.editor {\n\t position: inherit !important;\n\t width: 100% !important;\n\t padding: 18px !important;\n\t }\n\t `,N=`\n\t ${D}\n\n\t ha-card {\n\t margin-top: 0 !important;\n\t }\n\t #header-container {\n\t display: inline-flex;\n\t ${g||m||u||v||p?"":"flex-direction: row-reverse;"}\n\t height: 50px;\n\t width: 100%;\n\t margin: 0;\n\t padding: 0;\n\t }\n\t #header-container > div {\n\t display: ${g||m||u||v||p?"inline-flex":"none"};\n\t align-items: center;\n\t position: relative;\n\t padding-right: 6px;\n\t z-index: 1;\n\t flex-grow: 1;\n\t background-color: ${u?t.rgbColorOpacity:"var(--background-color,var(--secondary-background-color))"};\n\t transition: background 1s;\n\t border-radius: 25px;\n\t margin-right: 14px;\n\t backdrop-filter: blur(14px);\n\t -webkit-backdrop-filter: blur(14px);\n\t }\n\t #header-container h2 {\n\t display: inline-flex;\n\t margin: 0 18px 0 0;\n\t padding: 4px;\n\t z-index: 1;\n\t font-size: 18px;\n\t }\n\t #header-container p {\n\t display: inline-flex;\n\t font-size: 16px;\n\t min-width: fit-content ;\n\t }\n\t .power-button {\n\t cursor: pointer; \n\t flex-grow: inherit; \n\t width: 24px;\n\t height: 24px;\n\t border-radius: 12px;\n\t margin: 0 10px;\n\t background: none !important;\n\t justify-content: flex-end;\n\t background-color: var(--background-color,var(--secondary-background-color));\n\t }\n\t .close-pop-up {\n\t height: 50px;\n\t width: 50px;\n\t border: none;\n\t border-radius: 50%;\n\t z-index: 1;\n\t background: var(--background-color,var(--secondary-background-color));\n\t color: var(--primary-text-color);\n\t flex-shrink: 0;\n\t cursor: pointer;\n\t }\n\t `;setTimeout((()=>{(0,e.L2)(n,t,N,h,v,u,y),(0,e.L2)(n,t,X,h,v,u,t.bgColorChanged,"",i)}),0)}else t.host=t.getRootNode().host};if(t.popUp&&S&&S!==t.oldBgColor&&location.hash===U?(t.oldBgColor=S,t.bgColorChanged=!0):t.bgColorChanged=!1,t.popUpAdded)!r&&t.wasEditing&&(y||t.bgColorChanged)?(R(),t.wasEditing=!1):(U===window.hash&&(y||t.bgColorChanged)||r&&!t.editorModeAdded)&&(R(),r&&(t.wasEditing=!0));else{t.popUpAdded=!0;let e=setInterval((()=>{R(),t.popUp&&clearInterval(e)}),0);setTimeout((()=>{if(!t.popUp)throw t.errorTriggered=!0,clearInterval(e),new Error("Pop-up card must be placed inside a vertical_stack! If it's already the case, please ignore this error 🍻")}),6e3)}if(!r&&t.popUp&&t.editorModeAdded&&(t.popUp.classList.remove("editor"),t.editorModeAdded=!1),t.popUp&&H&&y){null===localStorage.getItem("previousTriggerState_"+U)&&localStorage.setItem("previousTriggerState_"+U,""),null===localStorage.getItem("isManuallyClosed_"+U)&&localStorage.setItem("isManuallyClosed_"+U,"false"),null===localStorage.getItem("isTriggered_"+U)&&localStorage.setItem("isTriggered_"+U,"false");let e=localStorage.getItem("previousTriggerState_"+U),o="true"===localStorage.getItem("isManuallyClosed_"+U),a="true"===localStorage.getItem("isTriggered_"+U);n.states[H].state!==Y||null!==e||a||(i(0,U),a=!0,localStorage.setItem("isTriggered_"+U,a)),n.states[H].state!==e&&(o=!1,localStorage.setItem("previousTriggerState_"+U,n.states[H].state),localStorage.setItem("isManuallyClosed_"+U,o)),n.states[H].state!==Y||o?n.states[H].state!==Y&&F&&t.popUp.classList.contains("open-pop-up")&&a&&!o&&(history.replaceState(null,null,location.href.split("#")[0]),$=U+!1,a=!1,o=!0,localStorage.setItem("isManuallyClosed_"+U,o),localStorage.setItem("isTriggered_"+U,a)):(i(0,U),a=!0,localStorage.setItem("isTriggered_"+U,a))}}(this);break;case"horizontal-buttons-stack":!function(t){const n=t._hass,a=t.editor;let{customStyles:r,entityId:s,icon:l,name:c,widthDesktop:d,widthDesktopDivided:h,isSidebarHidden:u,state:g,stateChanged:m,stateOn:b,riseAnimation:f,marginCenter:_,popUpOpen:v,rgbaColor:y,rgbColor:w,bgOpacity:x,shadowOpacity:k,bgBlur:C,iconColorOpacity:$,iconColor:S,iconFilter:E,iconStyles:O,haStyle:L,themeBgColor:A,color:I}=p(t,t.config,n);if(!t.buttonsAdded){const e=document.createElement("div");e.classList.add("horizontal-buttons-stack-container"),t.content.appendChild(e),t.buttonsContainer=e}const T=(t,o,i)=>{if(n.states[o].attributes.rgb_color){const i=n.states[o].attributes.rgb_color,a=(0,e.wW)(i)?"rgba(255,220,200, 0.5)":`rgba(${i}, 0.5)`;t.style.backgroundColor=a,t.style.border="1px solid rgba(0,0,0,0)"}else n.states[o].attributes.rgb_color||"on"!=n.states[o].state?(t.style.backgroundColor="rgba(0,0,0,0)",t.style.border="1px solid var(--primary-text-color)"):(t.style.backgroundColor="rgba(255,255,255,0.5)",t.style.border="1px solid rgba(0,0,0,0)")};let V=[],D=1;for(;t.config[D+"_link"];){const e=D+"_",n=t.config[e+"name"]||"",o=t.config[e+"pir_sensor"];l=t.config[e+"icon"]||"";const i=t.config[e+"link"],a=t.config[e+"entity"];V.push({button:n,pirSensor:o,icon:l,link:i,lightEntity:a}),D++}if(t.config.auto_order&&V.sort(((t,e)=>t.pirSensor&&e.pirSensor?"on"===n.states[t.pirSensor].state&&"on"===n.states[e.pirSensor].state?n.states[t.pirSensor].last_updated{const a=((e,n,a)=>{const r=document.createElement("button");return r.setAttribute("class",`button ${n.substring(1)}`),r.innerHTML=`\n ${""!==a?``:""}\n ${""!==e?`

${e}

`:""}\n `,r.hasListener||(r.addEventListener("click",(t=>{t.stopPropagation(),o("light"),v=location.hash+!0,localStorage.getItem("isManuallyClosed_"+n),v!==n+!0?(i(0,n),v=n+!0):(history.replaceState(null,null,location.href.split("#")[0]),v=n+!1)}),{passive:!0}),window.addEventListener("urlChanged",(function(){t.config.highlightCurrentview&&(location.pathname===n||location.hash===n?r.classList.add("highlight"):r.classList.remove("highlight"))}),{passive:!0}),r.hasListener=!0),r})(n.button,n.link,n.icon);e[n.link]=a,t.buttonsContainer.appendChild(a)})),t.buttonsAdded=!0,t.buttons=e}let z=0;!async function(t){if(t.buttonsUpdated)return;let e=[];for(let n of V)t.buttons[n.link]&&(e.push(localStorage.getItem(`buttonWidth-${n.link}`)),e.push(localStorage.getItem(`buttonContent-${n.link}`)));let n=await Promise.all(e),o=0;for(let e of V){let i=t.buttons[e.link];if(i){let r=n[o],s=n[o+1];o+=2,r&&"0"!==r&&s===i.innerHTML&&!a||(r=i.offsetWidth,await localStorage.setItem(`buttonWidth-${e.link}`,r),await localStorage.setItem(`buttonContent-${e.link}`,i.innerHTML),t.previousConfig=t.config),i.style.transform=`translateX(${z}px)`,z+=parseInt(r)+12}e.lightEntity&&T(i,e.lightEntity,e.link)}t.buttonsAdded=!0}(t);const M=`\n ha-card {\n border-radius: 0;\n }\n .horizontal-buttons-stack {\n width: 100%;\n margin-top: 0 !important;\n background: none !important;\n position: fixed;\n height: 51px;\n bottom: 16px;\n left: ${_};\n z-index: 1 !important; /* Higher value hide the more-info panel */\n }\n @keyframes from-bottom {\n 0% {transform: translateY(200px);}\n 20% {transform: translateY(200px);}\n 46% {transform: translateY(-8px);}\n 56% {transform: translateY(1px);}\n 62% {transform: translateY(-2px);}\n 70% {transform: translateY(0);}\n 100% {transform: translateY(0);}\n }\n .horizontal-buttons-stack-container {\n width: max-content;\n position: relative;\n height: 51px;\n }\n .button {\n display: inline-flex;\n position: absolute;\n box-sizing: border-box !important;\n border: 1px solid var(--primary-text-color);\n align-items: center;\n height: 50px;\n line-height: 16px;\n white-space: nowrap;\n width: auto;\n border-radius: 25px;\n z-index: 1;\n padding: 0 16px;\n background: none;\n transition: background-color 1s, border 1s, transform 1s;\n color: var(--primary-text-color);\n }\n .highlight {\n animation: pulse 1.4s infinite alternate;\n }\n @keyframes pulse {\n 0% {\n filter: brightness(0.7);\n }\n 100% {\n filter: brightness(1.3);\n }\n }\n .icon {\n height: 24px;\n }\n .card-content {\n width: calc(100% + 18px);\n box-sizing: border-box !important;\n margin: 0 -36px !important;\n padding: 0 36px !important;\n overflow: scroll !important;\n -ms-overflow-style: none;\n scrollbar-width: none;\n -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%);\n /* mask-image: linear-gradient(90deg, transparent 2%, rgba(0, 0, 0, 1) 6%, rgba(0, 0, 0, 1) 96%, transparent 100%); */\n /* -webkit-mask-image: linear-gradient(90deg, transparent 2%, rgba(0, 0, 0, 1) 6%, rgba(0, 0, 0, 1) 96%, transparent 100%); */\n }\n .horizontal-buttons-stack::before {\n content: '';\n position: absolute;\n top: -32px;\n left: -100%;\n display: block;\n background: linear-gradient(0deg, var(--background-color, var(--primary-background-color)) 50%, rgba(79, 69, 87, 0));\n width: 200%;\n height: 100px;\n }\n .card-content::-webkit-scrollbar {\n display: none;\n }\n @media only screen and (min-width: 600px) {\n .card-content {\n position: fixed;\n width: ${d} !important;\n left: calc(50% - ${h[1]/2}${h[2]});\n margin-left: -13px !important;\n padding: 0 26px !important;\n }\n }\n @media only screen and (min-width: 870px) {\n .card-content {\n position: fixed;\n width: calc(${d}${"%"!==h[2]||u?"":" - var(--mdc-drawer-width)"}) !important;\n left: calc(50% - ${h[1]/2}${h[2]} + ${!0===u?"0px":"var(--mdc-drawer-width) "+("%"===h[2]?"":"/ 2")});\n margin-left: -13px !important;\n padding: 0 26px !important;\n }\n }\n .horizontal-buttons-stack.editor {\n position: relative;\n bottom: 0;\n left: 0;\n overflow: hidden;\n }\n .horizontal-buttons-stack.editor::before {\n top: -32px;\n left: -100%;\n background: none;\n width: 100%;\n height: 0;\n }\n .horizontal-buttons-stack-container.editor > .button {\n transition: background-color 0s, border 0s, transform 0s;\n }\n .horizontal-buttons-stack-container.editor {\n margin-left: 1px;\n }\n .horizontal-buttons-stack.editor > .card-content {\n position: relative;\n width: calc(100% + 26px) !important;\n left: -26px;\n margin: 0 !important;\n padding: 0;\n }\n `;!window.hasAnimated&&f&&(t.content.style.animation="from-bottom 1.3s forwards",window.hasAnimated=!0,setTimeout((()=>{t.content.style.animation="none"}),1500)),(0,e.L2)(n,t,M,r),a?(t.buttonsContainer.classList.add("editor"),t.card.classList.add("editor")):(t.buttonsContainer.classList.remove("editor"),t.card.classList.remove("editor"))}(this);break;case"button":!function(t){const n=t._hass,i=t.editor;let{customStyles:r,entityId:l,icon:c,name:d,widthDesktop:h,widthDesktopDivided:u,isSidebarHidden:g,state:m,stateChanged:b,stateOn:f,formatedState:_,riseAnimation:v,marginCenter:y,popUpOpen:w,rgbaColor:x,rgbColor:k,bgOpacity:C,shadowOpacity:$,bgBlur:S,iconColorOpacity:E,iconColor:O,iconFilter:L,iconStyles:A,haStyle:I,themeBgColor:T,color:V}=p(t,t.config,n);_=b||i?n.formatEntityState(n.states[l]):_||"";const D=t.config.button_type||"switch",z=!!t.config.show_state&&t.config.show_state;let M=l?n.states[l].attributes.brightness||0:"",B=l?n.states[l].attributes.volume_level||0:"",q=!1,U=M,H=B,Y=0,F=0,R=0,W=!1,j=null;if(!t.buttonAdded){const e=document.createElement("div");e.setAttribute("class","button-container"),t.content.appendChild(e)}const P=document.createElement("div");P.setAttribute("class","icon-container"),t.iconContainer=P;const X=document.createElement("div");X.setAttribute("class","name-container");const N=document.createElement("div");N.setAttribute("class","switch-button");const G=document.createElement("div");G.setAttribute("class","range-slider");const K=document.createElement("div");if(K.setAttribute("class","range-fill"),!t.buttonContainer||i){if(i&&t.buttonContainer){for(;t.buttonContainer.firstChild;)t.buttonContainer.removeChild(t.buttonContainer.firstChild);t.eventAdded=!1,t.wasEditing=!0}t.buttonContainer=t.content.querySelector(".button-container"),"slider"!==D||t.buttonAdded&&!i?("switch"===D||"custom"===D||i)&&(t.buttonContainer.appendChild(N),N.appendChild(P),N.appendChild(X),t.switchButton=t.content.querySelector(".switch-button")):(t.buttonContainer.appendChild(G),G.appendChild(P),G.appendChild(X),G.appendChild(K),t.rangeFill=t.content.querySelector(".range-fill")),(0,e.IU)(t,n,l,c,P,i),X.innerHTML=`\n

${d}

\n ${z?`

${_}

`:""}\n `,t.buttonAdded=!0}function Z(t){o("success");let e=t.querySelector(".feedback-element");e||(e=document.createElement("div"),e.setAttribute("class","feedback-element"),t.appendChild(e)),e.style.animation="tap-feedback .5s",setTimeout((()=>{e.style.animation="none",t.removeChild(e)}),500)}function J(t){Y=t.pageX||(t.touches?t.touches[0].pageX:0),F=t.pageY||(t.touches?t.touches[0].pageY:0),R=G.value,t.target!==P&&t.target!==P.querySelector("ha-icon")&&(q=!0,document.addEventListener("mouseup",tt,{passive:!0}),document.addEventListener("touchend",tt,{passive:!0}),document.addEventListener("mousemove",Q,{passive:!0}),document.addEventListener("touchmove",Q,{passive:!0}),j=setTimeout((()=>{ot(t.pageX||t.touches[0].pageX),et(),j=null}),200))}function Q(t){const e=t.pageX||(t.touches?t.touches[0].pageX:0),n=t.pageY||(t.touches?t.touches[0].pageY:0);Math.abs(n-F)>Math.abs(e-Y)?(clearTimeout(j),tt()):(document.removeEventListener("mousemove",Q),document.removeEventListener("touchmove",Q),document.addEventListener("mousemove",nt,{passive:!0}),document.addEventListener("touchmove",nt,{passive:!0}))}function tt(){q=!1,W=!1,et(),document.removeEventListener("mouseup",tt),document.removeEventListener("touchend",tt),document.removeEventListener("mousemove",nt),document.removeEventListener("touchmove",nt)}function et(){l.startsWith("light.")?(M=U,n.callService("light","turn_on",{entity_id:l,brightness:M})):l.startsWith("media_player.")&&(B=H,n.callService("media_player","volume_set",{entity_id:l,volume_level:B}))}function nt(t){const e=t.pageX||(t.touches?t.touches[0].pageX:0),n=t.pageY||(t.touches?t.touches[0].pageY:0);q&&Math.abs(e-Y)>10?(o("light"),ot(e)):q&&Math.abs(n-F)>10&&(q=!1,G.value=R)}function ot(t){const e=G.getBoundingClientRect(),n=Math.min(Math.max(t-e.left,0),e.width)/e.width;l.startsWith("light.")?U=Math.round(255*n):l.startsWith("media_player.")&&(H=n),K.style.transition="none",K.style.transform=`translateX(${100*n}%)`}z&&_&&(t.content.querySelector(".state").textContent=_),t.eventAdded||"switch"!==D?t.eventAdded||"slider"!==D?t.eventAdded||"custom"!==D||(N.addEventListener("click",(()=>Z(t.switchButton)),{passive:!0}),s(P,t.config,0,o),t.eventAdded=!0):(G.addEventListener("mousedown",J,{passive:!0}),G.addEventListener("touchstart",J,{passive:!0}),s(P,t.config,0,o),t.eventAdded=!0):(N.addEventListener("click",(()=>Z(t.switchButton)),{passive:!0}),N.addEventListener("click",(function(t){t.target!==P&&t.target!==P.querySelector("ha-icon")&&a(n,l)}),{passive:!0}),s(P,t.config,0,o),t.eventAdded=!0),t.isDragging||"slider"!==D||(t.rangeFill.style.transition="all .3s",l.startsWith("light.")?t.rangeFill.style.transform=`translateX(${M/255*100}%)`:l.startsWith("media_player.")&&(t.rangeFill.style.transform=`translateX(${100*B}%)`));const it=`\n ha-card {\n margin-top: 0 !important;\n background: none !important;\n opacity: ${"unavailable"!==m?"1":"0.5"};\n }\n \n .button-container {\n position: relative;\n width: 100%;\n height: 50px;\n z-index: 0;\n background-color: var(--background-color-2,var(--secondary-background-color));\n border-radius: 25px;\n mask-image: radial-gradient(white, black);\n -webkit-mask-image: radial-gradient(white, black);\n -webkit-backface-visibility: hidden;\n -moz-backface-visibility: hidden;\n -webkit-transform: translateZ(0);\n overflow: hidden;\n }\n \n .switch-button,\n .range-slider {\n display: inline-flex;\n position: absolute;\n height: 100%;\n width: 100%;\n transition: background-color 1.5s;\n background-color: ${f&&["switch","custom"].includes(D)?"var(--accent-color)":"rgba(0,0,0,0)"};\n }\n\n .range-fill {\n z-index: -1;\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n background-color: ${E};\n width: 100%;\n left: -100%;\n }\n \n .switch-button {\n cursor: pointer !important;\n }\n \n .range-slider {\n cursor: ew-resize;\n }\n \n .name-container {\n position: relative;\n display: ${z?"block":"inline-flex"};\n margin-left: 4px;\n z-index: 1;\n font-weight: 600;\n align-items: center;\n line-height: ${z?"4px":"16px"};\n padding-right: 16px;\n }\n \n .state {\n font-size: 12px;\n opacity: 0.7;\n }\n \n .feedback-element {\n position: absolute;\n top: 0;\n left: 0;\n opacity: 0;\n width: 100%;\n height: 100%;\n background-color: rgb(0,0,0);\n }\n \n @keyframes tap-feedback {\n 0% {transform: translateX(-100%); opacity: 0;}\n 64% {transform: translateX(0); opacity: 0.1;}\n 100% {transform: translateX(100%); opacity: 0;}\n }\n\n ${A}\n `;(0,e.L2)(n,t,it,r,m,l,b)}(this);break;case"separator":!function(t){const n=t._hass,o=t.editor,i=t.config;let{customStyles:a,entityId:r,icon:s,name:l,widthDesktop:c,widthDesktopDivided:d,isSidebarHidden:h,state:u,stateChanged:g,stateOn:m,formatedState:b,riseAnimation:f,marginCenter:_,popUpOpen:v,rgbaColor:y,rgbColor:w,bgOpacity:x,shadowOpacity:k,bgBlur:C,iconColorOpacity:$,iconColor:S,iconFilter:E,iconStyles:O,haStyle:L,themeBgColor:A,color:I}=p(t,i,n);if(!t.separatorAdded||o){if(o&&t.separatorContainer)for(;t.separatorContainer.firstChild;)t.separatorContainer.removeChild(t.separatorContainer.firstChild);t.separatorAdded||(t.separatorContainer=document.createElement("div"),t.separatorContainer.setAttribute("class","separator-container")),t.separatorContainer.innerHTML=`\n
\n \n

${l}

\n
\n
\n `,t.content.appendChild(t.separatorContainer),t.separatorAdded=!0}(0,e.L2)(n,t,"\n .separator-container {\n display: inline-flex;\n width: 100%;\n margin-top: 12px;\n }\n .separator-container div:first-child {\n display: inline-flex;\n max-width: calc(100% - 38px);\n }\n .separator-container div ha-icon {\n display: inline-flex;\n height: 24px;\n width: 24px;\n margin: 0 22px 0 8px;\n transform: translateY(-2px);\n }\n .separator-container div h4 {\n display: inline-flex;\n margin: 0 20px 0 0;\n font-size: 16px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n .separator-container div:last-child {\n display: inline-flex; \n border-radius: 6px; \n opacity: 0.5; \n margin-left: 10px; \n flex-grow: 1; \n height: 6px; \n align-self: center; \n background-color: var(--background-color,var(--secondary-background-color));\n }\n ",a)}(this);break;case"cover":!function(t){const n=t._hass,i=t.editor,a=t.config;let{customStyles:r,entityId:l,icon:c,name:d,widthDesktop:h,widthDesktopDivided:u,isSidebarHidden:g,state:m,stateChanged:b,stateOn:f,formatedState:_,riseAnimation:v,marginCenter:y,popUpOpen:w,rgbaColor:x,rgbColor:k,bgOpacity:C,shadowOpacity:$,bgBlur:S,iconColorOpacity:E,iconColor:O,iconFilter:L,iconStyles:A,haStyle:I,themeBgColor:T,color:V}=p(t,a,n);const D=a.icon_open?a.icon_open:"mdi:window-shutter-open",z=a.icon_close?a.icon_close:"mdi:window-shutter",M=a.open_service?a.open_service:"cover.open_cover",B=a.close_service?a.close_service:"cover.close_cover",q=a.stop_service?a.stop_service:"cover.stop_cover",U=a.icon_up?a.icon_up:"mdi:arrow-up",H=a.icon_down?a.icon_down:"mdi:arrow-down",Y=!!t.config.show_state&&t.config.show_state;if(c="open"===n.states[a.entity].state?D:z,_=b?n.formatEntityState(n.states[l]):_||"",!t.coverAdded||i){if(i&&t.coverContainer)for(;t.coverContainer.firstChild;)t.coverContainer.removeChild(t.coverContainer.firstChild);t.coverContainer=document.createElement("div"),t.coverContainer.setAttribute("class","cover-container"),t.coverContainer.innerHTML=`\n
\n
\n
\n
\n

${d}

\n

\n
\n
\n
\n \n \n \n
\n `,t.content.appendChild(t.coverContainer);const e=t.coverContainer.querySelector(".open"),r=t.coverContainer.querySelector(".stop"),c=t.coverContainer.querySelector(".close");e.addEventListener("click",(()=>{n.callService(M.split(".")[0],M.split(".")[1],{entity_id:l})}),{passive:!0}),r.addEventListener("click",(()=>{n.callService(q.split(".")[0],q.split(".")[1],{entity_id:l})}),{passive:!0}),c.addEventListener("click",(()=>{n.callService(B.split(".")[0],B.split(".")[1],{entity_id:l})}),{passive:!0}),t.iconContainer=t.content.querySelector(".icon-container"),s(t.iconContainer,a,0,o),t.coverAdded=!0}t.iconContainer&&(b||i)&&(t.iconContainer.innerHTML=``,t.content.querySelector(".state").textContent=Y?_:""),(0,e.L2)(n,t,"\n ha-card {\n margin-top: 0 !important;\n background: none !important;\n }\n \n .header-container {\n display: flex;\n align-items: center;\n margin-bottom: 10px;\n }\n \n .cover-container {\n display: grid;\n }\n \n .icon-container {\n display: flex;\n margin: 0 !important;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n /*z-index: 1;*/\n width: 48px;\n height: 48px;\n margin: 6px;\n border-radius: 50%;\n background-color: var(--card-background-color,var(--ha-card-background));\n border: 6px solid var(--background-color-2,var(--secondary-background-color));\n box-sizing: border-box;\n }\n \n .name-container {\n font-weight: 600;\n margin-left: 10px;\n line-height: 4px;\n }\n \n .buttons-container {\n display: grid;\n align-self: center;\n grid-auto-flow: column;\n grid-gap: 18px; \n }\n \n .state {\n font-size: 12px;\n opacity: 0.7;\n }\n \n ha-icon {\n display: flex; \n height: 24px; \n width: 24px; \n color: var(--primary-text-color);\n }\n \n .button {\n display: flex;\n background: var(--background-color-2,var(--secondary-background-color));\n height: 42px;\n border-radius: 32px;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n border: none;\n }\n ",r,m,l)}(this);break;case"empty-column":!function(t){if(!t.emptyCollumnAdded){const e=document.createElement("div");e.setAttribute("class","empty-column"),e.innerHTML='\n
\n ',t.content.appendChild(e),t.emptyColumnAdded=!0}}(this)}}setConfig(t){if("pop-up"===t.card_type){if(!t.hash)throw new Error("You need to define an hash. Please note that this card must be placed inside a vertical_stack to work as a pop-up.")}else if("horizontal-buttons-stack"===t.card_type){var e={};for(var n in t)if(n.match(/^\d+_icon$/)){var o=n.replace("_icon","_link");if(void 0===t[o])throw new Error("You need to define "+o);if(e[t[o]])throw new Error("You can't use "+t[o]+" twice");e[t[o]]=!0}}else if(("button"===t.card_type||"cover"===t.card_type)&&!t.entity)throw new Error("You need to define an entity");if(window.entityError)throw new Error("You need to define a valid entity");this.config=t}getCardSize(){return-1e4}static getConfigElement(){return document.createElement("bubble-card-editor")}}customElements.define("bubble-card",f),customElements.define("bubble-card-editor",class extends u{setConfig(t){this._config={...t}}static get properties(){return{hass:{},_config:{}}}get _card_type(){return this._config.card_type||""}get _button_type(){return this._config.button_type||"switch"}get _entity(){return this._config.entity||""}get _name(){return this._config.name||""}get _icon(){return this._config.icon||""}get _state(){return this._config.state||""}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 _trigger_close(){return this._config.trigger_close||!1}get _margin(){return this._config.margin||"7px"}get _margin_top_mobile(){return this._config.margin_top_mobile||"0px"}get _margin_top_desktop(){return this._config.margin_top_desktop||"0px"}get _width_desktop(){return this._config.width_desktop||"540px"}get _bg_color(){return this._config.bg_color||window.color}get _bg_opacity(){return void 0!==this._config.bg_opacity?this._config.bg_opacity:"88"}get _bg_blur(){return void 0!==this._config.bg_blur?this._config.bg_blur:"14"}get _shadow_opacity(){return void 0!==this._config.shadow_opacity?this._config.shadow_opacity:"0"}get _is_sidebar_hidden(){return this._config.is_sidebar_hidden||!1}get _rise_animation(){return void 0===this._config.rise_animation||this._config.rise_animation}get _auto_close(){return this._config.auto_close||""}get _back_open(){return this._config.back_open||!1}get _icon_open(){return this._config.icon_open||""}get _icon_close(){return this._config.icon_close||""}get _open_service(){return this._config.open_service||"cover.open_cover"}get _close_service(){return this._config.open_service||"cover.close_cover"}get _stop_service(){return this._config.open_service||"cover.stop_cover"}get _auto_order(){return this._config.auto_order||!1}get _highlightCurrentview(){return this._config.highlightCurrentview||!1}get _show_state(){return this._config.show_state||!1}render(){if(!this.hass)return g``;if(!this.listsUpdated){const t=t=>({label:t,value:t});this.allEntitiesList=Object.keys(this.hass.states).map(t),this.lightList=Object.keys(this.hass.states).filter((t=>"light"===t.substr(0,t.indexOf(".")))).map(t),this.sensorList=Object.keys(this.hass.states).filter((t=>"sensor"===t.substr(0,t.indexOf(".")))).map(t),this.binarySensorList=Object.keys(this.hass.states).filter((t=>"binary_sensor"===t.substr(0,t.indexOf(".")))).map(t),this.coverList=Object.keys(this.hass.states).filter((t=>"cover"===t.substr(0,t.indexOf(".")))).map(t),this.cardTypeList=[{label:"Button",value:"button"},{label:"Cover",value:"cover"},{label:"Empty column",value:"empty-column"},{label:"Horizontal buttons stack",value:"horizontal-buttons-stack"},{label:"Pop-up",value:"pop-up"},{label:"Separator",value:"separator"}],this.buttonTypeList=[{label:"Switch",value:"switch"},{label:"Slider",value:"slider"}],this.listsUpdated=!0}const t=this.allEntitiesList,e=(this.lightList,this.sensorList,this.coverList),n=this.cardTypeList,o=this.buttonTypeList;if("pop-up"===this._config.card_type)return g` +
+ ${this.makeDropdown("Card type","card_type",n)} +

Pop-up + + Regular mode + +

+ 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.

How to get the optimized mode?
+ + + ${this.makeDropdown("Optional - Icon","icon")} + ${this.makeDropdown("Optional - Entity to toggle (e.g. room light group)","entity",t)} + ${this.makeDropdown("Optional - Entity state to display (e.g. room temperature)","state",t)} + + +

Pop-up trigger

+ This allows you to open 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",t)} + + + +
+ +
+
+

Styling options

+ + + + + + +
+ +
+
+ +
+ + +
+
+ + +
+
+ + +
+ You can't set a value to 0 with the sliders for now, just change it to 0 in the text field if you need to. +

Advanced settings

+ + +
+ +
+
+ Back button/event support : This allow you to navigate through your pop-ups history when you press the back button of your browser. This setting can be applied only once, you don't need to change it in all pop-ups. If it's not working just turn it on for each pop-ups. + ${this.makeVersion()} +
+ `;if("button"===this._config.card_type)return g` +
+ ${this.makeDropdown("Card type","card_type",n)} +

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("slider"!==this._button_type?"Entity (toggle)":"Entity (light or media_player)","entity",t)} + + +
+ +
+
+ ${this.makeDropdown("Button type","button_type",o)} + + ${this.makeDropdown("Optional - Icon","icon")} + ${this.makeVersion()} +
+ `;if("separator"===this._config.card_type)return g` +
+ ${this.makeDropdown("Card type","card_type",n)} +

Separator

+ This card is a simple separator for dividing your pop-up into categories / sections. e.g. Lights, Devices, Covers, Settings, Automations... + + ${this.makeDropdown("Icon","icon")} + ${this.makeVersion()} +
+ `;if("horizontal-buttons-stack"===this._config.card_type){if(!this.buttonAdded&&this.shadowRoot.querySelector("#add-button")){const t=this.shadowRoot.querySelector("#add-button");for(this.buttonIndex=0;this._config[this.buttonIndex+1+"_link"];)this.buttonIndex++;t.addEventListener("click",(()=>{this.buttonIndex++;const e=t.style.opacity,n=t.innerText;t.style.opacity="0.6",t.style.transition="opacity 1s",t.innerText="Loading...",setTimeout((()=>{t.style.opacity=e,t.innerText=n}),5e3)}),{passive:!0}),this.buttonAdded=!0}return g` +
+ ${this.makeDropdown("Card type","card_type",n)} +

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()} +
+ `}return"cover"===this._config.card_type?g` +
+ ${this.makeDropdown("Card type","card_type",n)} +

Cover

+ This card allows you to control your covers. + ${this.makeDropdown("Entity","entity",e)} + + +
+ +
+
+ + + + + ${this.makeDropdown("Optional - Open icon","icon_open")} + ${this.makeDropdown("Optional - Closed icon","icon_close")} +

Styling options

+ ${this.makeDropdown("Optional - Arrow down icon","icon_down")} + ${this.makeDropdown("Optional - Arrow up icon","icon_up")} + ${this.makeVersion()} +
+ `:"empty-column"===this._config.card_type?g` +
+ ${this.makeDropdown("Card type","card_type",n)} +

Empty column

+ Just an empty card to fill any empty column. + ${this.makeVersion()} +
+ `:this._config.card_type?void 0:g` +
+ ${this.makeDropdown("Card type","card_type",n)} + You need to add a card type first. + +

Almost everything is available in the GUI editor, but in the YAML editor you can add your own custom styles, create custom buttons or modify the tap actions of all cards. You can find more details on my GitHub page.

+ +

And if you like my project and want to support me, please consider making a donation. Any amount is welcome and very much appreciated! 🍻

+
+ + +
+ ${this.makeVersion()} +
+ `}makeDropdown(t,e,n){return this.hass,t.includes("icon")||t.includes("Icon")?g` +
+ +
+ `:g` +
+ +
+ `}makeButton(){let t=[];for(let e=1;e<=this.buttonIndex;e++)t.push(g` +
+
+ this.removeButton(e)}> + Button ${e} +
+ + + + + +
+ `);return t}makeVersion(){return g` +

+ Bubble Card + + ${t} + +

+ `}removeButton(t){delete this._config[t+"_name"],delete this._config[t+"_icon"],delete this._config[t+"_link"],delete this._config[t+"_entity"],delete this._config[t+"_pir_sensor"];for(let e=t;e{window.addEventListener(t,n)}),{passive:!0});const i=new Event("urlChanged");function n(){const t=window.location.href;t!==this.currentUrl&&(window.dispatchEvent(i),this.currentUrl=t)}const o=()=>{window.dispatchEvent(i),window.addEventListener("popstate",n,{passive:!0})};window.addEventListener("popUpInitialized",o,{passive:!0}),window.eventAdded=!0}}set hass(hass){if(!this.content){this.attachShadow({mode:"open"}),this.shadowRoot.innerHTML='\n \n
\n
\n
\n ',this.card=this.shadowRoot.querySelector("ha-card"),this.content=this.shadowRoot.querySelector("div");const t=new Promise((t=>{t(document.querySelector("body > home-assistant").shadowRoot.querySelector("home-assistant-main").shadowRoot.querySelector("ha-drawer > partial-panel-resolver > ha-panel-lovelace").shadowRoot.querySelector("hui-root").shadowRoot.querySelector("div"))}));t.then((t=>{this.editorElement=t}))}let customStyles=this.config.styles?this.config.styles:"",entityId=this.config.entity&&hass.states[this.config.entity]?this.config.entity:"",icon=!this.config.icon&&this.config.entity?hass.states[entityId].attributes.icon||hass.states[entityId].attributes.entity_picture||"":this.config.icon||"",name=this.config.name?this.config.name:this.config.entity?hass.states[entityId].attributes.friendly_name:"",widthDesktop=this.config.width_desktop||"540px",widthDesktopDivided=widthDesktop?widthDesktop.match(/(\d+)(\D+)/):"",shadowOpacity=void 0!==this.config.shadow_opacity?this.config.shadow_opacity:"0",bgBlur=void 0!==this.config.bg_blur?this.config.bg_blur:"10",isSidebarHidden=this.config.is_sidebar_hidden||!1,state=entityId?hass.states[entityId].state:"",stateOn=["on","open","cleaning","true","home","playing"].includes(state)||0!==Number(state)&&!isNaN(Number(state)),formatedState,autoClose=this.config.auto_close||!1,riseAnimation=void 0===this.config.rise_animation||this.config.rise_animation,marginCenter=this.config.margin?"0"!==this.config.margin?this.config.margin:"0px":"7px",popUpHash=this.config.hash,popUpOpen,startTouchY,lastTouchY,triggerEntity=this.config.trigger_entity?this.config.trigger_entity:"",triggerState=this.config.trigger_state?this.config.trigger_state:"",triggerClose=!!this.config.trigger_close&&this.config.trigger_close,stateEntity=this.config.state;function toggleEntity(t){hass.callService("homeassistant","toggle",{entity_id:t})}this.editorElement&&(editor=this.editorElement.classList.contains("edit-mode"));const addStyles=function(context,styles,customStyles,state,entityId,stateChangedVar,path="",element=context.content){const customStylesEval=customStyles?eval("`"+customStyles+"`"):"";let styleAddedKey=styles+"Added";if(!context[styleAddedKey]||context.previousStyle!==customStylesEval||stateChangedVar||context.previousConfig!==context.config){if(!context[styleAddedKey]){if(editor&&!element)return;if(context.styleElement=element.querySelector("style"),!context.styleElement){context.styleElement=document.createElement("style");const t=path?element.querySelector(path):element;t?.appendChild(context.styleElement)}context[styleAddedKey]=!0}context.styleElement.innerHTML!==customStylesEval+styles&&(context.styleElement.innerHTML=customStylesEval+styles),context.previousStyle=customStylesEval,context.previousConfig=context.config}},forwardHaptic=t=>{fireEvent(window,"haptic",t)},navigate=(t,e,i=!1)=>{i?history.replaceState(null,"",e):history.pushState(null,"",e),fireEvent(window,"location-changed",{replace:i})},handleActionConfig=(t,e,i,n)=>{if(!n.confirmation||n.confirmation.exemptions&&n.confirmation.exemptions.some((t=>t.user===e.user.id))||(forwardHaptic("warning"),confirm(n.confirmation.text||`Are you sure you want to ${n.action}?`)))switch(n.action){case"more-info":(this.config.entity||this.config.camera_image)&&fireEvent(t,"hass-more-info",{entityId:this.config.entity?this.config.entity:this.config.camera_image});break;case"navigate":n.navigation_path&&navigate(t,n.navigation_path);break;case"url":n.url_path&&window.open(n.url_path);break;case"toggle":this.config.entity&&(toggleEntity(this.config.entity),forwardHaptic("success"));break;case"call-service":{if(!n.service)return void forwardHaptic("failure");const[t,i]=n.service.split(".",2);e.callService(t,i,n.service_data,n.target),forwardHaptic("success");break}case"fire-dom-event":fireEvent(t,"ll-custom",n)}},handleAction=(t,e,i,n)=>{let o;"double_tap"===n&&this.config.double_tap_action?o=this.config.double_tap_action:"hold"===n&&this.config.hold_action?o=this.config.hold_action:"tap"===n&&this.config.tap_action?o=this.config.tap_action:"double_tap"!==n||this.config.double_tap_action?("hold"!==n||this.config.hold_action)&&("tap"!==n||this.config.tap_action)||(o={action:"more-info"}):o={action:"toggle"},handleActionConfig(t,e,i,o)},addAction=function(){let t,e;return function(i,n,o,a){o.addEventListener(i,(()=>{const n=(new Date).getTime();"click"===i?n-(e||0)<250?(clearTimeout(t),handleAction(a,hass,{},"double_tap")):t=setTimeout((()=>{handleAction(a,hass,{},"tap")}),250):handleAction(a,hass,{},"hold"),e=n}),{passive:!0})}}();function addActions(t,e){addAction("click","tap",e,t),addAction("contextmenu","hold",e,t)}if(entityId){const t=!!hass.states[entityId].attributes&&hass.states[entityId].attributes;this.newPictureUrl=!!t.entity_picture&&t.entity_picture}function createIcon(t,e,i,n,o){updateIcon(t,e,i,n,o)}function updateIcon(t,e,i,n,o){for(;o.firstChild;)o.removeChild(o.firstChild);if(t.newPictureUrl&&!t.config.icon){const e=document.createElement("img");e.setAttribute("src",t.newPictureUrl),e.setAttribute("class","entity-picture"),e.setAttribute("alt","Icon"),o&&o.appendChild(e)}else{const t=document.createElement("ha-icon");t.setAttribute("icon",n),t.setAttribute("class","icon"),o&&o.appendChild(t)}}function isColorCloseToWhite(t){let e=[220,220,190];for(let i=0;i<3;i++)if(t[i]{stateChanged=!1}),0)),this.errorTriggered)return;this.initStyleAdded||this.host||editor||(this.card.style.marginTop="4000px",this.initStyleAdded=!0);const createPopUp=()=>{if(this.host){if(!this.popUp&&(this.verticalStack=this.getRootNode(),this.popUp=this.verticalStack.querySelector("#root"),this.verticalStack.contains(this.popUp)&&this.verticalStack.removeChild(this.popUp),!window.popUpInitialized&&this.popUp)){this.config.back_open||!1?localStorage.setItem("backOpen",!0):localStorage.setItem("backOpen",!1);if("true"===localStorage.getItem("backOpen")){window.backOpen=!0;const w=new Event("popUpInitialized");setTimeout((()=>{window.dispatchEvent(w)}),100)}else window.backOpen=!1,popUpOpen=popUpHash+!1,history.replaceState(null,null,location.href.split("#")[0]);window.popUpInitialized=!0}const t=this.popUp,e=this.verticalStack,i=this.config.text||"",n=this.config.state;formatedState=n?hass.formatEntityState(hass.states[n])+" "+i:i;const o=this.config.margin_top_mobile&&"0"!==this.config.margin_top_mobile?this.config.margin_top_mobile:"0px",a=this.config.margin_top_desktop&&"0"!==this.config.margin_top_desktop?this.config.margin_top_desktop:"0px",s=this.config.entity?"flex":"none";let r,l;if(state=n?hass.states[n].state:"",this.headerAdded){if(entityId){const x=this.content.querySelector("#header-container .header-icon"),k=this.content.querySelector("#header-container h2"),$=this.content.querySelector("#header-container p"),C=this.content.querySelector("#header-container .power-button");x.innerHTML="",createIcon(this,hass,entityId,icon,x),k.textContent=name,$.textContent=formatedState,C.setAttribute("style",`display: ${s};`)}}else{const S=document.createElement("div");S.setAttribute("id","header-container");const E=document.createElement("div");S.appendChild(E);const I=document.createElement("div");I.setAttribute("class","header-icon"),E.appendChild(I),createIcon(this,hass,entityId,icon,I),addActions(this,I);const O=document.createElement("h2");O.textContent=name,E.appendChild(O);const U=document.createElement("p");U.textContent=formatedState,E.appendChild(U);const H=document.createElement("ha-icon");H.setAttribute("class","power-button"),H.setAttribute("icon","mdi:power"),H.setAttribute("style",`display: ${s};`),E.appendChild(H);const T=document.createElement("button");T.setAttribute("class","close-pop-up"),T.onclick=function(){history.replaceState(null,null,location.href.split("#")[0]),localStorage.setItem("isManuallyClosed_"+popUpHash,!0)},S.appendChild(T);const A=document.createElement("ha-icon");A.setAttribute("icon","mdi:close"),T.appendChild(A),this.content.appendChild(S),this.header=E,this.headerAdded=!0}function c(){toggleEntity(entityId)}function d(t){"Escape"===t.key&&(popUpOpen=popUpHash+!1,history.replaceState(null,null,location.href.split("#")[0]),localStorage.setItem("isManuallyClosed_"+popUpHash,!0))}function h(t){window.hash===popUpHash&&m(),startTouchY=t.touches[0].clientY,lastTouchY=startTouchY}function p(t){t.touches[0].clientY-startTouchY>300&&t.touches[0].clientY>lastTouchY&&(popUpOpen=popUpHash+!1,history.replaceState(null,null,location.href.split("#")[0]),popUpOpen=popUpHash+!1,localStorage.setItem("isManuallyClosed_"+popUpHash,!0)),lastTouchY=t.touches[0].clientY}if(this.eventAdded||editor||(window["checkHashRef_"+popUpHash]=u,window.addEventListener("urlChanged",window["checkHashRef_"+popUpHash],{passive:!0}),window.addEventListener("click",(function(t){if(location.hash===popUpHash&&m(),!window.justOpened)return;const e=t.composedPath();!e||e.some((t=>"HA-MORE-INFO-DIALOG"===t.nodeName))||e.some((t=>"root"===t.id&&!t.classList.contains("close-pop-up")))||popUpOpen!==popUpHash+!0||(popUpOpen=popUpHash+!1,history.replaceState(null,null,location.href.split("#")[0]),localStorage.setItem("isManuallyClosed_"+popUpHash,!0))}),{passive:!0}),this.eventAdded=!0),entityId){const L=hass.states[entityId].attributes.rgb_color;this.rgbColor=L?isColorCloseToWhite(L)?"rgb(255,220,200)":`rgb(${L})`:stateOn?entityId.startsWith("light.")?"rgba(255,220,200, 0.5)":"var(--accent-color)":"rgba(255, 255, 255, 1",this.rgbColorOpacity=L?isColorCloseToWhite(L)?"rgba(255,220,200, 0.5)":`rgba(${L}, 0.5)`:entityId&&stateOn?entityId.startsWith("light.")?"rgba(255,220,200, 0.5)":"var(--accent-color)":"var(--background-color,var(--secondary-background-color))",l=convertToRGBA(color,0),this.iconFilter=L?isColorCloseToWhite(L)?"none":"brightness(1.1)":"none"}function u(){editor||(window.hash=location.hash.split("?")[0],window.hash===popUpHash?f():t.classList.contains("open-pop-up")&&b())}let g=this.content;function f(){t&&e.appendChild(t),setTimeout((function(){t.classList.remove("close-pop-up"),t.classList.add("open-pop-up"),g.querySelector(".power-button").addEventListener("click",c,{passive:!0}),window.addEventListener("keydown",d,{passive:!0}),t.addEventListener("touchstart",h,{passive:!0}),t.addEventListener("touchmove",p,{passive:!0}),popUpOpen=popUpHash+!0,setTimeout((()=>{window.justOpened=!0}),10),m()}),0)}function b(){t.classList.remove("open-pop-up"),t.classList.add("close-pop-up"),g.querySelector(".power-button").removeEventListener("click",c),window.removeEventListener("keydown",d),t.removeEventListener("touchstart",h),t.removeEventListener("touchmove",p),popUpOpen=popUpHash+!1,window.justOpened=!1,clearTimeout(r),setTimeout((function(){e.contains(t)&&e.removeChild(t)}),320)}function m(){clearTimeout(r),autoClose>0&&(r=setTimeout(_,autoClose))}function _(){history.replaceState(null,null,location.href.split("#")[0])}const y=`\n ha-card {\n margin-top: 0 !important;\n background: none !important;\n border: none !important;\n }\n .card-content {\n width: 100% !important;\n padding: 0 !important;\n }\n #root {\n transition: all 1s !important;\n position: fixed !important;\n margin: 0 -${marginCenter}; /* 7px */\n width: 100%;\n background-color: ${rgbaColor};\n box-shadow: 0px 0px 50px rgba(0,0,0,${shadowOpacity/100});\n backdrop-filter: blur(${bgBlur}px);\n -webkit-backdrop-filter: blur(${bgBlur}px);\n border-radius: 42px;\n box-sizing: border-box;\n top: calc(120% + ${o} + var(--header-height));\n grid-gap: 12px !important;\n gap: 12px !important;\n grid-auto-rows: min-content;\n padding: 18px 18px 220px 18px !important;\n height: 100% !important;\n -ms-overflow-style: none; /* for Internet Explorer, Edge */\n scrollbar-width: none; /* for Firefox */\n overflow-y: auto; \n overflow-x: hidden; \n z-index: 1 !important; /* Higher value hide the more-info panel */\n /* For older Safari but not working with Firefox */\n /* display: grid !important; */ \n }\n #root > bubble-pop-up:first-child::after {\n content: '';\n display: block;\n position: sticky;\n top: 0;\n left: -50px;\n margin: -70px 0 -36px -36px;\n overflow: visible;\n width: 200%;\n height: 100px;\n background: linear-gradient(0deg, ${l} 0%, ${rgbaColor} 80%);\n z-index: 0;\n } \n #root::-webkit-scrollbar {\n display: none; /* for Chrome, Safari, and Opera */\n }\n #root > bubble-pop-up:first-child {\n position: sticky;\n top: 0;\n z-index: 1;\n background: none !important;\n overflow: visible;\n }\n #root.open-pop-up {\n /*will-change: transform;*/\n transform: translateY(-120%);\n transition: transform .4s !important;\n }\n #root.open-pop-up > * {\n /* Block child items to overflow and if they do clip them */\n /*max-width: calc(100vw - 38px);*/\n max-width: 100% !important;\n overflow-x: clip;\n }\n #root.close-pop-up { \n transform: translateY(-20%);\n transition: transform .4s !important;\n box-shadow: none;\n }\n @media only screen and (min-width: 768px) {\n #root {\n top: calc(120% + ${a} + var(--header-height));\n width: calc(${widthDesktop}${"%"!==widthDesktopDivided[2]||isSidebarHidden?"":" - var(--mdc-drawer-width)"}) !important;\n left: calc(50% - ${widthDesktopDivided[1]/2}${widthDesktopDivided[2]});\n margin: 0 !important;\n }\n } \n @media only screen and (min-width: 870px) {\n #root {\n left: calc(50% - ${widthDesktopDivided[1]/2}${widthDesktopDivided[2]} + ${isSidebarHidden?"0px":"var(--mdc-drawer-width) "+("%"===widthDesktopDivided[2]?"":"/ 2")});\n }\n } \n #root.editor {\n position: inherit !important;\n width: 100% !important;\n padding: 18px !important;\n }\n `,v=`\n ha-card {\n margin-top: 0 !important;\n }\n #header-container {\n display: inline-flex;\n ${icon||name||entityId||state||i?"":"flex-direction: row-reverse;"}\n width: 100%;\n margin: 0;\n padding: 0;\n }\n #header-container > div {\n display: ${icon||name||entityId||state||i?"inline-flex":"none"};\n align-items: center;\n position: relative;\n padding: 6px;\n z-index: 1;\n flex-grow: 1;\n background-color: ${entityId?this.rgbColorOpacity:"var(--background-color,var(--secondary-background-color))"};\n transition: background 1s;\n border-radius: 25px;\n margin-right: 14px;\n backdrop-filter: blur(14px);\n -webkit-backdrop-filter: blur(14px);\n }\n .header-icon {\n display: inline-flex;\n min-width: 38px;\n min-height: 38px;\n background-color: var(--card-background-color,var(--ha-card-background));\n border-radius: 100%;\n margin: 0 10px 0 0;\n cursor: ${this.config.entity||this.config.double_tap_action||this.config.tap_action||this.config.hold_action?"pointer":"default"}; \n flex-wrap: wrap;\n align-content: center;\n justify-content: center;\n overflow: hidden;\n }\n .header-icon > ha-icon {\n color: ${stateOn?this.rgbColor?this.rgbColor:"var(--accent-color)":"inherit"};\n opacity: ${stateOn?"1":"0.6"};\n filter: ${this.iconFilter};\n }\n .header-icon::after {\n content: '';\n position: absolute;\n width: 38px;\n height: 38px;\n display: block;\n opacity: 0.2;\n transition: background-color 1s;\n border-radius: 50%;\n background-color: ${stateOn?this.rgbColor?this.rgbColor:"var(--accent-color)":"var(--card-background-color,var(--ha-card-background))"};\n }\n .entity-picture {\n height: calc(100% + 16px);\n width: calc(100% + 16px);\n }\n #header-container h2 {\n display: inline-flex;\n margin: 0 18px 0 0;\n /*line-height: 0px;*/\n z-index: 1;\n font-size: 20px;\n }\n #header-container p {\n display: inline-flex;\n line-height: 0px;\n font-size: 16px;\n min-width: fit-content ;\n }\n .power-button {\n cursor: pointer; \n flex-grow: inherit; \n width: 24px;\n height: 24px;\n border-radius: 12px;\n margin: 0 10px;\n background: none !important;\n justify-content: flex-end;\n background-color: var(--background-color,var(--secondary-background-color));\n }\n .close-pop-up {\n height: 50px;\n width: 50px;\n border: none;\n border-radius: 50%;\n z-index: 1;\n background: var(--background-color,var(--secondary-background-color));\n color: var(--primary-text-color);\n flex-shrink: 0;\n cursor: pointer;\n }\n `;if(addStyles(this,y,customStyles,state,entityId,"","",t),addStyles(this,v,customStyles,state,entityId,stateChanged),editor&&!this.editorModeAdded){if(!t)return;e.appendChild(t),t.classList.add("editor"),t.classList.remove("open-pop-up"),t.classList.remove("close-pop-up"),this.editorModeAdded=!0}else!editor&&this.editorModeAdded&&(t.classList.remove("editor"),e.contains(t)&&e.removeChild(t),this.editorModeAdded=!1)}else this.host=this.getRootNode().host};if(this.popUpAdded)!editor&&this.wasEditing&&stateChanged?(createPopUp(),this.wasEditing=!1):(popUpHash===window.hash&&stateChanged||editor&&!this.editorModeAdded)&&(createPopUp(),editor&&(this.wasEditing=!0));else{this.popUpAdded=!0;let t=setInterval((()=>{createPopUp(),this.popUp&&clearInterval(t)}),100);setTimeout((()=>{if(!this.popUp)throw this.errorTriggered=!0,clearInterval(t),new Error("Pop-up card must be placed inside a vertical_stack! If it's already the case, please ignore this error 🍻")}),6e3)}if(this.popUp&&triggerEntity&&stateChanged){null===localStorage.getItem("previousTriggerState_"+popUpHash)&&localStorage.setItem("previousTriggerState_"+popUpHash,""),null===localStorage.getItem("isManuallyClosed_"+popUpHash)&&localStorage.setItem("isManuallyClosed_"+popUpHash,"false"),null===localStorage.getItem("isTriggered_"+popUpHash)&&localStorage.setItem("isTriggered_"+popUpHash,"false");let t=localStorage.getItem("previousTriggerState_"+popUpHash),e="true"===localStorage.getItem("isManuallyClosed_"+popUpHash),i="true"===localStorage.getItem("isTriggered_"+popUpHash);hass.states[triggerEntity].state!==triggerState||null!==t||i||(navigate("",popUpHash),i=!0,localStorage.setItem("isTriggered_"+popUpHash,i)),hass.states[triggerEntity].state!==t&&(e=!1,localStorage.setItem("previousTriggerState_"+popUpHash,hass.states[triggerEntity].state),localStorage.setItem("isManuallyClosed_"+popUpHash,e)),hass.states[triggerEntity].state!==triggerState||e?hass.states[triggerEntity].state!==triggerState&&triggerClose&&this.popUp.classList.contains("open-pop-up")&&i&&!e&&(history.replaceState(null,null,location.href.split("#")[0]),popUpOpen=popUpHash+!1,i=!1,e=!0,localStorage.setItem("isManuallyClosed_"+popUpHash,e),localStorage.setItem("isTriggered_"+popUpHash,i)):(navigate("",popUpHash),i=!0,localStorage.setItem("isTriggered_"+popUpHash,i))}}setConfig(t){if("pop-up"===t.card_type&&!t.hash)throw new Error("You need to define an hash. Please note that this card must be placed inside a vertical_stack to work as a pop-up.");this.config=t}getCardSize(){return 0}static getConfigElement(){return document.createElement("bubble-pop-up-editor")}}let checkElementInterval=setInterval((function(){customElements.define("bubble-pop-up",BubblePopUp),customElements.get("bubble-pop-up")&&clearInterval(checkElementInterval)}),50);console.info(`%c Bubble Card - Pop-up %c ${version} `,"background-color: #555;color: #fff;padding: 3px 2px 3px 3px;border-radius: 14px 0 0 14px;font-family: DejaVu Sans,Verdana,Geneva,sans-serif;text-shadow: 0 1px 0 rgba(1, 1, 1, 0.3)","background-color: #506eac;color: #fff;padding: 3px 3px 3px 2px;border-radius: 0 14px 14px 0;font-family: DejaVu Sans,Verdana,Geneva,sans-serif;text-shadow: 0 1px 0 rgba(1, 1, 1, 0.3)");const fireEvent=(t,e,i,n)=>{n=n||{},i=null==i?{}:i;const o=new Event(e,{bubbles:void 0===n.bubbles||n.bubbles,cancelable:Boolean(n.cancelable),composed:void 0===n.composed||n.composed});return o.detail=i,t.dispatchEvent(o),o};customElements.get("ha-switch");const waitForElement=async()=>{for(;!customElements.get("ha-panel-lovelace");)await new Promise((t=>setTimeout(t,300)));const t=Object.getPrototypeOf(customElements.get("ha-panel-lovelace")),e=t.prototype.html,i=t.prototype.css;customElements.define("bubble-pop-up-editor",class extends t{setConfig(t){this._config={...t}}static get properties(){return{hass:{},_config:{}}}get _entity(){return this._config.entity||""}get _name(){return this._config.name||""}get _icon(){return this._config.icon||""}get _state(){return this._config.state||""}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 _trigger_close(){return this._config.trigger_close||!1}get _margin(){return this._config.margin||"7px"}get _margin_top_mobile(){return this._config.margin_top_mobile||"0px"}get _margin_top_desktop(){return this._config.margin_top_desktop||"0px"}get _width_desktop(){return this._config.width_desktop||"540px"}get _bg_color(){return this._config.bg_color||window.color}get _bg_opacity(){return void 0!==this._config.bg_opacity?this._config.bg_opacity:"88"}get _bg_blur(){return void 0!==this._config.bg_blur?this._config.bg_blur:"14"}get _shadow_opacity(){return void 0!==this._config.shadow_opacity?this._config.shadow_opacity:"0"}get _is_sidebar_hidden(){return this._config.is_sidebar_hidden||!1}get _auto_close(){return this._config.auto_close||""}get _back_open(){return this._config.back_open||!1}render(){if(!this.hass)return e``;if(!this.listsUpdated){const t=t=>({label:t,value:t});this.allEntitiesList=Object.keys(this.hass.states).map(t),this.lightList=Object.keys(this.hass.states).filter((t=>"light"===t.substr(0,t.indexOf(".")))).map(t),this.sensorList=Object.keys(this.hass.states).filter((t=>"sensor"===t.substr(0,t.indexOf(".")))).map(t),this.binarySensorList=Object.keys(this.hass.states).filter((t=>"binary_sensor"===t.substr(0,t.indexOf(".")))).map(t),this.coverList=Object.keys(this.hass.states).filter((t=>"cover"===t.substr(0,t.indexOf(".")))).map(t),this.cardTypeList=[{label:"Button",value:"button"},{label:"Cover",value:"cover"},{label:"Empty column",value:"empty-column"},{label:"Horizontal buttons stack",value:"horizontal-buttons-stack"},{label:"Pop-up",value:"pop-up"},{label:"Separator",value:"separator"}],this.buttonTypeList=[{label:"Switch",value:"switch"},{label:"Slider",value:"slider"}],this.listsUpdated=!0}const t=this.allEntitiesList;this.lightList,this.sensorList,this.coverList,this.cardTypeList,this.buttonTypeList;return e`

Pop-up 👍 Optimized mode

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("Optional - Icon","icon")} ${this.makeDropdown("Optional - Entity to toggle (e.g. room light group)","entity",t)} ${this.makeDropdown("Optional - Entity state to display (e.g. room temperature)","state",t)}

Pop-up trigger

This allows you to open 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",t)}

Styling options

You can't set a value to 0 with the sliders for now, just change it to 0 in the text field if you need to.

Advanced settings

Back button/event support : This allow you to navigate through your pop-ups history when you press the back button of your browser. This setting can be applied only once, you don't need to change it in all pop-ups. If it's not working just turn it on for each pop-ups. ${this.makeVersion()}
`}makeDropdown(t,i,n){this.hass;return t.includes("icon")||t.includes("Icon")?e`
`:e`
`}makeButton(){let t=[];for(let i=1;i<=this.buttonIndex;i++)t.push(e`
this.removeButton(i)}> Button ${i}
`);return t}makeVersion(){return e`

Bubble Card - Pop-up ${version}

`}removeButton(t){delete this._config[t+"_name"],delete this._config[t+"_icon"],delete this._config[t+"_link"],delete this._config[t+"_entity"],delete this._config[t+"_pir_sensor"];for(let e=t;e{"use strict";var __webpack_modules__={946:(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{__webpack_require__.d(__webpack_exports__,{G:()=>getIconStyles,IU:()=>createIcon,L2:()=>addStyles,_k:()=>convertToRGBA,mk:()=>getIconColor,wW:()=>isColorCloseToWhite});const addStyles=function(hass,context,styles,customStyles,state,entityId,stateChanged,path="",element=context.content){const customStylesEval=customStyles?eval("`"+customStyles+"`"):"";let styleAddedKey=styles+"Added";if(!context[styleAddedKey]||context.previousStyle!==customStylesEval||stateChanged||context.previousConfig!==context.config){if(!context[styleAddedKey]){if(context.styleElement=element.querySelector("style"),!context.styleElement){context.styleElement=document.createElement("style");const t=path?element.querySelector(path):element;t?.appendChild(context.styleElement)}context[styleAddedKey]=!0}context.styleElement.innerHTML!==customStylesEval+styles&&(context.styleElement.innerHTML=customStylesEval+styles),context.previousStyle=customStylesEval,context.previousConfig=context.config}};function createIcon(t,e,n,o,i,a){let r=!(!n||!e.states[n].attributes)&&e.states[n].attributes;t.imageUrl=!!r.entity_picture&&r.entity_picture,updateIcon(t,e,n,o,i),a||e.connection.subscribeEvents((a=>{a.data.entity_id===n&&a.data.old_state&&a.data.old_state.attributes.entity_picture!==a.data.new_state.attributes.entity_picture&&(t.imageUrl=a.data.new_state.attributes.entity_picture,updateIcon(t,e,n,o,i))}),"state_changed")}function updateIcon(t,e,n,o,i){for(;i.firstChild;)i.removeChild(i.firstChild);let a=t.config.icon&&t.config.icon.includes("/")?t.config.icon:t.imageUrl?t.imageUrl:"";if(a&&(r=e.states[n].state,n.startsWith("media_player.")&&!["off","unknown","idle",void 0].includes(r)||!n.startsWith("media_player."))){const t=document.createElement("div");t.setAttribute("class","entity-picture"),t.setAttribute("alt","Icon"),i&&(i.appendChild(t),i.style.background="center / cover no-repeat url("+a+"), var(--card-background-color,var(--ha-card-background))")}else{const t=document.createElement("ha-icon");t.setAttribute("icon",o),t.setAttribute("class","icon"),i&&i.appendChild(t)}var r}function isColorCloseToWhite(t){let e=[220,220,190];for(let n=0;n<3;n++)if(t[n]{for(var n in e)__webpack_require__.o(e,n)&&!__webpack_require__.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},__webpack_require__.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var __webpack_exports__={};(()=>{var t="v1.6.0-beta.1",e=__webpack_require__(946);const n=(t,e,n,o)=>{o=o||{},n=null==n?{}:n;const i=new Event(e,{bubbles:void 0===o.bubbles||o.bubbles,cancelable:Boolean(o.cancelable),composed:void 0===o.composed||o.composed});return i.detail=n,t.dispatchEvent(i),i},o=t=>{n(window,"haptic",t)},i=(t,e,o=!1)=>{o?history.replaceState(null,"",e):history.pushState(null,"",e),n(window,"location-changed",{replace:o})};function a(t,e,n){const o={entity:e.entity,tap_action:{action:"more-info"},double_tap_action:{action:"toggle"},hold_action:{action:"toggle"}},i=new Event("hass-action",{bubbles:!0,composed:!0});i.detail={config:o,action:n},t.dispatchEvent(i)}let r,s,l,c,d;function p(t,n,o,i){let a=n.styles?n.styles:"",d=n.entity&&o.states[n.entity]?n.entity:"",p=!n.icon&&n.entity?o.states[d].attributes.icon||o.states[d].attributes.entity_picture||"":n.icon||"",h=n.name?n.name:n.entity?o.states[d].attributes.friendly_name:"",u=n.width_desktop||"540px",g=u?u.match(/(\d+)(\D+)/):"",b=n.is_sidebar_hidden||!1,_=d?o.states[d].state:"";!function(t,e,n){t.hasState=e.states[n],t.hasState&&(t.newState=[t.hasState.state,t.hasState.attributes.rgb_color],t.oldState&&t.newState[0]===t.oldState[0]&&t.newState[1]===t.oldState[1]?t.stateChanged=!1:(t.oldState=t.newState,t.stateChanged=!0),t.stateChanged)}(t,o,d);let m=t.stateChanged,f=["on","open","cleaning","true","home","playing"].includes(_)||0!==Number(_)&&!isNaN(Number(_)),y=void 0===n.rise_animation||n.rise_animation,v=n.margin?"0"!==n.margin?n.margin:"0px":"7px",w=void 0!==n.bg_opacity?n.bg_opacity:"88",x=void 0!==n.shadow_opacity?n.shadow_opacity:"0",k=void 0!==n.bg_blur?n.bg_blur:"10",{iconColorOpacity:$,iconColor:C,iconFilter:S}=(0,e.mk)(o,d,f,e.wW),E=(0,e.G)(d,f,C,S),O=getComputedStyle(document.body),I=O.getPropertyValue("--ha-card-background")||O.getPropertyValue("--card-background-color"),L=n.bg_color?n.bg_color:I;if(L&&(!t.color||L!==t.color)){const n=1.02;s=(0,e._k)(L,w/100,n),t.color=L,window.color=L}return{customStyles:a,entityId:d,icon:p,name:h,widthDesktop:u,widthDesktopDivided:g,isSidebarHidden:b,state:_,stateChanged:m,stateOn:f,formatedState:c,riseAnimation:y,marginCenter:v,popUpOpen:r,rgbaColor:s,rgbColor:l,bgOpacity:w,shadowOpacity:x,bgBlur:k,iconColorOpacity:$,iconColor:C,iconFilter:S,iconStyles:E,haStyle:O,themeBgColor:I,color:L}}new MutationObserver(((e,o)=>{if(customElements.get("ha-panel-lovelace")){const e=Object.getPrototypeOf(customElements.get("ha-panel-lovelace")),i=e.prototype.html,a=e.prototype.css;class r extends e{setConfig(t){this._config={...t}}static get properties(){return{hass:{},_config:{}}}get _entity(){return this._config.entity||""}get _name(){return this._config.name||""}get _icon(){return this._config.icon||""}get _state(){return this._config.state||""}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 _trigger_close(){return this._config.trigger_close||!1}get _margin(){return this._config.margin||"7px"}get _margin_top_mobile(){return this._config.margin_top_mobile||"0px"}get _margin_top_desktop(){return this._config.margin_top_desktop||"0px"}get _width_desktop(){return this._config.width_desktop||"540px"}get _bg_color(){return this._config.bg_color||window.color}get _bg_opacity(){return void 0!==this._config.bg_opacity?this._config.bg_opacity:"88"}get _bg_blur(){return void 0!==this._config.bg_blur?this._config.bg_blur:"14"}get _shadow_opacity(){return void 0!==this._config.shadow_opacity?this._config.shadow_opacity:"0"}get _is_sidebar_hidden(){return this._config.is_sidebar_hidden||!1}get _auto_close(){return this._config.auto_close||""}get _back_open(){return this._config.back_open||!1}render(){if(!this.hass)return i``;if(!this.listsUpdated){const t=t=>({label:t,value:t});this.allEntitiesList=Object.keys(this.hass.states).map(t),this.lightList=Object.keys(this.hass.states).filter((t=>"light"===t.substr(0,t.indexOf(".")))).map(t),this.sensorList=Object.keys(this.hass.states).filter((t=>"sensor"===t.substr(0,t.indexOf(".")))).map(t),this.binarySensorList=Object.keys(this.hass.states).filter((t=>"binary_sensor"===t.substr(0,t.indexOf(".")))).map(t),this.coverList=Object.keys(this.hass.states).filter((t=>"cover"===t.substr(0,t.indexOf(".")))).map(t),this.cardTypeList=[{label:"Button",value:"button"},{label:"Cover",value:"cover"},{label:"Empty column",value:"empty-column"},{label:"Horizontal buttons stack",value:"horizontal-buttons-stack"},{label:"Pop-up",value:"pop-up"},{label:"Separator",value:"separator"}],this.buttonTypeList=[{label:"Switch",value:"switch"},{label:"Slider",value:"slider"}],this.listsUpdated=!0}const t=this.allEntitiesList;return this.lightList,this.sensorList,this.coverList,this.cardTypeList,this.buttonTypeList,i` +
+

Pop-up + + 👍 Optimized mode + +

+ 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("Optional - Icon","icon")} + ${this.makeDropdown("Optional - Entity to toggle (e.g. room light group)","entity",t)} + ${this.makeDropdown("Optional - Entity state to display (e.g. room temperature)","state",t)} + + +

Pop-up trigger

+ This allows you to open 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",t)} + + + +
+ +
+
+

Styling options

+ + + + + + +
+ +
+
+ +
+ + +
+
+ + +
+
+ + +
+ You can't set a value to 0 with the sliders for now, just change it to 0 in the text field if you need to. +

Advanced settings

+ + +
+ +
+
+ Back button/event support : This allow you to navigate through your pop-ups history when you press the back button of your browser. This setting can be applied only once, you don't need to change it in all pop-ups. If it's not working just turn it on for each pop-ups. + ${this.makeVersion()} +
+ `}makeDropdown(t,e,n){return this.hass,t.includes("icon")||t.includes("Icon")?i` +
+ +
+ `:i` +
+ +
+ `}makeButton(){let t=[];for(let e=1;e<=this.buttonIndex;e++)t.push(i` +
+
+ this.removeButton(e)}> + Button ${e} +
+ + + + + +
+ `);return t}makeVersion(){return i` +

+ Bubble Card - Pop-up + + ${t} + +

+ `}removeButton(t){delete this._config[t+"_name"],delete this._config[t+"_icon"],delete this._config[t+"_link"],delete this._config[t+"_entity"],delete this._config[t+"_pir_sensor"];for(let e=t;e{e<10?(window.dispatchEvent(t),e++):clearInterval(n)}),1e3)}window.popUpInitialized=!1,["click","mousedown","touchstart","focus","location-changed","connection-status"].forEach((t=>{window.addEventListener(t,e)}),{passive:!0});const n=()=>{window.dispatchEvent(t),window.addEventListener("popstate",e,{passive:!0})};window.addEventListener("popUpInitialized",n,{passive:!0}),window.eventAdded=!0}}();class h extends HTMLElement{set hass(t){var n;this._hass=t,this.editor=d,async function(t){if(!window.resourcesChecked){window.resourcesChecked=!0;let e=(await t.callWS({type:"lovelace/resources"})).find((t=>t.url.includes("bubble-pop-up.js")));e&&await t.callWS({type:"lovelace/resources/delete",resource_id:e.id})}}(t),(n=this).content||(n.attachShadow({mode:"open"}),n.shadowRoot.innerHTML='\n \n
\n
\n
\n ',n.card=n.shadowRoot.querySelector("ha-card"),n.content=n.shadowRoot.querySelector("div")),async function(t){if(window.editorElement)t=window.editorElement.classList.contains("edit-mode");else{const t=new Promise((t=>{t(document.querySelector("body > home-assistant").shadowRoot.querySelector("home-assistant-main").shadowRoot.querySelector("ha-drawer > partial-panel-resolver > ha-panel-lovelace").shadowRoot.querySelector("hui-root").shadowRoot.querySelector("div"))}));window.editorElement=await t}return t}(d).then((t=>{d=t})),function(t){const n=t._hass,r=t.editor,s=t.config;if(!n)return;let l,c,{customStyles:d,entityId:h,icon:u,name:g,widthDesktop:b,widthDesktopDivided:_,isSidebarHidden:m,state:f,stateChanged:y,stateOn:v,formatedState:w,riseAnimation:x,marginCenter:k,popUpOpen:$,rgbaColor:C,rgbColor:S,bgOpacity:E,shadowOpacity:O,bgBlur:I,iconColorOpacity:L,iconColor:T,iconFilter:A,iconStyles:V,haStyle:M,themeBgColor:U,color:q}=p(t,s,n),B=s.auto_close||!1,z=s.hash,D=s.trigger_entity?s.trigger_entity:"",j=s.trigger_state?s.trigger_state:"",R=!!s.trigger_close&&s.trigger_close;if(t.errorTriggered)return;t.initStyleAdded||t.host||r||(t.card.style.marginTop="4000px",t.initStyleAdded=!0);const W=()=>{if(t.host){if(!t.popUp&&(t.verticalStack=t.getRootNode(),t.popUp=t.verticalStack.querySelector("#root"),!window.popUpInitialized&&t.popUp)){if(s.back_open?localStorage.setItem("backOpen",!0):localStorage.setItem("backOpen",!1),"true"===localStorage.getItem("backOpen")){window.backOpen=!0;const G=new Event("popUpInitialized");setTimeout((()=>{window.dispatchEvent(G)}),0)}else window.backOpen=!1,$=z+!1,history.replaceState(null,null,location.href.split("#")[0]);window.popUpInitialized=!0}const i=t.popUp,p=(t.verticalStack,s.text||""),x=s.state;w=x?n.formatEntityState(n.states[x]):w||"";const S=s.margin_top_mobile&&"0"!==s.margin_top_mobile?s.margin_top_mobile:"0px",E=s.margin_top_desktop&&"0"!==s.margin_top_desktop?s.margin_top_desktop:"0px",L=s.entity?"flex":"none";let T,A;if(f=x?n.states[x].state:"",t.headerAdded){if(h){const K=t.content.querySelector("#header-container .icon-container"),J=t.content.querySelector("#header-container h2"),Q=t.content.querySelector("#header-container p"),X=t.content.querySelector("#header-container .power-button");K.innerHTML="",(0,e.IU)(t,n,h,u,K,r),J.textContent=g,Q.textContent=w,X.setAttribute("style",`display: ${L};`)}}else{const Z=document.createElement("div");Z.setAttribute("id","header-container");const tt=document.createElement("div");Z.appendChild(tt);const et=document.createElement("div");et.setAttribute("class","icon-container"),tt.appendChild(et),(0,e.IU)(t,n,h,u,et,r),function(t,e,n,o){e.tap_action,e.double_tap_action,e.hold_action;let i,r=0,s=0,l=0;t.addEventListener("mousedown",(()=>{s=Date.now(),i=setTimeout((()=>{a(t,e,"hold")}),300)})),t.addEventListener("mouseup",(()=>{clearTimeout(i),l=Date.now(),l-s<300&&(r++,1===r&&setTimeout((()=>{1===r?a(t,e,"tap"):(a(t,e,"double_tap"),o("success")),r=0}),300)),s=0,l=0})),t.addEventListener("touchstart",(n=>{o("light"),s=Date.now(),i=setTimeout((()=>{a(t,e,"hold")}),300),n.preventDefault()})),t.addEventListener("touchend",(n=>{clearTimeout(i),l=Date.now(),l-s<300&&(r++,1===r&&setTimeout((()=>{a(t,e,1===r?"tap":"double_tap"),r=0}),300)),s=0,l=0,n.preventDefault()})),t.addEventListener("mouseout",(()=>{clearTimeout(i)})),t.addEventListener("touchcancel",(()=>{clearTimeout(i)}))}(et,s,0,o);const nt=document.createElement("h2");nt.textContent=g,tt.appendChild(nt);const ot=document.createElement("p");ot.textContent=w,tt.appendChild(ot);const it=document.createElement("ha-icon");it.setAttribute("class","power-button"),it.setAttribute("icon","mdi:power"),it.setAttribute("style",`display: ${L};`),tt.appendChild(it);const at=document.createElement("button");at.setAttribute("class","close-pop-up"),at.onclick=function(){history.replaceState(null,null,location.href.split("#")[0]),localStorage.setItem("isManuallyClosed_"+z,!0)},Z.appendChild(at);const rt=document.createElement("ha-icon");rt.setAttribute("icon","mdi:close"),at.appendChild(rt),t.content.appendChild(Z),t.header=tt,t.headerAdded=!0}function M(){!function(t,e){t.callService("homeassistant","toggle",{entity_id:e})}(n,h)}function U(t){"Escape"===t.key&&($=z+!1,history.replaceState(null,null,location.href.split("#")[0]),localStorage.setItem("isManuallyClosed_"+z,!0))}function D(t){window.hash===z&&H(),l=t.touches[0].clientY,c=l}function j(t){t.touches[0].clientY-l>300&&t.touches[0].clientY>c&&($=z+!1,history.replaceState(null,null,location.href.split("#")[0]),$=z+!1,localStorage.setItem("isManuallyClosed_"+z,!0)),c=t.touches[0].clientY}if(t.eventAdded||r||(window["checkHashRef_"+z]=R,window.addEventListener("urlChanged",window["checkHashRef_"+z],{passive:!0}),window.addEventListener("click",(function(t){if(location.hash===z&&H(),!window.justOpened)return;const e=t.composedPath();!e||e.some((t=>"HA-MORE-INFO-DIALOG"===t.nodeName))||e.some((t=>"root"===t.id&&!t.classList.contains("close-pop-up")))||$!==z+!0||setTimeout((function(){location.hash===z&&($=z+!1,history.replaceState(null,null,location.href.split("#")[0]),localStorage.setItem("isManuallyClosed_"+z,!0))}),2)}),{passive:!0}),t.eventAdded=!0),h){const st=n.states[h].attributes.rgb_color;t.rgbColor=st?(0,e.wW)(st)?"rgb(255,220,200)":`rgb(${st})`:v?h.startsWith("light.")?"rgba(255,220,200, 0.5)":"var(--accent-color)":"rgba(255, 255, 255, 1",t.rgbColorOpacity=st?(0,e.wW)(st)?"rgba(255,220,200, 0.5)":`rgba(${st}, 0.5)`:h&&v?h.startsWith("light.")?"rgba(255,220,200, 0.5)":"var(--accent-color)":"var(--background-color,var(--secondary-background-color))",A=(0,e._k)(q,0),t.iconFilter=st?(0,e.wW)(st)?"none":"brightness(1.1)":"none"}else A=(0,e._k)(q,0);function R(){r||(window.hash=location.hash.split("?")[0],window.hash===z?(setTimeout((function(){i.classList.remove("close-pop-up"),i.classList.add("open-pop-up"),W.querySelector(".power-button").addEventListener("click",M,{passive:!0}),window.addEventListener("keydown",U,{passive:!0}),i.addEventListener("touchstart",D,{passive:!0}),i.addEventListener("touchmove",j,{passive:!0}),$=z+!0,document.body.style.overflow="hidden",setTimeout((()=>{window.justOpened=!0}),10),H()}),0),setTimeout((function(){P(i,!1)}),0)):i.classList.contains("open-pop-up")&&(setTimeout((function(){i.classList.remove("open-pop-up"),i.classList.add("close-pop-up"),W.querySelector(".power-button").removeEventListener("click",M),window.removeEventListener("keydown",U),i.removeEventListener("touchstart",D),i.removeEventListener("touchmove",j),$=z+!1,document.body.style.overflow="",window.justOpened=!1,clearTimeout(T)}),0),setTimeout((function(){P(i,!0)}),320)))}let W=t.content;function P(t,e){for(var n=t.querySelectorAll("video"),o=0;o0&&!n[o].paused&&!n[o].ended&&n[o].readyState>n[o].HAVE_CURRENT_DATA;e&&i?n[o].pause():e||i||(n[o].play(),n[o].currentTime>0&&(n[o].currentTime=1e4))}var a=t.querySelectorAll("*");for(o=0;o0&&(T=setTimeout(F,B))}function F(){history.replaceState(null,null,location.href.split("#")[0])}r&&!t.editorModeAdded&&(console.log(z),i.classList.add("editor"),i.classList.remove("open-pop-up"),i.classList.remove("close-pop-up"),t.editorModeAdded=!0);const N=` \n\t ha-card {\n\t margin-top: 0 !important;\n\t background: none !important;\n\t border: none !important;\n\t }\n\t .card-content {\n\t width: 100% !important;\n\t padding: 0 !important;\n\t }\n\t #root {\n\t transition: all 1s !important;\n\t position: fixed !important;\n\t margin: 0 -${k}; /* 7px */\n\t width: 100%;\n\t background-color: ${C};\n\t box-shadow: 0px 0px 50px rgba(0,0,0,${O/100});\n\t backdrop-filter: blur(${I}px);\n\t -webkit-backdrop-filter: blur(${I}px);\n\t border-radius: 42px;\n\t box-sizing: border-box;\n\t top: calc(120% + ${S} + var(--header-height));\n\t grid-gap: 12px !important;\n\t gap: 12px !important;\n\t grid-auto-rows: min-content;\n\t padding: 18px 18px 220px 18px !important;\n\t height: 100% !important;\n\t -ms-overflow-style: none; /* for Internet Explorer, Edge */\n\t scrollbar-width: none; /* for Firefox */\n\t overflow-y: auto; \n\t overflow-x: hidden; \n\t z-index: 1 !important; /* Higher value hide the more-info panel */\n\t /* For older Safari but not working with Firefox */\n\t /* display: grid !important; */ \n\t }\n\t #root.hidden {\n\t \tdisplay: none !important;\n\t }\n\t #root > :first-child::after {\n\t content: '';\n\t display: block;\n\t position: sticky;\n\t top: 0;\n\t left: -50px;\n\t margin: -70px 0 -36px -36px;\n\t overflow: visible;\n\t width: 200%;\n\t height: 100px;\n\t background: linear-gradient(0deg, ${A} 0%, ${C} 80%);\n\t z-index: 0;\n\t } \n\t #root::-webkit-scrollbar {\n\t display: none; /* for Chrome, Safari, and Opera */\n\t }\n\t #root > :first-child {\n\t position: sticky;\n\t top: 0;\n\t z-index: 1;\n\t background: none !important;\n\t overflow: visible;\n\t }\n\t #root.open-pop-up {\n\t /*will-change: transform;*/\n\t transform: translateY(-120%);\n\t transition: transform .36s !important;\n\t }\n\t #root.open-pop-up > * {\n\t /* Block child items to overflow and if they do clip them */\n\t /*max-width: calc(100vw - 38px);*/\n\t max-width: 100% !important;\n\t /*overflow-x: clip;*/\n\t }\n\t #root.close-pop-up { \n\t transform: translateY(-20%);\n\t transition: transform .4s !important;\n\t box-shadow: none;\n\t }\n\t @media only screen and (min-width: 600px) {\n\t #root {\n\t top: calc(120% + ${E} + var(--header-height));\n\t width: calc(${b}${"%"!==_[2]||m?"":" - var(--mdc-drawer-width)"}) !important;\n\t left: calc(50% - ${_[1]/2}${_[2]});\n\t margin: 0 !important;\n\t }\n\t } \n\t @media only screen and (min-width: 870px) {\n\t #root {\n\t left: calc(50% - ${_[1]/2}${_[2]} + ${m?"0px":"var(--mdc-drawer-width) "+("%"===_[2]?"":"/ 2")});\n\t }\n\t } \n\t #root.editor {\n\t position: inherit !important;\n\t width: 100% !important;\n\t padding: 18px !important;\n\t }\n\t `,Y=`\n\t ${V}\n\n\t ha-card {\n\t margin-top: 0 !important;\n\t }\n\t #header-container {\n\t display: inline-flex;\n\t ${u||g||h||f||p?"":"flex-direction: row-reverse;"}\n\t height: 50px;\n\t width: 100%;\n\t margin: 0;\n\t padding: 0;\n\t }\n\t #header-container > div {\n\t display: ${u||g||h||f||p?"inline-flex":"none"};\n\t align-items: center;\n\t position: relative;\n\t padding-right: 6px;\n\t z-index: 1;\n\t flex-grow: 1;\n\t background-color: ${h?t.rgbColorOpacity:"var(--background-color,var(--secondary-background-color))"};\n\t transition: background 1s;\n\t border-radius: 25px;\n\t margin-right: 14px;\n\t backdrop-filter: blur(14px);\n\t -webkit-backdrop-filter: blur(14px);\n\t }\n\t #header-container h2 {\n\t display: inline-flex;\n\t margin: 0 18px 0 0;\n\t padding: 4px;\n\t z-index: 1;\n\t font-size: 18px;\n\t }\n\t #header-container p {\n\t display: inline-flex;\n\t font-size: 16px;\n\t min-width: fit-content ;\n\t }\n\t .power-button {\n\t cursor: pointer; \n\t flex-grow: inherit; \n\t width: 24px;\n\t height: 24px;\n\t border-radius: 12px;\n\t margin: 0 10px;\n\t background: none !important;\n\t justify-content: flex-end;\n\t background-color: var(--background-color,var(--secondary-background-color));\n\t }\n\t .close-pop-up {\n\t height: 50px;\n\t width: 50px;\n\t border: none;\n\t border-radius: 50%;\n\t z-index: 1;\n\t background: var(--background-color,var(--secondary-background-color));\n\t color: var(--primary-text-color);\n\t flex-shrink: 0;\n\t cursor: pointer;\n\t }\n\t `;setTimeout((()=>{(0,e.L2)(n,t,Y,d,f,h,y),(0,e.L2)(n,t,N,d,f,h,t.bgColorChanged,"",i)}),0)}else t.host=t.getRootNode().host};if(t.popUp&&C&&C!==t.oldBgColor&&location.hash===z?(t.oldBgColor=C,t.bgColorChanged=!0):t.bgColorChanged=!1,t.popUpAdded)!r&&t.wasEditing&&(y||t.bgColorChanged)?(W(),t.wasEditing=!1):(z===window.hash&&(y||t.bgColorChanged)||r&&!t.editorModeAdded)&&(W(),r&&(t.wasEditing=!0));else{t.popUpAdded=!0;let e=setInterval((()=>{W(),t.popUp&&clearInterval(e)}),0);setTimeout((()=>{if(!t.popUp)throw t.errorTriggered=!0,clearInterval(e),new Error("Pop-up card must be placed inside a vertical_stack! If it's already the case, please ignore this error 🍻")}),6e3)}if(!r&&t.popUp&&t.editorModeAdded&&(t.popUp.classList.remove("editor"),t.editorModeAdded=!1),t.popUp&&D&&y){null===localStorage.getItem("previousTriggerState_"+z)&&localStorage.setItem("previousTriggerState_"+z,""),null===localStorage.getItem("isManuallyClosed_"+z)&&localStorage.setItem("isManuallyClosed_"+z,"false"),null===localStorage.getItem("isTriggered_"+z)&&localStorage.setItem("isTriggered_"+z,"false");let e=localStorage.getItem("previousTriggerState_"+z),o="true"===localStorage.getItem("isManuallyClosed_"+z),a="true"===localStorage.getItem("isTriggered_"+z);n.states[D].state!==j||null!==e||a||(i(0,z),a=!0,localStorage.setItem("isTriggered_"+z,a)),n.states[D].state!==e&&(o=!1,localStorage.setItem("previousTriggerState_"+z,n.states[D].state),localStorage.setItem("isManuallyClosed_"+z,o)),n.states[D].state!==j||o?n.states[D].state!==j&&R&&t.popUp.classList.contains("open-pop-up")&&a&&!o&&(history.replaceState(null,null,location.href.split("#")[0]),$=z+!1,a=!1,o=!0,localStorage.setItem("isManuallyClosed_"+z,o),localStorage.setItem("isTriggered_"+z,a)):(i(0,z),a=!0,localStorage.setItem("isTriggered_"+z,a))}}(this)}setConfig(t){if(!t.hash)throw new Error("You need to define an hash. Please note that this card must be placed inside a vertical_stack to work as a pop-up.");this.config=t}getCardSize(){return-1e4}static getConfigElement(){return document.createElement("bubble-pop-up-editor")}}new MutationObserver(((t,e)=>{customElements.get("ha-panel-lovelace")&&(customElements.define("bubble-pop-up",h),e.disconnect())})).observe(document,{childList:!0,subtree:!0}),window.customCards=window.customCards||[],window.customCards.push({type:"bubble-pop-up",name:"Bubble Pop-up",preview:!1,description:"Just add it in a vertical-stack first."}),console.info(`%c Bubble Card - Pop-up %c ${t} `,"background-color: #555;color: #fff;padding: 3px 2px 3px 3px;border-radius: 14px 0 0 14px;font-family: DejaVu Sans,Verdana,Geneva,sans-serif;text-shadow: 0 1px 0 rgba(1, 1, 1, 0.3)","background-color: #506eac;color: #fff;padding: 3px 3px 3px 2px;border-radius: 0 14px 14px 0;font-family: DejaVu Sans,Verdana,Geneva,sans-serif;text-shadow: 0 1px 0 rgba(1, 1, 1, 0.3)")})()})(); \ No newline at end of file diff --git a/src/bubble-card.ts b/src/bubble-card.ts new file mode 100644 index 00000000..398dfb19 --- /dev/null +++ b/src/bubble-card.ts @@ -0,0 +1,123 @@ +import { version } from './var/version.ts'; +import { addUrlListener } from './tools/url-listener.ts'; +import { initializeContent, checkEditor } from './tools/init.ts'; +import { handlePopUp } from './cards/pop-up.ts'; +import { handleHorizontalButtonsStack } from './cards/horizontal-buttons-stack.ts'; +import { handleButton } from './cards/button.ts'; +import { handleSeparator } from './cards/separator.ts'; +import { handleCover } from './cards/cover.ts'; +import { handleEmptyColumn } from './cards/empty-column.ts'; +import BubbleCardEditor from './editor/bubble-card-editor.ts'; + +let editor; +addUrlListener(); + +class BubbleCard extends HTMLElement { + + set hass(hass) { + + this._hass = hass; + this.editor = editor; + + initializeContent(this); + + checkEditor(editor).then((value) => { + editor = value; + }); + + switch (this.config.card_type) { + // Initialize pop-up card + case 'pop-up': + handlePopUp(this); + break; + + // Initialize horizontal buttons stack + case 'horizontal-buttons-stack' : + handleHorizontalButtonsStack(this); + break; + + // Initialize button + case 'button' : + handleButton(this); + break; + + // Initialize separator + case 'separator' : + handleSeparator(this); + break; + + // Initialize cover card + case 'cover' : + handleCover(this); + break; + + // Intitalize empty card + case 'empty-column' : + handleEmptyColumn(this); + break; + } + } + + setConfig(config) { + if (config.card_type === 'pop-up') { + if (!config.hash) { + throw new Error("You need to define an hash. Please note that this card must be placed inside a vertical_stack to work as a pop-up."); + } + } else if (config.card_type === 'horizontal-buttons-stack') { + var definedLinks = {}; + + for (var key in config) { + if (key.match(/^\d+_icon$/)) { + var iconKey = key; + var linkKey = key.replace('_icon', '_link'); + + if (config[linkKey] === undefined) { + throw new Error("You need to define " + linkKey); + } + + if (definedLinks[config[linkKey]]) { + throw new Error("You can't use " + config[linkKey] + " twice" ); + } + + definedLinks[config[linkKey]] = true; + } + } + } else if (config.card_type === 'button' || config.card_type === 'cover') { + if (!config.entity) { + throw new Error("You need to define an entity"); + } + } + + if (window.entityError) { + throw new Error("You need to define a valid entity"); + } + + this.config = config; + } + + getCardSize() { + // Fix the empty columns caused by the pop-ups on the dashboard + return -10000; + } + + static getConfigElement() { + return document.createElement("bubble-card-editor"); + } +} + +customElements.define("bubble-card", BubbleCard); +customElements.define('bubble-card-editor', BubbleCardEditor); + +window.customCards = window.customCards || []; +window.customCards.push({ + type: "bubble-card", + name: "Bubble Card", + preview: false, + description: "A minimalist card collection with a nice pop-up touch." +}); + +console.info( + `%c Bubble Card %c ${version} `, + 'background-color: #555;color: #fff;padding: 3px 2px 3px 3px;border-radius: 14px 0 0 14px;font-family: DejaVu Sans,Verdana,Geneva,sans-serif;text-shadow: 0 1px 0 rgba(1, 1, 1, 0.3)', + 'background-color: #506eac;color: #fff;padding: 3px 3px 3px 2px;border-radius: 0 14px 14px 0;font-family: DejaVu Sans,Verdana,Geneva,sans-serif;text-shadow: 0 1px 0 rgba(1, 1, 1, 0.3)' +); \ No newline at end of file diff --git a/src/bubble-pop-up.ts b/src/bubble-pop-up.ts new file mode 100644 index 00000000..f503a93c --- /dev/null +++ b/src/bubble-pop-up.ts @@ -0,0 +1,67 @@ +import { version } from './var/version.ts'; +import { addUrlListener } from './tools/url-listener.ts'; +import { initializeContent, checkEditor, checkResources } from './tools/init.ts'; +import { handlePopUp } from './cards/pop-up.ts'; +import { bubblePopUpEditor } from './editor/bubble-pop-up-editor.ts'; + +let editor; +addUrlListener(); + +class BubblePopUp extends HTMLElement { + + set hass(hass) { + + this._hass = hass; + this.editor = editor; + + checkResources(hass); + + initializeContent(this); + + checkEditor(editor).then((value) => { + editor = value; + }); + + // Initialize pop-up card + handlePopUp(this); + } + + setConfig(config) { + if (!config.hash) { + throw new Error("You need to define an hash. Please note that this card must be placed inside a vertical_stack to work as a pop-up."); + } + this.config = config; + } + + getCardSize() { + // Fix the empty columns caused by the pop-ups on the dashboard + return -10000; + } + + static getConfigElement() { + return document.createElement("bubble-pop-up-editor"); + } +} + +let customElementObserver = new MutationObserver((mutationsList, observer) => { + if (customElements.get("ha-panel-lovelace")) { + customElements.define("bubble-pop-up", BubblePopUp); + observer.disconnect(); + } +}); + +customElementObserver.observe(document, { childList: true, subtree: true }); + +window.customCards = window.customCards || []; +window.customCards.push({ + type: "bubble-pop-up", + name: "Bubble Pop-up", + preview: false, + description: "Just add it in a vertical-stack first." +}); + +console.info( + `%c Bubble Card - Pop-up %c ${version} `, + 'background-color: #555;color: #fff;padding: 3px 2px 3px 3px;border-radius: 14px 0 0 14px;font-family: DejaVu Sans,Verdana,Geneva,sans-serif;text-shadow: 0 1px 0 rgba(1, 1, 1, 0.3)', + 'background-color: #506eac;color: #fff;padding: 3px 3px 3px 2px;border-radius: 0 14px 14px 0;font-family: DejaVu Sans,Verdana,Geneva,sans-serif;text-shadow: 0 1px 0 rgba(1, 1, 1, 0.3)' +); \ No newline at end of file diff --git a/src/cards/button.ts b/src/cards/button.ts new file mode 100644 index 00000000..cd191eeb --- /dev/null +++ b/src/cards/button.ts @@ -0,0 +1,353 @@ +import { + addStyles, + createIcon, + updateIcon, + isColorCloseToWhite, + convertToRGBA, + getIconColor, + getIconStyles +} from '../tools/style.ts'; +import { + initializeContent, + checkEditor, + checkResources +} from '../tools/init.ts'; +import { + fireEvent, + forwardHaptic, + navigate, + toggleEntity, + hasStateChanged +} from '../tools/utils.ts'; +import { addActions } from '../tools/tap-actions.ts'; +import { getVariables } from '../var/cards.ts'; + +export function handleButton(context) { + + const hass = context._hass; + const editor = context.editor; + + let { + customStyles, + entityId, + icon, + name, + widthDesktop, + widthDesktopDivided, + isSidebarHidden, + state, + stateChanged, + stateOn, + formatedState, + riseAnimation, + marginCenter, + popUpOpen, + rgbaColor, + rgbColor, + bgOpacity, + shadowOpacity, + bgBlur, + iconColorOpacity, + iconColor, + iconFilter, + iconStyles, + haStyle, + themeBgColor, + color, + } = getVariables(context, context.config, hass, editor); + + formatedState = stateChanged || editor ? hass.formatEntityState(hass.states[entityId]) : formatedState || ''; + const buttonType = context.config.button_type || 'switch'; + const showState = !context.config.show_state ? false : context.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; + + if (!context.buttonAdded) { + const buttonContainer = document.createElement("div"); + buttonContainer.setAttribute("class", "button-container"); + context.content.appendChild(buttonContainer); + } + + const iconContainer = document.createElement('div'); + iconContainer.setAttribute('class', 'icon-container'); + context.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 (!context.buttonContainer || editor) { + // Fix for editor mode + if (editor && context.buttonContainer) { + while (context.buttonContainer.firstChild) { + context.buttonContainer.removeChild(context.buttonContainer.firstChild); + } + context.eventAdded = false; + context.wasEditing = true; + } + // End of fix + + context.buttonContainer = context.content.querySelector(".button-container"); + + if (buttonType === 'slider' && (!context.buttonAdded || editor)) { + context.buttonContainer.appendChild(rangeSlider); + rangeSlider.appendChild(iconContainer); + rangeSlider.appendChild(nameContainer); + rangeSlider.appendChild(rangeFill); + context.rangeFill = context.content.querySelector(".range-fill"); + } else if (buttonType === 'switch' || buttonType === 'custom' || editor) { + context.buttonContainer.appendChild(switchButton); + switchButton.appendChild(iconContainer); + switchButton.appendChild(nameContainer); + context.switchButton = context.content.querySelector(".switch-button"); + } + + createIcon(context, hass, entityId, icon, iconContainer, editor); + nameContainer.innerHTML = ` +

${name}

+ ${!showState ? '' : `

${formatedState}

`} + `; + + context.buttonAdded = true; + } + + if (showState && formatedState) { + context.content.querySelector(".state").textContent = formatedState; + } + + function tapFeedback(content) { + forwardHaptic("success"); + 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); + } + + 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 && e.target !== iconContainer.querySelector('ha-icon')) { + isDragging = true; + document.addEventListener('mouseup', handleEnd, { passive: true }); + document.addEventListener('touchend', handleEnd, { passive: true }); + document.addEventListener('mousemove', checkVerticalScroll, { passive: true }); + document.addEventListener('touchmove', checkVerticalScroll, { passive: true }); + + // 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 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, { passive: true }); + document.addEventListener('touchmove', handleMove, { passive: true }); + } + } + + 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 (entityId.startsWith("media_player.")) { // && currentVolume !== volume + 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) { + forwardHaptic("light"); + 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 (!context.eventAdded && buttonType === 'switch') { + switchButton.addEventListener('click', () => tapFeedback(context.switchButton), { passive: true }); + switchButton.addEventListener('click', function(e) { + if (e.target !== iconContainer && e.target !== iconContainer.querySelector('ha-icon')) { + toggleEntity(hass, entityId); + } + }, { passive: true }); + addActions(iconContainer, context.config, hass, forwardHaptic); + context.eventAdded = true; + } else if (!context.eventAdded && buttonType === 'slider') { + rangeSlider.addEventListener('mousedown', handleStart, { passive: true }); + rangeSlider.addEventListener('touchstart', handleStart, { passive: true }); + addActions(iconContainer, context.config, hass, forwardHaptic); + context.eventAdded = true; + } else if (!context.eventAdded && buttonType === 'custom') { + switchButton.addEventListener('click', () => tapFeedback(context.switchButton), { passive: true }); + addActions(iconContainer, context.config, hass, forwardHaptic); + context.eventAdded = true; + } + + if (!context.isDragging && buttonType === 'slider') { + context.rangeFill.style.transition = 'all .3s'; + if (entityId.startsWith("light.")) { + context.rangeFill.style.transform = `translateX(${(currentBrightness / 255) * 100}%)`; + } else if (entityId.startsWith("media_player.")) { + context.rangeFill.style.transform = `translateX(${currentVolume * 100}%)`; + } + } + + 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}%)`; + } + + const buttonStyles = ` + ha-card { + margin-top: 0 !important; + background: none !important; + opacity: ${state !== 'unavailable' ? '1' : '0.5'}; + } + + .button-container { + position: relative; + width: 100%; + height: 50px; + z-index: 0; + background-color: var(--background-color-2,var(--secondary-background-color)); + border-radius: 25px; + mask-image: radial-gradient(white, black); + -webkit-mask-image: radial-gradient(white, black); + -webkit-backface-visibility: hidden; + -moz-backface-visibility: hidden; + -webkit-transform: translateZ(0); + overflow: hidden; + } + + .switch-button, + .range-slider { + display: inline-flex; + position: absolute; + height: 100%; + width: 100%; + transition: background-color 1.5s; + background-color: ${stateOn && ['switch', 'custom'].includes(buttonType) ? 'var(--accent-color)' : 'rgba(0,0,0,0)'}; + } + + .range-fill { + z-index: -1; + position: absolute; + top: 0; + bottom: 0; + left: 0; + background-color: ${iconColorOpacity}; + width: 100%; + left: -100%; + } + + .switch-button { + cursor: pointer !important; + } + + .range-slider { + cursor: ew-resize; + } + + .name-container { + position: relative; + display: ${!showState ? 'inline-flex' : 'block'}; + margin-left: 4px; + z-index: 1; + font-weight: 600; + align-items: center; + line-height: ${!showState ? '16px' : '4px'}; + padding-right: 16px; + } + + .state { + font-size: 12px; + opacity: 0.7; + } + + .feedback-element { + position: absolute; + top: 0; + left: 0; + opacity: 0; + width: 100%; + height: 100%; + background-color: rgb(0,0,0); + } + + @keyframes tap-feedback { + 0% {transform: translateX(-100%); opacity: 0;} + 64% {transform: translateX(0); opacity: 0.1;} + 100% {transform: translateX(100%); opacity: 0;} + } + + ${iconStyles} + `; + + addStyles(hass, context, buttonStyles, customStyles, state, entityId, stateChanged); +} \ No newline at end of file diff --git a/src/cards/cover.ts b/src/cards/cover.ts new file mode 100644 index 00000000..854c4a19 --- /dev/null +++ b/src/cards/cover.ts @@ -0,0 +1,207 @@ +import { + addStyles, + createIcon, + updateIcon, + isColorCloseToWhite, + convertToRGBA, + getIconColor, + getIconStyles +} from '../tools/style.ts'; +import { + initializeContent, + checkEditor, + checkResources +} from '../tools/init.ts'; +import { + fireEvent, + forwardHaptic, + navigate, + toggleEntity, + hasStateChanged +} from '../tools/utils.ts'; +import { addActions } from '../tools/tap-actions.ts'; +import { getVariables } from '../var/cards.ts'; + +export function handleCover(context) { + + const hass = context._hass; + const editor = context.editor; + const config = context.config; + + let { + customStyles, + entityId, + icon, + name, + widthDesktop, + widthDesktopDivided, + isSidebarHidden, + state, + stateChanged, + stateOn, + formatedState, + riseAnimation, + marginCenter, + popUpOpen, + rgbaColor, + rgbColor, + bgOpacity, + shadowOpacity, + bgBlur, + iconColorOpacity, + iconColor, + iconFilter, + iconStyles, + haStyle, + themeBgColor, + color, + } = getVariables(context, config, hass, editor); + + const iconOpen = config.icon_open ? config.icon_open : 'mdi:window-shutter-open'; + const iconClosed = config.icon_close ? config.icon_close : 'mdi:window-shutter' + const openCover = !config.open_service ? 'cover.open_cover' : config.open_service; + const closeCover = !config.close_service ? 'cover.close_cover' : config.close_service; + const stopCover = !config.stop_service ? 'cover.stop_cover' : config.stop_service; + const iconUp = config.icon_up ? config.icon_up : "mdi:arrow-up"; + const iconDown = config.icon_down ? config.icon_down : "mdi:arrow-down"; + const showState = !context.config.show_state ? false : context.config.show_state; + icon = hass.states[config.entity].state === 'open' ? iconOpen : iconClosed; + formatedState = stateChanged ? hass.formatEntityState(hass.states[entityId]) : formatedState || ''; + + if (!context.coverAdded || editor) { + // Fix for editor mode + if (editor && context.coverContainer) { + while (context.coverContainer.firstChild) { + context.coverContainer.removeChild(context.coverContainer.firstChild); + } + } + // End of fix + + context.coverContainer = document.createElement("div"); + + context.coverContainer.setAttribute("class", "cover-container"); + context.coverContainer.innerHTML = ` +
+
+
+
+

${name}

+

+
+
+
+ + + +
+ ` + context.content.appendChild(context.coverContainer); + + const openButton = context.coverContainer.querySelector('.open'); + const stopButton = context.coverContainer.querySelector('.stop'); + const closeButton = context.coverContainer.querySelector('.close'); + + openButton.addEventListener('click', () => { + hass.callService(openCover.split('.')[0], openCover.split('.')[1], { + entity_id: entityId + }); + }, { passive: true }); + stopButton.addEventListener('click', () => { + hass.callService(stopCover.split('.')[0], stopCover.split('.')[1], { + entity_id: entityId + }); + }, { passive: true }); + closeButton.addEventListener('click', () => { + hass.callService(closeCover.split('.')[0], closeCover.split('.')[1], { + entity_id: entityId + }); + }, { passive: true }); + + context.iconContainer = context.content.querySelector('.icon-container'); + addActions(context.iconContainer, config, hass, forwardHaptic); + + context.coverAdded = true; + } + + if (context.iconContainer && (stateChanged || editor)) { + context.iconContainer.innerHTML = ``; + context.content.querySelector(".state").textContent = showState ? formatedState : ''; + } + + const coverStyles = ` + ha-card { + margin-top: 0 !important; + background: none !important; + } + + .header-container { + display: flex; + align-items: center; + margin-bottom: 10px; + } + + .cover-container { + display: grid; + } + + .icon-container { + display: flex; + margin: 0 !important; + align-items: center; + justify-content: center; + cursor: pointer; + /*z-index: 1;*/ + width: 48px; + height: 48px; + margin: 6px; + border-radius: 50%; + background-color: var(--card-background-color,var(--ha-card-background)); + border: 6px solid var(--background-color-2,var(--secondary-background-color)); + box-sizing: border-box; + } + + .name-container { + font-weight: 600; + margin-left: 10px; + line-height: 4px; + } + + .buttons-container { + display: grid; + align-self: center; + grid-auto-flow: column; + grid-gap: 18px; + } + + .state { + font-size: 12px; + opacity: 0.7; + } + + ha-icon { + display: flex; + height: 24px; + width: 24px; + color: var(--primary-text-color); + } + + .button { + display: flex; + background: var(--background-color-2,var(--secondary-background-color)); + height: 42px; + border-radius: 32px; + align-items: center; + justify-content: center; + cursor: pointer; + border: none; + } + `; + + addStyles(hass, context, coverStyles, customStyles, state, entityId); +} \ No newline at end of file diff --git a/src/cards/empty-column.ts b/src/cards/empty-column.ts new file mode 100644 index 00000000..fe3768c3 --- /dev/null +++ b/src/cards/empty-column.ts @@ -0,0 +1,11 @@ +export function handleEmptyColumn(context) { + if (!context.emptyCollumnAdded) { + const separatorContainer = document.createElement("div"); + separatorContainer.setAttribute("class", "empty-column"); + separatorContainer.innerHTML = ` +
+ ` + context.content.appendChild(separatorContainer); + context.emptyColumnAdded = true; + } +} \ No newline at end of file diff --git a/src/cards/horizontal-buttons-stack.ts b/src/cards/horizontal-buttons-stack.ts new file mode 100644 index 00000000..68bae294 --- /dev/null +++ b/src/cards/horizontal-buttons-stack.ts @@ -0,0 +1,390 @@ +import { + addStyles, + createIcon, + updateIcon, + isColorCloseToWhite, + convertToRGBA, + getIconColor, + getIconStyles +} from '../tools/style.ts'; +import { + initializeContent, + checkEditor, + checkResources +} from '../tools/init.ts'; +import { + fireEvent, + forwardHaptic, + navigate, + toggleEntity, + hasStateChanged +} from '../tools/utils.ts'; +import { addActions } from '../tools/tap-actions.ts'; +import { getVariables } from '../var/cards.ts'; + +export function handleHorizontalButtonsStack(context) { + + const hass = context._hass; + const editor = context.editor; + + let { + customStyles, + entityId, + icon, + name, + widthDesktop, + widthDesktopDivided, + isSidebarHidden, + state, + stateChanged, + stateOn, + riseAnimation, + marginCenter, + popUpOpen, + rgbaColor, + rgbColor, + bgOpacity, + shadowOpacity, + bgBlur, + iconColorOpacity, + iconColor, + iconFilter, + iconStyles, + haStyle, + themeBgColor, + color, + } = getVariables(context, context.config, hass, editor); + + const createButton = (button, link, icon) => { + const buttonElement = document.createElement("button"); + buttonElement.setAttribute("class", `button ${link.substring(1)}`); + buttonElement.innerHTML = ` + ${icon !== '' ? `` : ''} + ${button !== '' ? `

${button}

` : ''} + `; + + if (!buttonElement.hasListener) { + buttonElement.addEventListener('click', (event) => { + event.stopPropagation(); + forwardHaptic("light"); + popUpOpen = location.hash + true; + const manuallyClosed = localStorage.getItem('isManuallyClosed_' + link) === 'true'; + if (popUpOpen !== link + true) { + navigate('', link); + popUpOpen = link + true; + } else { + history.replaceState(null, null, location.href.split('#')[0]); + popUpOpen = link + false; + } + }, { passive: true }); + + window.addEventListener('urlChanged', highlightButton, { passive: true }); + + buttonElement.hasListener = true; + } + + function highlightButton() { + if (context.config.highlightCurrentview) { + const isShown = location.pathname === link || location.hash === link; + if (isShown) { + buttonElement.classList.add("highlight"); + } else { + buttonElement.classList.remove("highlight"); + } + } + } + + return buttonElement; + }; + + if (!context.buttonsAdded) { + const buttonsContainer = document.createElement("div"); + buttonsContainer.classList.add("horizontal-buttons-stack-container"); + context.content.appendChild(buttonsContainer); + context.buttonsContainer = buttonsContainer; + } + + const updateButtonStyle = (buttonElement, lightEntity, buttonLink) => { + if (hass.states[lightEntity].attributes.rgb_color) { + const rgbColor = hass.states[lightEntity].attributes.rgb_color; + const rgbColorOpacity = (!isColorCloseToWhite(rgbColor) ? `rgba(${rgbColor}, 0.5)` : 'rgba(255,220,200, 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)'; + } + }; + + let buttonsList = []; + let i = 1; + while (context.config[i + '_link']) { + const prefix = i + '_'; + const button = context.config[prefix + 'name'] || ''; + const pirSensor = context.config[prefix + 'pir_sensor']; + icon = context.config[prefix + 'icon'] || ''; + const link = context.config[prefix + 'link']; + const lightEntity = context.config[prefix + 'entity']; + buttonsList.push({ + button, + pirSensor, + icon, + link, + lightEntity + }); + i++; + } + + if (context.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 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 (!context.buttonsAdded || editor) { + context.card.classList.add('horizontal-buttons-stack'); + + // Fix for editor mode + if (editor && context.buttonsContainer) { + while (context.buttonsContainer.firstChild) { + context.buttonsContainer.removeChild(context.buttonsContainer.firstChild); + } + localStorage.setItem('editorMode', true); + } else { + localStorage.setItem('editorMode', false); + } + // 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; + context.buttonsContainer.appendChild(buttonElement); + }); + context.buttonsAdded = true; + context.buttons = buttons; + } + + let currentPosition = 0; + let buttonMargin = 12; + + async function updateButtons(context) { + if (context.buttonsUpdated) { + return; + } + + let promises = []; + for (let button of buttonsList) { + let buttonElement = context.buttons[button.link]; + if (buttonElement) { + promises.push(localStorage.getItem(`buttonWidth-${button.link}`)); + promises.push(localStorage.getItem(`buttonContent-${button.link}`)); + } + } + let results = await Promise.all(promises); + let index = 0; + for (let button of buttonsList) { + let buttonElement = context.buttons[button.link]; + if (buttonElement) { + let buttonWidth = results[index]; + let buttonContent = results[index + 1]; + index += 2; + if (!buttonWidth || buttonWidth === '0' || buttonContent !== buttonElement.innerHTML || editor) { + buttonWidth = buttonElement.offsetWidth; + await localStorage.setItem(`buttonWidth-${button.link}`, buttonWidth); + await localStorage.setItem(`buttonContent-${button.link}`, buttonElement.innerHTML); + context.previousConfig = context.config; + } + buttonElement.style.transform = `translateX(${currentPosition}px)`; + currentPosition += parseInt(buttonWidth) + buttonMargin; + } + if (button.lightEntity) { + updateButtonStyle(buttonElement, button.lightEntity, button.link); + } + } + + context.buttonsAdded = true; + } + + updateButtons(context); + + const horizontalButtonsStackStyles = ` + ha-card { + border-radius: 0; + } + .horizontal-buttons-stack { + width: 100%; + margin-top: 0 !important; + background: none !important; + position: fixed; + height: 51px; + bottom: 16px; + left: ${marginCenter}; + z-index: 1 !important; /* Higher value hide the more-info panel */ + } + @keyframes from-bottom { + 0% {transform: translateY(200px);} + 20% {transform: translateY(200px);} + 46% {transform: translateY(-8px);} + 56% {transform: translateY(1px);} + 62% {transform: translateY(-2px);} + 70% {transform: translateY(0);} + 100% {transform: translateY(0);} + } + .horizontal-buttons-stack-container { + width: max-content; + position: relative; + height: 51px; + } + .button { + display: inline-flex; + position: absolute; + box-sizing: border-box !important; + border: 1px solid var(--primary-text-color); + align-items: center; + height: 50px; + line-height: 16px; + white-space: nowrap; + width: auto; + border-radius: 25px; + z-index: 1; + padding: 0 16px; + background: none; + transition: background-color 1s, border 1s, transform 1s; + color: var(--primary-text-color); + } + .highlight { + animation: pulse 1.4s infinite alternate; + } + @keyframes pulse { + 0% { + filter: brightness(0.7); + } + 100% { + filter: brightness(1.3); + } + } + .icon { + height: 24px; + } + .card-content { + width: calc(100% + 18px); + box-sizing: border-box !important; + margin: 0 -36px !important; + padding: 0 36px !important; + overflow: scroll !important; + -ms-overflow-style: none; + scrollbar-width: none; + -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: ''; + position: absolute; + top: -32px; + left: -100%; + display: block; + background: linear-gradient(0deg, var(--background-color, var(--primary-background-color)) 50%, rgba(79, 69, 87, 0)); + width: 200%; + height: 100px; + } + .card-content::-webkit-scrollbar { + display: none; + } + @media only screen and (min-width: 600px) { + .card-content { + position: fixed; + 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: calc(${widthDesktop}${widthDesktopDivided[2] === '%' && !isSidebarHidden ? ' - 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; + } + `; + + if (!window.hasAnimated && riseAnimation) { + context.content.style.animation = 'from-bottom 1.3s forwards'; + window.hasAnimated = true; + setTimeout(() => { + context.content.style.animation = 'none'; + }, 1500); + } + + addStyles(hass, context, horizontalButtonsStackStyles, customStyles); + + if (editor) { + context.buttonsContainer.classList.add('editor'); + context.card.classList.add('editor'); + } else { + context.buttonsContainer.classList.remove('editor'); + context.card.classList.remove('editor'); + } +} \ No newline at end of file diff --git a/src/cards/pop-up.ts b/src/cards/pop-up.ts new file mode 100644 index 00000000..8a6ad006 --- /dev/null +++ b/src/cards/pop-up.ts @@ -0,0 +1,647 @@ +import { + addStyles, + createIcon, + updateIcon, + isColorCloseToWhite, + convertToRGBA, + getIconColor, + getIconStyles +} from '../tools/style.ts'; +import { + initializeContent, + checkEditor, + checkResources +} from '../tools/init.ts'; +import { + fireEvent, + forwardHaptic, + navigate, + toggleEntity, + hasStateChanged +} from '../tools/utils.ts'; +import { addActions } from '../tools/tap-actions.ts'; +import { getVariables } from '../var/cards.ts'; + +export function handlePopUp(context) { + + const hass = context._hass; + const editor = context.editor; + const config = context.config; + + if (!hass) { + return; + } + + let { + customStyles, + entityId, + icon, + name, + widthDesktop, + widthDesktopDivided, + isSidebarHidden, + state, + stateChanged, + stateOn, + formatedState, + riseAnimation, + marginCenter, + popUpOpen, + rgbaColor, + rgbColor, + bgOpacity, + shadowOpacity, + bgBlur, + iconColorOpacity, + iconColor, + iconFilter, + iconStyles, + haStyle, + themeBgColor, + color, + } = getVariables(context, config, hass, editor); + + let autoClose = config.auto_close || false; + let popUpHash = config.hash; + let triggerEntity = config.trigger_entity ? config.trigger_entity : ''; + let triggerState = config.trigger_state ? config.trigger_state : ''; + let triggerClose = config.trigger_close ? config.trigger_close : false; + let startTouchY; + let lastTouchY; + + if (context.errorTriggered) { + return; + } + + if (!context.initStyleAdded && !context.host && !editor) { + // Hide vertical stack content before initialization + context.card.style.marginTop = '4000px'; + context.initStyleAdded = true; + } + + const createPopUp = () => { + if (!context.host) { + context.host = context.getRootNode().host; + } else { + if (!context.popUp) { + context.verticalStack = context.getRootNode(); + context.popUp = context.verticalStack.querySelector('#root'); + + if (!window.popUpInitialized && context.popUp) { + const backOpen = config.back_open || false; + backOpen ? localStorage.setItem('backOpen', true) : localStorage.setItem('backOpen', false); + const backOpenState = localStorage.getItem('backOpen') === 'true'; + + if (backOpenState) { + window.backOpen = true; + const event = new Event('popUpInitialized'); + setTimeout(() => { + window.dispatchEvent(event); + }, 0); + } else { + window.backOpen = false; + popUpOpen = popUpHash + false; + history.replaceState(null, null, location.href.split('#')[0]); + } + + window.popUpInitialized = true; + } + } + + const popUp = context.popUp; + const verticalStack = context.verticalStack; + const text = config.text || ''; + const stateEntityId = config.state; + formatedState = stateEntityId ? hass.formatEntityState(hass.states[stateEntityId]) : formatedState || ''; + const marginTopMobile = config.margin_top_mobile + ? (config.margin_top_mobile !== '0' ? config.margin_top_mobile : '0px') + : '0px'; + const marginTopDesktop = config.margin_top_desktop + ? (config.margin_top_desktop !== '0' ? config.margin_top_desktop : '0px') + : '0px'; + const displayPowerButton = config.entity ? 'flex' : 'none'; + state = stateEntityId ? hass.states[stateEntityId].state : ''; + let closeTimeout; + let rgbaBgColor; + + if (!context.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", "icon-container"); + div.appendChild(iconContainer); + + createIcon(context, hass, entityId, icon, iconContainer, editor); + addActions(iconContainer, config, hass, forwardHaptic); + + 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); + + context.content.appendChild(headerContainer); + context.header = div; + + context.headerAdded = true; + } else if (entityId) { + const iconContainer = context.content.querySelector("#header-container .icon-container"); + const h2 = context.content.querySelector("#header-container h2"); + const p = context.content.querySelector("#header-container p"); + const haIcon2 = context.content.querySelector("#header-container .power-button"); + + iconContainer.innerHTML = ''; // Clear the container + createIcon(context, hass, entityId, icon, iconContainer, editor); + + h2.textContent = name; + p.textContent = formatedState; + haIcon2.setAttribute("style", `display: ${displayPowerButton};`); + } + + if (!context.eventAdded && !editor) { + window['checkHashRef_' + popUpHash] = checkHash; + window.addEventListener('urlChanged', window['checkHashRef_' + popUpHash], { passive: true }); + window.addEventListener('click', function(e) { + // Reset auto close + location.hash === popUpHash && resetAutoClose(); + + if (!window.justOpened) { + return; + } + + const target = e.composedPath(); + + if (target && + !target.some(el => el.nodeName === 'HA-MORE-INFO-DIALOG') && + !target.some(el => el.id === 'root' && !el.classList.contains('close-pop-up')) && + popUpOpen === popUpHash + true) { + setTimeout(function() { + if (location.hash === popUpHash) { + popUpOpen = popUpHash + false; + history.replaceState(null, null, location.href.split('#')[0]); + localStorage.setItem('isManuallyClosed_' + popUpHash, true); + } + }, 2); + } + }, { passive: true }); + + context.eventAdded = true; + } + + function powerButtonClickHandler() { + toggleEntity(hass, entityId); + } + + function windowClickHandler(e) { + // Reset auto close + if (window.hash === popUpHash) { + resetAutoClose(); + } + + if (!window.justOpened) { + return; + } + + const target = e.composedPath(); + + if (target && + !target.some(el => el.nodeName === 'HA-MORE-INFO-DIALOG') && + !target.some(el => el.id === 'root' && !el.classList.contains('close-pop-up')) && + popUpOpen === popUpHash + true) { + popUpOpen = popUpHash + false; + history.replaceState(null, null, location.href.split('#')[0]); + localStorage.setItem('isManuallyClosed_' + popUpHash, true) + } + } + + function windowKeydownHandler(e) { + if (e.key === 'Escape') { + popUpOpen = popUpHash + false; + history.replaceState(null, null, location.href.split('#')[0]); + localStorage.setItem('isManuallyClosed_' + popUpHash, true) + } + } + + function popUpTouchstartHandler(event) { + // Reset auto close + if (window.hash === popUpHash) { + resetAutoClose(); + } + + // Record the Y position of the finger at the start of the touch + startTouchY = event.touches[0].clientY; + lastTouchY = startTouchY; + } + + function popUpTouchmoveHandler(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) { + popUpOpen = popUpHash + false; + history.replaceState(null, null, location.href.split('#')[0]); + popUpOpen = popUpHash + false; + localStorage.setItem('isManuallyClosed_' + popUpHash, true) + } + + // Update the Y position of the last touch + lastTouchY = event.touches[0].clientY; + } + + if (entityId) { + const rgbColor = hass.states[entityId].attributes.rgb_color; + context.rgbColor = rgbColor + ? (!isColorCloseToWhite(rgbColor) ? `rgb(${rgbColor})` : 'rgb(255,220,200)') + : (stateOn + ? (entityId.startsWith("light.") ? 'rgba(255,220,200, 0.5)' : 'var(--accent-color)') + : 'rgba(255, 255, 255, 1'); + + context.rgbColorOpacity = rgbColor + ? (!isColorCloseToWhite(rgbColor) ? `rgba(${rgbColor}, 0.5)` : 'rgba(255,220,200, 0.5)') + : (entityId && stateOn + ? (entityId.startsWith("light.") ? 'rgba(255,220,200, 0.5)' : 'var(--accent-color)') + : 'var(--background-color,var(--secondary-background-color))'); + + rgbaBgColor = convertToRGBA(color, 0); + + context.iconFilter = rgbColor ? + (!isColorCloseToWhite(rgbColor) ? 'brightness(1.1)' : 'none') : + 'none'; + } else { + rgbaBgColor = convertToRGBA(color, 0); + } + + function checkHash() { + if (!editor) { + window.hash = location.hash.split('?')[0]; + + // Open on hash change + if (window.hash === popUpHash) { + openPopUp(); + // Close on back button from browser + } else if (popUp.classList.contains('open-pop-up')) { + closePopUp(); + } + } + }; + + let content = context.content; + + function pauseVideos(root, pause) { + var videos = root.querySelectorAll('video'); + for (var i=0; i 0 && !videos[i].paused && !videos[i].ended && videos[i].readyState > videos[i].HAVE_CURRENT_DATA; + if (pause && isPlaying) { + videos[i].pause(); + } else if (!pause && !isPlaying) { + videos[i].play(); + if (videos[i].currentTime > 0) { + videos[i].currentTime = 10000; + } + } + } + + var nodes = root.querySelectorAll('*'); + for(var i=0; i { window.justOpened = true; }, 10); + resetAutoClose(); + }, 0); + + setTimeout(function() { + pauseVideos(popUp, false); + }, 0); + } + + function closePopUp() { + setTimeout(function() { + popUp.classList.remove('open-pop-up'); + popUp.classList.add('close-pop-up'); + content.querySelector('.power-button').removeEventListener('click', powerButtonClickHandler); + window.removeEventListener('keydown', windowKeydownHandler); + popUp.removeEventListener('touchstart', popUpTouchstartHandler); + popUp.removeEventListener('touchmove', popUpTouchmoveHandler); + popUpOpen = popUpHash + false; + document.body.style.overflow = ''; + window.justOpened = false; + clearTimeout(closeTimeout); + }, 0); + + setTimeout(function() { + pauseVideos(popUp, true); + }, 320); + } + + function resetAutoClose() { + // Clear any existing timeout + clearTimeout(closeTimeout); + // Start autoclose if enabled + if (autoClose > 0) { + closeTimeout = setTimeout(autoClosePopUp, autoClose); + } + } + + function autoClosePopUp(){ + history.replaceState(null, null, location.href.split('#')[0]); + } + + if (editor && !context.editorModeAdded) { + console.log(popUpHash); + popUp.classList.add('editor'); + popUp.classList.remove('open-pop-up'); + popUp.classList.remove('close-pop-up'); + context.editorModeAdded = true; + } + + const popUpStyles = ` + ha-card { + margin-top: 0 !important; + background: none !important; + border: none !important; + } + .card-content { + width: 100% !important; + padding: 0 !important; + } + #root { + transition: all 1s !important; + position: fixed !important; + margin: 0 -${marginCenter}; /* 7px */ + width: 100%; + background-color: ${rgbaColor}; + box-shadow: 0px 0px 50px rgba(0,0,0,${shadowOpacity / 100}); + backdrop-filter: blur(${bgBlur}px); + -webkit-backdrop-filter: blur(${bgBlur}px); + border-radius: 42px; + box-sizing: border-box; + top: calc(120% + ${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; */ + } + #root.hidden { + display: none !important; + } + #root > :first-child::after { + content: ''; + display: block; + position: sticky; + top: 0; + left: -50px; + margin: -70px 0 -36px -36px; + overflow: visible; + width: 200%; + height: 100px; + background: linear-gradient(0deg, ${rgbaBgColor} 0%, ${rgbaColor} 80%); + z-index: 0; + } + #root::-webkit-scrollbar { + display: none; /* for Chrome, Safari, and Opera */ + } + #root > :first-child { + position: sticky; + top: 0; + z-index: 1; + background: none !important; + overflow: visible; + } + #root.open-pop-up { + /*will-change: transform;*/ + transform: translateY(-120%); + transition: transform .36s !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(-20%); + transition: transform .4s !important; + box-shadow: none; + } + @media only screen and (min-width: 600px) { + #root { + top: calc(120% + ${marginTopDesktop} + var(--header-height)); + width: calc(${widthDesktop}${widthDesktopDivided[2] === '%' && !isSidebarHidden ? ' - var(--mdc-drawer-width)' : ''}) !important; + left: calc(50% - ${widthDesktopDivided[1] / 2}${widthDesktopDivided[2]}); + margin: 0 !important; + } + } + @media only screen and (min-width: 870px) { + #root { + left: calc(50% - ${widthDesktopDivided[1] / 2}${widthDesktopDivided[2]} + ${isSidebarHidden ? '0px' : `var(--mdc-drawer-width) ${widthDesktopDivided[2] === '%' ? '' : '/ 2'}`}); + } + } + #root.editor { + position: inherit !important; + width: 100% !important; + padding: 18px !important; + } + `; + + const headerStyles = ` + ${iconStyles} + + ha-card { + margin-top: 0 !important; + } + #header-container { + display: inline-flex; + ${!icon && !name && !entityId && !state && !text ? 'flex-direction: row-reverse;' : ''} + height: 50px; + width: 100%; + margin: 0; + padding: 0; + } + #header-container > div { + display: ${!icon && !name && !entityId && !state && !text ? 'none' : 'inline-flex'}; + align-items: center; + position: relative; + padding-right: 6px; + z-index: 1; + flex-grow: 1; + background-color: ${entityId ? context.rgbColorOpacity : 'var(--background-color,var(--secondary-background-color))'}; + transition: background 1s; + border-radius: 25px; + margin-right: 14px; + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + } + #header-container h2 { + display: inline-flex; + margin: 0 18px 0 0; + padding: 4px; + z-index: 1; + font-size: 18px; + } + #header-container p { + display: inline-flex; + font-size: 16px; + min-width: fit-content ; + } + .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; + } + `; + + setTimeout(() => { + addStyles(hass, context, headerStyles, customStyles, state, entityId, stateChanged); + addStyles(hass, context, popUpStyles, customStyles, state, entityId, context.bgColorChanged, '', popUp); + }, 0); + } + } + + // Change pop-ups background color if the theme just changed + + if (context.popUp && rgbaColor && rgbaColor !== context.oldBgColor && location.hash === popUpHash) { + context.oldBgColor = rgbaColor; + context.bgColorChanged = true; + } else { + context.bgColorChanged = false; + } + + // Initialize pop-up card + + if (!context.popUpAdded) { + context.popUpAdded = true; + let initPopUp = setInterval(() => { + createPopUp(); + if (context.popUp) { + clearInterval(initPopUp); + } + }, 0); + + setTimeout(() => { + if (!context.popUp) { + context.errorTriggered = true; + clearInterval(initPopUp); + throw new Error("Pop-up card must be placed inside a vertical_stack! If it's already the case, please ignore this error 🍻"); + } + }, 6000); + } else if (!editor && context.wasEditing && (stateChanged || context.bgColorChanged)) { + createPopUp(); + context.wasEditing = false; + } else if ((popUpHash === window.hash && (stateChanged || context.bgColorChanged)) || (editor && !context.editorModeAdded)) { + createPopUp(); + if (editor) { + context.wasEditing = true; + } + } + + // Hide pop-ups when exiting editor + + if (!editor && context.popUp && context.editorModeAdded) { + context.popUp.classList.remove('editor'); + context.editorModeAdded = false; + } + + // Pop-up triggers + + if (context.popUp && triggerEntity && stateChanged) { + if (localStorage.getItem('previousTriggerState_' + popUpHash) === null) { + localStorage.setItem('previousTriggerState_' + popUpHash, ''); + } + if (localStorage.getItem('isManuallyClosed_' + popUpHash) === null) { + localStorage.setItem('isManuallyClosed_' + popUpHash, 'false'); + } + if (localStorage.getItem('isTriggered_' + popUpHash) === null) { + localStorage.setItem('isTriggered_' + popUpHash, 'false'); + } + + 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 (hass.states[triggerEntity].state !== triggerState && triggerClose && context.popUp.classList.contains('open-pop-up') && isTriggered && !isManuallyClosed) { + history.replaceState(null, null, location.href.split('#')[0]); + popUpOpen = popUpHash + false; + isTriggered = false; + isManuallyClosed = true; + localStorage.setItem('isManuallyClosed_' + popUpHash, isManuallyClosed); + localStorage.setItem('isTriggered_' + popUpHash, isTriggered); + } + } +} \ No newline at end of file diff --git a/src/cards/separator.ts b/src/cards/separator.ts new file mode 100644 index 00000000..276c52e8 --- /dev/null +++ b/src/cards/separator.ts @@ -0,0 +1,122 @@ +import { + addStyles, + createIcon, + updateIcon, + isColorCloseToWhite, + convertToRGBA, + getIconColor, + getIconStyles +} from '../tools/style.ts'; +import { + initializeContent, + checkEditor, + checkResources +} from '../tools/init.ts'; +import { + fireEvent, + forwardHaptic, + navigate, + toggleEntity, + hasStateChanged +} from '../tools/utils.ts'; +import { addActions } from '../tools/tap-actions.ts'; +import { getVariables } from '../var/cards.ts'; + +export function handleSeparator(context) { + + const hass = context._hass; + const editor = context.editor; + const config = context.config; + + let { + customStyles, + entityId, + icon, + name, + widthDesktop, + widthDesktopDivided, + isSidebarHidden, + state, + stateChanged, + stateOn, + formatedState, + riseAnimation, + marginCenter, + popUpOpen, + rgbaColor, + rgbColor, + bgOpacity, + shadowOpacity, + bgBlur, + iconColorOpacity, + iconColor, + iconFilter, + iconStyles, + haStyle, + themeBgColor, + color, + } = getVariables(context, config, hass, editor); + + if (!context.separatorAdded || editor) { + // Fix for editor mode + if (editor && context.separatorContainer) { + while (context.separatorContainer.firstChild) { + context.separatorContainer.removeChild(context.separatorContainer.firstChild); + } + } + // End of fix + + if (!context.separatorAdded) { + context.separatorContainer = document.createElement("div"); + context.separatorContainer.setAttribute("class", "separator-container"); + } + context.separatorContainer.innerHTML = ` +
+ +

${name}

+
+
+ ` + context.content.appendChild(context.separatorContainer); + context.separatorAdded = true; + } + + const separatorStyles = ` + .separator-container { + display: inline-flex; + width: 100%; + margin-top: 12px; + } + .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 22px 0 8px; + transform: translateY(-2px); + } + .separator-container div h4 { + display: inline-flex; + margin: 0 20px 0 0; + font-size: 16px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .separator-container div:last-child { + display: inline-flex; + border-radius: 6px; + opacity: 0.5; + margin-left: 10px; + flex-grow: 1; + height: 6px; + align-self: center; + background-color: var(--background-color,var(--secondary-background-color)); + } + `; + + addStyles(hass, context, separatorStyles, customStyles); +} \ No newline at end of file diff --git a/src/editor/bubble-card-editor.ts b/src/editor/bubble-card-editor.ts new file mode 100644 index 00000000..1d502b66 --- /dev/null +++ b/src/editor/bubble-card-editor.ts @@ -0,0 +1,849 @@ +import { version } from '../var/version.ts'; +import { fireEvent } from '../tools/utils.ts'; + +const LitElement = Object.getPrototypeOf(customElements.get("ha-panel-lovelace")); +const html = LitElement.prototype.html; +const css = LitElement.prototype.css; + +export default class BubbleCardEditor extends LitElement { + + setConfig(config) { + this._config = { + ...config + }; + } + + static get properties() { + return { + hass: {}, + _config: {} + }; + } + + get _card_type() { + return this._config.card_type || ''; + } + + get _button_type() { + return this._config.button_type || 'switch'; + } + + get _entity() { + return this._config.entity || ''; + } + + get _name() { + return this._config.name || ''; + } + + get _icon() { + return this._config.icon || ''; + } + + get _state() { + return this._config.state || ''; + } + + 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 _trigger_close() { + return this._config.trigger_close || false; + } + + get _margin() { + return this._config.margin || '7px'; + } + + get _margin_top_mobile() { + return this._config.margin_top_mobile || '0px'; + } + + get _margin_top_desktop() { + return this._config.margin_top_desktop || '0px'; + } + + get _width_desktop() { + return this._config.width_desktop || '540px'; + } + + get _bg_color() { + return this._config.bg_color || window.color; + } + + get _bg_opacity() { + return this._config.bg_opacity !== undefined ? this._config.bg_opacity : '88'; + } + + get _bg_blur() { + return this._config.bg_blur !== undefined ? this._config.bg_blur : '14'; + } + + get _shadow_opacity() { + return this._config.shadow_opacity !== undefined ? this._config.shadow_opacity : '0'; + } + + get _is_sidebar_hidden() { + return this._config.is_sidebar_hidden || false; + } + + get _rise_animation() { + return this._config.rise_animation !== undefined ? this._config.rise_animation : true; + } + + get _auto_close() { + return this._config.auto_close || ''; + } + + get _back_open() { + return this._config.back_open || false; + } + + get _icon_open() { + return this._config.icon_open || ''; + } + + get _icon_close() { + return this._config.icon_close || ''; + } + + get _open_service() { + return this._config.open_service || 'cover.open_cover'; + } + + get _close_service() { + return this._config.open_service || 'cover.close_cover'; + } + + get _stop_service() { + return this._config.open_service || 'cover.stop_cover'; + } + + get _auto_order() { + return this._config.auto_order || false; + } + + get _highlightCurrentview() { + return this._config.highlightCurrentview || false; + } + + get _show_state() { + return this._config.show_state || false; + } + + render() { + if (!this.hass) { + return html``; + } + + if (!this.listsUpdated) { + const formateList = item => ({ + label: item, + value: item + }); + + this.allEntitiesList = Object.keys(this.hass.states).map(formateList); + + this.lightList = Object.keys(this.hass.states).filter( + (eid) => eid.substr(0, eid.indexOf(".")) === "light" + ).map(formateList); + + this.sensorList = Object.keys(this.hass.states).filter( + (eid) => eid.substr(0, eid.indexOf(".")) === "sensor" + ).map(formateList); + + this.binarySensorList = Object.keys(this.hass.states).filter( + (eid) => eid.substr(0, eid.indexOf(".")) === "binary_sensor" + ).map(formateList); + + this.coverList = Object.keys(this.hass.states).filter( + (eid) => eid.substr(0, eid.indexOf(".")) === "cover" + ).map(formateList); + + this.cardTypeList = [{ + 'label': 'Button', + 'value': 'button' + }, + { + 'label': 'Cover', + 'value': 'cover' + }, + { + 'label': 'Empty column', + 'value': 'empty-column' + }, + { + 'label': 'Horizontal buttons stack', + 'value': 'horizontal-buttons-stack' + }, + { + 'label': 'Pop-up', + 'value': 'pop-up' + }, + { + 'label': 'Separator', + 'value': 'separator' + } + ]; + + this.buttonTypeList = [{ + 'label': 'Switch', + 'value': 'switch' + }, + { + 'label': 'Slider', + 'value': 'slider' + } + ]; + + this.listsUpdated = true; + } + + const allEntitiesList = this.allEntitiesList; + const lightList = this.lightList; + const sensorList = this.sensorList; + const coverList = this.coverList; + const cardTypeList = this.cardTypeList; + const buttonTypeList = this.buttonTypeList; + + if (this._config.card_type === 'pop-up') { + return html` +
+ ${this.makeDropdown("Card type", "card_type", cardTypeList)} +

Pop-up + + Regular mode + +

+ 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.

How to get the optimized mode?
+ + + ${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 open 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

+ + + + + + +
+ +
+
+ +
+ + +
+
+ + +
+
+ + +
+ You can't set a value to 0 with the sliders for now, just change it to 0 in the text field if you need to. +

Advanced settings

+ + +
+ +
+
+ Back button/event support : This allow you to navigate through your pop-ups history when you press the back button of your browser. This setting can be applied only once, you don't need to change it in all pop-ups. If it's not working just turn it on for each pop-ups. + ${this.makeVersion()} +
+ `; + } else if (this._config.card_type === 'button') { + 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("Optional - Icon", "icon")} + ${this.makeVersion()} +
+ `; + } else if (this._config.card_type === 'separator') { + 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.makeDropdown("Icon", "icon")} + ${this.makeVersion()} +
+ `; + } else if (this._config.card_type === 'horizontal-buttons-stack') { + if (!this.buttonAdded && this.shadowRoot.querySelector("#add-button")) { + const addButton = this.shadowRoot.querySelector("#add-button"); + + this.buttonIndex = 0; + + while (this._config[(this.buttonIndex + 1) + '_link']) { + this.buttonIndex++; + } + + addButton.addEventListener("click", () => { + this.buttonIndex++; + + 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); + }, { passive: true }); + + this.buttonAdded = true; + } + + 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()} +
+ `; + } else if (this._config.card_type === 'cover') { + 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("Optional - Open icon", "icon_open")} + ${this.makeDropdown("Optional - Closed icon", "icon_close")} +

Styling options

+ ${this.makeDropdown("Optional - Arrow down icon", "icon_down")} + ${this.makeDropdown("Optional - Arrow up icon", "icon_up")} + ${this.makeVersion()} +
+ `; + } else if (this._config.card_type === 'empty-column') { + return html` +
+ ${this.makeDropdown("Card type", "card_type", cardTypeList)} +

Empty column

+ Just an empty card to fill any empty column. + ${this.makeVersion()} +
+ `; + } else if (!this._config.card_type) { + return html` +
+ ${this.makeDropdown("Card type", "card_type", cardTypeList)} + You need to add a card type first. + +

Almost everything is available in the GUI editor, but in the YAML editor you can add your own custom styles, create custom buttons or modify the tap actions of all cards. You can find more details on my GitHub page.

+ +

And if you like my project and want to support me, please consider making a donation. Any amount is welcome and very much appreciated! 🍻

+
+ + +
+ ${this.makeVersion()} +
+ `; + } + } + + makeDropdown(label, configValue, items) { + const hass = this.hass; + + if (label.includes('icon') || label.includes('Icon')) { + return html` +
+ +
+ `; + } else { + return html` +
+ +
+ `; + } + } + + makeButton() { + let buttons = []; + + for (let i = 1; i <= this.buttonIndex; i++) { + buttons.push(html` +
+
+ this.removeButton(i)}> + Button ${i} +
+ + + + + +
+ `); + } + return buttons; + } + + makeVersion() { + return html` +

+ Bubble Card + + ${version} + +

+ `; + } + + removeButton(index) { + // Removing button fields + delete this._config[index + '_name']; + delete this._config[index + '_icon']; + delete this._config[index + '_link']; + delete this._config[index + '_entity']; + delete this._config[index + '_pir_sensor']; + + // Updating indexes of following buttons + for (let i = index; i < this.buttonIndex; i++) { + this._config[i + '_name'] = this._config[(i + 1) + '_name']; + this._config[i + '_icon'] = this._config[(i + 1) + '_icon']; + this._config[i + '_link'] = this._config[(i + 1) + '_link']; + this._config[i + '_entity'] = this._config[(i + 1) + '_entity']; + this._config[i + '_pir_sensor'] = this._config[(i + 1) + '_pir_sensor']; + } + + // Removing fields of the last button + delete this._config[this.buttonIndex + '_name']; + delete this._config[this.buttonIndex + '_icon']; + delete this._config[this.buttonIndex + '_link']; + delete this._config[this.buttonIndex + '_entity']; + delete this._config[this.buttonIndex + '_pir_sensor']; + + // Updating index of the last button + this.buttonIndex--; + + fireEvent(this, "config-changed", { + config: this._config + }); + } + + // Working for sliders (setting to 0) but add more issues, to be fixed + // _valueChanged(ev) { + // if (!this._config || !this.hass) { + // return; + // } + // const target = ev.target; + // const detail = ev.detail; + // if (target.configValue) { + // this._config = { + // ...this._config, + // [target.configValue]: target.value !== undefined ? target.value : (target.checked !== undefined ? target.checked : detail.value), + // } + // } + // fireEvent(this, "config-changed", { + // config: this._config + // }); + // } + + _valueChanged(ev) { + if (!this._config || !this.hass) { + return; + } + const target = ev.target; + const detail = ev.detail; + if (target.configValue) { + if (target.type === 'ha-switch') { + this._config = { + ...this._config, + [target.configValue]: target.checked, + } + } else { + this._config = { + ...this._config, + [target.configValue]: target.checked !== undefined || !detail.value ? target.value || target.checked : target.checked || detail.value, + } + } + } + fireEvent(this, "config-changed", { + config: this._config + }); + } + + static get styles() { + return css` + div { + display: grid; + grid-gap: 12px; + } + #add-button { + height: 32px; + border-radius: 16px; + border: none; + background-color: var(--accent-color); + } + .button-header { + height: auto; + width: 100%; + display: inline-flex; + align-items: center; + + } + .button-number { + display: inline-flex; + width: auto; + } + .remove-button { + display: inline-flex; + border-radius: 50%; + width: 24px; + height: 24px; + text-align: center; + line-height: 24px; + vertical-align: middle; + cursor: pointer; + } + `; + } +} \ No newline at end of file diff --git a/src/editor/bubble-pop-up-editor.ts b/src/editor/bubble-pop-up-editor.ts new file mode 100644 index 00000000..1ebeae58 --- /dev/null +++ b/src/editor/bubble-pop-up-editor.ts @@ -0,0 +1,586 @@ +import { version } from '../var/version.ts'; +import { fireEvent } from '../tools/utils.ts'; + +let bubblePopUpEditor = new MutationObserver((mutationsList, observer) => { + if (customElements.get("ha-panel-lovelace")) { + + const LitElement = Object.getPrototypeOf(customElements.get("ha-panel-lovelace")); + const html = LitElement.prototype.html; + const css = LitElement.prototype.css; + + class BubblePopUpEditor extends LitElement { + + setConfig(config) { + this._config = { + ...config + }; + } + + static get properties() { + return { + hass: {}, + _config: {} + }; + } + + get _entity() { + return this._config.entity || ''; + } + + get _name() { + return this._config.name || ''; + } + + get _icon() { + return this._config.icon || ''; + } + + get _state() { + return this._config.state || ''; + } + + 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 _trigger_close() { + return this._config.trigger_close || false; + } + + get _margin() { + return this._config.margin || '7px'; + } + + get _margin_top_mobile() { + return this._config.margin_top_mobile || '0px'; + } + + get _margin_top_desktop() { + return this._config.margin_top_desktop || '0px'; + } + + get _width_desktop() { + return this._config.width_desktop || '540px'; + } + + get _bg_color() { + return this._config.bg_color || window.color; + } + + get _bg_opacity() { + return this._config.bg_opacity !== undefined ? this._config.bg_opacity : '88'; + } + + get _bg_blur() { + return this._config.bg_blur !== undefined ? this._config.bg_blur : '14'; + } + + get _shadow_opacity() { + return this._config.shadow_opacity !== undefined ? this._config.shadow_opacity : '0'; + } + + get _is_sidebar_hidden() { + return this._config.is_sidebar_hidden || false; + } + + get _auto_close() { + return this._config.auto_close || ''; + } + + get _back_open() { + return this._config.back_open || false; + } + + render() { + if (!this.hass) { + return html``; + } + + if (!this.listsUpdated) { + const formateList = item => ({ + label: item, + value: item + }); + + this.allEntitiesList = Object.keys(this.hass.states).map(formateList); + + this.lightList = Object.keys(this.hass.states).filter( + (eid) => eid.substr(0, eid.indexOf(".")) === "light" + ).map(formateList); + + this.sensorList = Object.keys(this.hass.states).filter( + (eid) => eid.substr(0, eid.indexOf(".")) === "sensor" + ).map(formateList); + + this.binarySensorList = Object.keys(this.hass.states).filter( + (eid) => eid.substr(0, eid.indexOf(".")) === "binary_sensor" + ).map(formateList); + + this.coverList = Object.keys(this.hass.states).filter( + (eid) => eid.substr(0, eid.indexOf(".")) === "cover" + ).map(formateList); + + this.cardTypeList = [{ + 'label': 'Button', + 'value': 'button' + }, + { + 'label': 'Cover', + 'value': 'cover' + }, + { + 'label': 'Empty column', + 'value': 'empty-column' + }, + { + 'label': 'Horizontal buttons stack', + 'value': 'horizontal-buttons-stack' + }, + { + 'label': 'Pop-up', + 'value': 'pop-up' + }, + { + 'label': 'Separator', + 'value': 'separator' + } + ]; + + this.buttonTypeList = [{ + 'label': 'Switch', + 'value': 'switch' + }, + { + 'label': 'Slider', + 'value': 'slider' + } + ]; + + this.listsUpdated = true; + } + + const allEntitiesList = this.allEntitiesList; + const lightList = this.lightList; + const sensorList = this.sensorList; + const coverList = this.coverList; + const cardTypeList = this.cardTypeList; + const buttonTypeList = this.buttonTypeList; + + return html` +
+

Pop-up + + 👍 Optimized mode + +

+ 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("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 open 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

+ + + + + + +
+ +
+
+ +
+ + +
+
+ + +
+
+ + +
+ You can't set a value to 0 with the sliders for now, just change it to 0 in the text field if you need to. +

Advanced settings

+ + +
+ +
+
+ Back button/event support : This allow you to navigate through your pop-ups history when you press the back button of your browser. This setting can be applied only once, you don't need to change it in all pop-ups. If it's not working just turn it on for each pop-ups. + ${this.makeVersion()} +
+ `; + } + + makeDropdown(label, configValue, items) { + const hass = this.hass; + + if (label.includes('icon') || label.includes('Icon')) { + return html` +
+ +
+ `; + } else { + return html` +
+ +
+ `; + } + } + + makeButton() { + let buttons = []; + + for (let i = 1; i <= this.buttonIndex; i++) { + buttons.push(html` +
+
+ this.removeButton(i)}> + Button ${i} +
+ + + + + +
+ `); + } + return buttons; + } + + makeVersion() { + return html` +

+ Bubble Card - Pop-up + + ${version} + +

+ `; + } + + removeButton(index) { + // Removing button fields + delete this._config[index + '_name']; + delete this._config[index + '_icon']; + delete this._config[index + '_link']; + delete this._config[index + '_entity']; + delete this._config[index + '_pir_sensor']; + + // Updating indexes of following buttons + for (let i = index; i < this.buttonIndex; i++) { + this._config[i + '_name'] = this._config[(i + 1) + '_name']; + this._config[i + '_icon'] = this._config[(i + 1) + '_icon']; + this._config[i + '_link'] = this._config[(i + 1) + '_link']; + this._config[i + '_entity'] = this._config[(i + 1) + '_entity']; + this._config[i + '_pir_sensor'] = this._config[(i + 1) + '_pir_sensor']; + } + + // Removing fields of the last button + delete this._config[this.buttonIndex + '_name']; + delete this._config[this.buttonIndex + '_icon']; + delete this._config[this.buttonIndex + '_link']; + delete this._config[this.buttonIndex + '_entity']; + delete this._config[this.buttonIndex + '_pir_sensor']; + + // Updating index of the last button + this.buttonIndex--; + + fireEvent(this, "config-changed", { + config: this._config + }); + } + + // Working for sliders (setting to 0) but add more issues, to be fixed + // _valueChanged(ev) { + // if (!this._config || !this.hass) { + // return; + // } + // const target = ev.target; + // const detail = ev.detail; + // if (target.configValue) { + // this._config = { + // ...this._config, + // [target.configValue]: target.value !== undefined ? target.value : (target.checked !== undefined ? target.checked : detail.value), + // } + // } + // fireEvent(this, "config-changed", { + // config: this._config + // }); + // } + + _valueChanged(ev) { + if (!this._config || !this.hass) { + return; + } + const target = ev.target; + const detail = ev.detail; + if (target.configValue) { + if (target.type === 'ha-switch') { + this._config = { + ...this._config, + [target.configValue]: target.checked, + } + } else { + this._config = { + ...this._config, + [target.configValue]: target.checked !== undefined || !detail.value ? target.value || target.checked : target.checked || detail.value, + } + } + } + fireEvent(this, "config-changed", { + config: this._config + }); + } + + static get styles() { + return css` + div { + display: grid; + grid-gap: 12px; + } + #add-button { + height: 32px; + border-radius: 16px; + border: none; + background-color: var(--accent-color); + } + .button-header { + height: auto; + width: 100%; + display: inline-flex; + align-items: center; + + } + .button-number { + display: inline-flex; + width: auto; + } + .remove-button { + display: inline-flex; + border-radius: 50%; + width: 24px; + height: 24px; + text-align: center; + line-height: 24px; + vertical-align: middle; + cursor: pointer; + } + `; + } + } + + customElements.define("bubble-pop-up-editor", BubblePopUpEditor); + observer.disconnect(); + } +}); + +bubblePopUpEditor.observe(document, { childList: true, subtree: true }); \ No newline at end of file diff --git a/src/tools/init.ts b/src/tools/init.ts new file mode 100644 index 00000000..d83fbf53 --- /dev/null +++ b/src/tools/init.ts @@ -0,0 +1,53 @@ +// Initialize the content if it's not there yet. + +export function initializeContent(context) { // Ajout du mot-clé async + if (!context.content) { + context.attachShadow({ + mode: 'open' + }); + context.shadowRoot.innerHTML = ` + +
+
+
+ `; + context.card = context.shadowRoot.querySelector("ha-card"); + context.content = context.shadowRoot.querySelector("div"); + } +} + +// Check for edit mode + +export async function checkEditor(editor) { + if (!window.editorElement) { + const editorElementPromise = new Promise((resolve) => { + resolve(document.querySelector("body > home-assistant") + .shadowRoot.querySelector("home-assistant-main") + .shadowRoot.querySelector("ha-drawer > partial-panel-resolver > ha-panel-lovelace") + .shadowRoot.querySelector("hui-root") + .shadowRoot.querySelector("div")); + }); + + window.editorElement = await editorElementPromise; + } else { + editor = window.editorElement.classList.contains('edit-mode'); + } + return editor; +} + +// Check if bubble-pop-up.js is installed as a resource and remove it (fix for the previous 1.5.0/1 users) + +export async function checkResources(hass) { + if (!window.resourcesChecked) { + window.resourcesChecked = true; + let resources = await hass.callWS({ type: "lovelace/resources" }); + let resource = resources.find(r => r.url.includes("bubble-pop-up.js")); + if (resource) { + await hass.callWS({ + type: "lovelace/resources/delete", + resource_id: resource.id + }); + } + } +} + diff --git a/src/tools/style.ts b/src/tools/style.ts new file mode 100644 index 00000000..bc659861 --- /dev/null +++ b/src/tools/style.ts @@ -0,0 +1,187 @@ +export const addStyles = function(hass, context, styles, customStyles, state, entityId, stateChanged, 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 || stateChanged || context.previousConfig !== context.config) { + if (!context[styleAddedKey]) { + // Check if the style element already exists + context.styleElement = element.querySelector('style'); //context.content + if (!context.styleElement) { + // If not, create a new style element + context.styleElement = document.createElement('style'); + const parentElement = (path ? element.querySelector(path) : element); + parentElement?.appendChild(context.styleElement); + } + context[styleAddedKey] = true; + } + + // Update the content of the existing style element only if styles have changed + if (context.styleElement.innerHTML !== customStylesEval + styles) { + context.styleElement.innerHTML = customStylesEval + styles; + } + + context.previousStyle = customStylesEval; // Store the current style + context.previousConfig = context.config; // Store the current config + } +} + +export function createIcon(context, hass, entityId, icon, iconContainer, editor) { + let entityAttributes = !entityId || !hass.states[entityId].attributes ? false : hass.states[entityId].attributes; + context.imageUrl = !entityAttributes.entity_picture ? false : entityAttributes.entity_picture; + updateIcon(context, hass, entityId, icon, iconContainer); + + if (editor) { + return; + } + + hass.connection.subscribeEvents((event) => { + if (event.data.entity_id === entityId) { + if (event.data.old_state && event.data.old_state.attributes.entity_picture !== event.data.new_state.attributes.entity_picture) { + context.imageUrl = event.data.new_state.attributes.entity_picture; + updateIcon(context, hass, entityId, icon, iconContainer); + } + } + }, 'state_changed'); +} + +export function updateIcon(context, hass, entityId, icon, iconContainer) { + while (iconContainer.firstChild) { + iconContainer.removeChild(iconContainer.firstChild); + } + + let imageUrl = context.config.icon && context.config.icon.includes('/') + ? context.config.icon + : context.imageUrl + ? context.imageUrl + : ''; + + function isPlayerActive(state) { + if (entityId.startsWith("media_player.")) { + const inactiveStates = ['off', 'unknown', 'idle', undefined]; + return !inactiveStates.includes(state); + } else { + return false; + } + } + + if (imageUrl && (isPlayerActive(hass.states[entityId].state) || !entityId.startsWith("media_player."))) { + const img = document.createElement("div"); + img.setAttribute("class", "entity-picture"); + img.setAttribute("alt", "Icon"); + if (iconContainer) { + iconContainer.appendChild(img); + iconContainer.style.background = "center / cover no-repeat url(" + imageUrl + "), var(--card-background-color,var(--ha-card-background))"; + } + } else { + const haIcon = document.createElement("ha-icon"); + haIcon.setAttribute("icon", icon); + haIcon.setAttribute("class", "icon"); + if (iconContainer) { + iconContainer.appendChild(haIcon); + } + } +} + +export function isColorCloseToWhite(rgbColor) { + let whiteThreshold = [220, 220, 190]; + for(let i = 0; i < 3; i++) { + if(rgbColor[i] < whiteThreshold[i]) { + return false; + } + } + return true; +} + +export function convertToRGBA(color, opacity, lighten = 1) { + let rgbaColor = ''; + if (color.startsWith('#')) { + if (color.length === 4) { // Short hexadecimal color + let r = Math.min(255, parseInt(color.charAt(1).repeat(2), 16) * lighten), + g = Math.min(255, parseInt(color.charAt(2).repeat(2), 16) * lighten), + b = Math.min(255, parseInt(color.charAt(3).repeat(2), 16) * lighten); + rgbaColor = "rgba(" + r + ", " + g + ", " + b + ", " + opacity + ")"; + } else { // Regular hexadecimal color + let r = Math.min(255, parseInt(color.slice(1, 3), 16) * lighten), + g = Math.min(255, parseInt(color.slice(3, 5), 16) * lighten), + b = Math.min(255, parseInt(color.slice(5, 7), 16) * lighten); + rgbaColor = "rgba(" + r + ", " + g + ", " + b + ", " + opacity + ")"; + } + } else if (color.startsWith('rgb')) { + let rgbValues = color.match(/\d+/g); + if (color.includes('rgba')) { // Color is already in RGBA + rgbaColor = "rgba(" + Math.min(255, rgbValues[0] * lighten) + ", " + Math.min(255, rgbValues[1] * lighten) + ", " + Math.min(255, rgbValues[2] * lighten) + ", " + opacity + ")"; + } else { // Color is in RGB + rgbaColor = "rgba(" + Math.min(255, rgbValues[0] * lighten) + ", " + Math.min(255, rgbValues[1] * lighten) + ", " + Math.min(255, rgbValues[2] * lighten) + ", " + opacity + ")"; + } + } + return rgbaColor; +} + +export function getIconColor(hass, entityId, stateOn, isColorCloseToWhite, rgbColor) { + let iconColorOpacity, iconColor, iconFilter; + + if (entityId && entityId.startsWith("light.")) { + rgbColor = hass.states[entityId].attributes.rgb_color; + iconColorOpacity = rgbColor + ? (!isColorCloseToWhite(rgbColor) ? `rgba(${rgbColor}, 0.5)` : 'rgba(255,220,200,0.5)') + : (stateOn ? 'rgba(255,220,200, 0.5)' : `rgba(255, 255, 255, 0.5)`); + iconColor = rgbColor + ? (!isColorCloseToWhite(rgbColor) ? `rgb(${rgbColor})` : 'rgb(255,220,200)') + : (stateOn ? 'rgba(255,220,200, 1)' : 'rgba(255, 255, 255, 1)'); + iconFilter = rgbColor ? + (!isColorCloseToWhite(rgbColor) ? 'brightness(1.1)' : 'none') : + 'none'; + } else { + iconColorOpacity = `var(--accent-color)`; + iconFilter = 'brightness(1.1)'; + } + + return { iconColorOpacity, iconColor, iconFilter }; +} + +export function getIconStyles(entityId, stateOn, iconColor, iconFilter) { + return ` + .icon-container { + position: relative; + display: flex; + flex-wrap: wrap; + align-content: center; + justify-content: center; + z-index: 1; + min-width: 38px; + min-height: 38px; + margin: 6px; + border-radius: 50%; + cursor: pointer !important; + background-color: var(--card-background-color,var(--ha-card-background)); + } + + .icon-container::after { + content: ''; + position: absolute; + display: block; + opacity: ${entityId.startsWith("light.") ? '0.2' : '0'}; + width: 100%; + height: 100%; + transition: all 1s; + border-radius: 50%; + background-color: ${stateOn ? (iconColor ? iconColor : 'var(--accent-color)') : 'var(--card-background-color,var(--ha-card-background))'}; + } + + .icon { + display: flex; + width: 22px; + color: ${stateOn ? (iconColor ? iconColor : 'var(--accent-color)') : 'inherit'} !important; + opacity: ${stateOn ? '1' : (entityId ? '0.6' : '1')}; + filter: ${stateOn ? (iconColor ? iconFilter : 'brightness(1.1)') : 'inherit'}; + } + + .entity-picture { + display: flex; + height: 38px; + width: 38px; + border-radius: 100%; + } + `; +} \ No newline at end of file diff --git a/src/tools/tap-actions.ts b/src/tools/tap-actions.ts new file mode 100644 index 00000000..28bd746c --- /dev/null +++ b/src/tools/tap-actions.ts @@ -0,0 +1,135 @@ +// Définir une fonction qui crée et envoie un événement hass-action avec la configuration et l'action à effectuer +export function sendActionEvent(element, config, action) { + const actionConfig = { + entity: config.entity, + tap_action: { + action: "more-info" + }, + double_tap_action: { + action: "toggle" + }, + hold_action: { + action: "toggle" + } + }; + + // Créer un événement de type hass-action avec les détails de la configuration et de l'action + const event = new Event('hass-action', { + bubbles: true, + composed: true, + }); + event.detail = { + config: actionConfig, + action: action, + }; + // Envoyer l'événement au gestionnaire d'événements de Home Assistant avec la méthode dispatchEvent + element.dispatchEvent(event); +} + +// Définir une fonction qui ajoute des actions à un élément en fonction de la configuration +export function addActions(element, config, hass, forwardHaptic) { + // Extraire les actions possibles de la configuration + let tap_action = config.tap_action; + let double_tap_action = config.double_tap_action; + let hold_action = config.hold_action; + + // Définir des variables pour compter le nombre de clics et le temps écoulé entre les clics + let clickCount = 0; + let startTime = 0; + let endTime = 0; + + // Définir une variable pour le timeout de l'action de maintien + let holdTimeout; + + // Ajouter un écouteur d'événement pour le clic sur l'élément + element.addEventListener('mousedown', () => { + // Enregistrer le temps de départ au moment du clic + startTime = Date.now(); + // Définir un timeout pour déclencher l'action de maintien après 300 ms + holdTimeout = setTimeout(() => { + // Envoyer l'événement de type hold + sendActionEvent(element, config, 'hold'); + }, 300); + }); + + element.addEventListener('mouseup', () => { + // Annuler le timeout de l'action de maintien + clearTimeout(holdTimeout); + // Enregistrer le temps de fin au moment où le bouton de la souris est relâché + endTime = Date.now(); + + // Si le temps écoulé entre le clic et le relâchement du bouton de la souris est inférieur à 300 ms, considérer comme un clic simple ou double + if (endTime - startTime < 300) { + clickCount++; + if (clickCount === 1) { + setTimeout(() => { + if (clickCount === 1) { + // Envoyer l'événement de type tap + sendActionEvent(element, config, 'tap'); + } else { + // Envoyer l'événement de type double_tap + sendActionEvent(element, config, 'double_tap'); + forwardHaptic("success"); + } + // Réinitialiser le compteur de clics + clickCount = 0; + }, 300); + } + } + // Réinitialiser les temps de départ et de fin + startTime = 0; + endTime = 0; + }); + + // Ajouter un écouteur d'événement pour le toucher sur l'élément + element.addEventListener('touchstart', (e) => { + forwardHaptic("light"); + // Enregistrer le temps de départ au moment du toucher + startTime = Date.now(); + // Définir un timeout pour déclencher l'action de maintien après 300 ms + holdTimeout = setTimeout(() => { + // Envoyer l'événement de type hold + sendActionEvent(element, config, 'hold'); + }, 300); + // Empêcher le comportement par défaut pour éviter un double événement + e.preventDefault(); + }); + + element.addEventListener('touchend', (e) => { + // Annuler le timeout de l'action de maintien + clearTimeout(holdTimeout); + // Enregistrer le temps de fin au moment où le toucher est relâché + endTime = Date.now(); + + // Si le temps écoulé entre le toucher et le relâchement du toucher est inférieur à 300 ms, considérer comme un toucher simple ou double + if (endTime - startTime < 300) { + clickCount++; + if (clickCount === 1) { + setTimeout(() => { + if (clickCount === 1) { + // Envoyer l'événement de type tap + sendActionEvent(element, config, 'tap'); + } else { + // Envoyer l'événement de type double_tap + sendActionEvent(element, config, 'double_tap'); + } + // Réinitialiser le compteur de clics + clickCount = 0; + }, 300); + } + } + // Réinitialiser les temps de départ et de fin + startTime = 0; + endTime = 0; + // Empêcher le comportement par défaut pour éviter un double événement + e.preventDefault(); + }); + + // Ajouter des écouteurs d'événements pour annuler l'action de maintien si l'utilisateur arrête de toucher ou de cliquer sur l'élément + element.addEventListener('mouseout', () => { + clearTimeout(holdTimeout); + }); + element.addEventListener('touchcancel', () => { + clearTimeout(holdTimeout); + }); +} diff --git a/src/tools/url-listener.ts b/src/tools/url-listener.ts new file mode 100644 index 00000000..75e3438c --- /dev/null +++ b/src/tools/url-listener.ts @@ -0,0 +1,38 @@ +export function addUrlListener() { + if (!window.eventAdded) { + + // 'urlChanged' custom event + + const event = new Event('urlChanged'); + window.popUpInitialized = false; + + ['click', 'mousedown', 'touchstart', 'focus', 'location-changed', 'connection-status'].forEach((eventType) => { + window.addEventListener(eventType, urlChanged); + }, { passive: true }); + + function urlChanged() { + let count = 0; + window.dispatchEvent(event); + + // Send more events for when the connexion was lost + const intervalId = setInterval(() => { + if (count < 10) { + window.dispatchEvent(event); + count++; + } else { + clearInterval(intervalId); + } + }, 1000); + } + + // Check url when pop-ups are initialized + const popUpInitialized = () => { + window.dispatchEvent(event); + window.addEventListener('popstate', urlChanged, { passive: true }); + }; + + window.addEventListener('popUpInitialized', popUpInitialized, { passive: true }); + + window.eventAdded = true; + } +} diff --git a/src/tools/utils.ts b/src/tools/utils.ts new file mode 100644 index 00000000..33efa16c --- /dev/null +++ b/src/tools/utils.ts @@ -0,0 +1,55 @@ +export function hasStateChanged(context, hass, entityId) { + context.hasState = hass.states[entityId]; + if (context.hasState) { + context.newState = [context.hasState.state, context.hasState.attributes.rgb_color]; + if (!context.oldState || context.newState[0] !== context.oldState[0] || context.newState[1] !== context.oldState[1]) { + context.oldState = context.newState; + context.stateChanged = true; + } else { + context.stateChanged = false; + } + + return context.stateChanged; + } +} + +export const fireEvent = (node, type, detail, options) => { + options = options || {}; + detail = detail === null || detail === undefined ? {} : detail; + const event = new Event(type, { + bubbles: options.bubbles === undefined ? true : options.bubbles, + cancelable: Boolean(options.cancelable), + composed: options.composed === undefined ? true : options.composed, + }); + event.detail = detail; + node.dispatchEvent(event); + return event; +}; + +export const forwardHaptic = hapticType => { + fireEvent(window, "haptic", hapticType) +} + +export const navigate = (_node, path, replace = false) => { + if (replace) { + history.replaceState(null, "", path) + } else { + history.pushState(null, "", path) + } + fireEvent(window, "location-changed", { + replace + }) +} + +export function toggleEntity(hass, entityId) { + hass.callService('homeassistant', 'toggle', { + entity_id: entityId + }); +} + + + + + + + diff --git a/src/var/cards.ts b/src/var/cards.ts new file mode 100644 index 00000000..e8b17fe9 --- /dev/null +++ b/src/var/cards.ts @@ -0,0 +1,60 @@ +// Cards variables + +import { + isColorCloseToWhite, + convertToRGBA, + getIconColor, + getIconStyles +} from '../tools/style.ts'; +import { hasStateChanged } from '../tools/utils.ts'; + +let popUpOpen; +let rgbaColor; +let rgbColor; +let formatedState; + +export function getVariables(context, config, hass, editor) { + let customStyles = !config.styles ? '' : config.styles; + let entityId = config.entity && hass.states[config.entity] + ? config.entity + : ''; + let icon = !config.icon && config.entity + ? hass.states[entityId].attributes.icon || hass.states[entityId].attributes.entity_picture || '' + : config.icon || ''; + let name = config.name + ? config.name + : config.entity + ? hass.states[entityId].attributes.friendly_name + : ''; + let widthDesktop = config.width_desktop || '540px'; + let widthDesktopDivided = widthDesktop ? widthDesktop.match(/(\d+)(\D+)/) : ''; + let isSidebarHidden = config.is_sidebar_hidden || false; + let state = entityId ? hass.states[entityId].state : ''; + hasStateChanged(context, hass, entityId); + let stateChanged = context.stateChanged; + let stateOn = ['on', 'open', 'cleaning', 'true', 'home', 'playing'].includes(state) || (Number(state) !== 0 && !isNaN(Number(state))); + let riseAnimation = config.rise_animation !== undefined ? config.rise_animation : true; + let marginCenter = config.margin + ? (config.margin !== '0' ? config.margin : '0px') + : '7px'; + let bgOpacity = config.bg_opacity !== undefined ? config.bg_opacity : '88'; + let shadowOpacity = config.shadow_opacity !== undefined ? config.shadow_opacity : '0'; + let bgBlur = config.bg_blur !== undefined ? config.bg_blur : '10'; + let { + iconColorOpacity, + iconColor, + iconFilter + } = getIconColor(hass, entityId, stateOn, isColorCloseToWhite); + let iconStyles = getIconStyles(entityId, stateOn, iconColor, iconFilter); + let haStyle = getComputedStyle(document.body); + let themeBgColor = haStyle.getPropertyValue('--ha-card-background') || haStyle.getPropertyValue('--card-background-color'); + let color = config.bg_color ? config.bg_color : themeBgColor; + if (color && (!context.color || color !== context.color)) { + const lighten = 1.02; + rgbaColor = convertToRGBA(color, (bgOpacity / 100), lighten); + context.color = color; + window.color = color; + } + + return { customStyles, entityId, icon, name, widthDesktop, widthDesktopDivided, isSidebarHidden, state, stateChanged, stateOn, formatedState, riseAnimation, marginCenter, popUpOpen, rgbaColor, rgbColor, bgOpacity, shadowOpacity, bgBlur, iconColorOpacity, iconColor, iconFilter, iconStyles, haStyle, themeBgColor, color }; +} diff --git a/src/var/version.ts b/src/var/version.ts new file mode 100644 index 00000000..dae66f81 --- /dev/null +++ b/src/var/version.ts @@ -0,0 +1 @@ +export var version = 'v1.6.0-beta.1'; \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 00000000..3ba57e4d --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,28 @@ +const webpack = require ('webpack'); +const path = require('path'); + +module.exports = [ + { + mode: 'production', + entry: { + 'bubble-card': './src/bubble-card.ts', + 'bubble-pop-up': './src/bubble-pop-up.ts', + }, + output: { + path: path.resolve(__dirname, 'dist'), + filename: '[name].js', + }, + }, + { + mode: 'production', + entry: { + 'bubble-card': './src/bubble-card.ts', + 'bubble-pop-up': './src/bubble-pop-up.ts', + }, + output: { + path: path.resolve('/Volumes/config/www'), + filename: '[name].js', + }, + } +]; +