324 lines
9.4 KiB
JavaScript
324 lines
9.4 KiB
JavaScript
console.log(
|
|
`%cvertical-stack-in-card\n%cVersion: ${'1.1.2'}`,
|
|
'color: #1976d2; font-weight: bold;',
|
|
'',
|
|
);
|
|
|
|
class VerticalStackInCardEditor extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this._config = {};
|
|
this._initialized = false;
|
|
}
|
|
|
|
async setConfig(config) {
|
|
this._config = config;
|
|
if (!this._initialized) {
|
|
await this._build();
|
|
} else {
|
|
this._sync();
|
|
}
|
|
}
|
|
|
|
async _build() {
|
|
this._initialized = true;
|
|
|
|
// Load the hui-vertical-stack-card editor for title + cards UI.
|
|
let cls = customElements.get('hui-vertical-stack-card');
|
|
if (!cls) {
|
|
const helpers = await window.loadCardHelpers();
|
|
helpers.createCardElement({ type: 'vertical-stack', cards: [] });
|
|
await customElements.whenDefined('hui-vertical-stack-card');
|
|
cls = customElements.get('hui-vertical-stack-card');
|
|
}
|
|
this._huiEditor = await cls.getConfigElement();
|
|
this._huiEditor.addEventListener('config-changed', (ev) => {
|
|
// Only intercept the final top-level event from the hui editor itself.
|
|
// Intermediate events (from child card editors, the card picker, etc.)
|
|
// must propagate so HA's internals keep working.
|
|
if (ev.detail.config.type !== 'custom:vertical-stack-in-card') return;
|
|
ev.stopPropagation();
|
|
this._fireConfigChanged({ ...this._config, ...ev.detail.config });
|
|
});
|
|
|
|
// Horizontal toggle row.
|
|
const switchContainer = document.createElement('div');
|
|
switchContainer.style.cssText =
|
|
'display:flex;align-items:center;justify-content:space-between;padding:8px 0;';
|
|
const label = document.createElement('span');
|
|
label.textContent = 'Stack horizontally';
|
|
this._horizontalSwitch = document.createElement('ha-switch');
|
|
this._horizontalSwitch.addEventListener('change', () => {
|
|
const config = { ...this._config };
|
|
if (this._horizontalSwitch.checked) {
|
|
config.horizontal = true;
|
|
} else {
|
|
delete config.horizontal;
|
|
}
|
|
this._fireConfigChanged(config);
|
|
});
|
|
switchContainer.appendChild(label);
|
|
switchContainer.appendChild(this._horizontalSwitch);
|
|
this.appendChild(switchContainer);
|
|
this.appendChild(this._huiEditor);
|
|
|
|
this._sync();
|
|
}
|
|
|
|
set hass(hass) {
|
|
this._hass = hass;
|
|
if (this._huiEditor) this._huiEditor.hass = hass;
|
|
}
|
|
|
|
// HA sets lovelace on the config element — forward it into the hui editor
|
|
// so hui-cards-editor and hui-card-picker receive it and work correctly.
|
|
set lovelace(lovelace) {
|
|
this._lovelace = lovelace;
|
|
if (this._huiEditor) this._huiEditor.lovelace = lovelace;
|
|
}
|
|
|
|
_sync() {
|
|
if (this._huiEditor) {
|
|
if (this._hass) this._huiEditor.hass = this._hass;
|
|
if (this._lovelace) this._huiEditor.lovelace = this._lovelace;
|
|
this._huiEditor.setConfig({
|
|
type: this._config.type,
|
|
title: this._config.title,
|
|
cards: this._config.cards || [],
|
|
});
|
|
}
|
|
if (this._horizontalSwitch) {
|
|
this._horizontalSwitch.checked = !!this._config.horizontal;
|
|
}
|
|
}
|
|
|
|
_fireConfigChanged(config) {
|
|
this._config = config;
|
|
this.dispatchEvent(
|
|
new CustomEvent('config-changed', {
|
|
detail: { config },
|
|
bubbles: true,
|
|
composed: true,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
customElements.define(
|
|
'vertical-stack-in-card-editor',
|
|
VerticalStackInCardEditor,
|
|
);
|
|
|
|
class VerticalStackInCard extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
}
|
|
|
|
get updateComplete() {
|
|
return this._cardSize.promise;
|
|
}
|
|
|
|
setConfig(config) {
|
|
this._cardSize = {};
|
|
this._cardSize.promise = new Promise(
|
|
(resolve) => (this._cardSize.resolve = resolve),
|
|
);
|
|
|
|
if (!config || !config.cards || !Array.isArray(config.cards)) {
|
|
throw new Error('Card config incorrect');
|
|
}
|
|
this._config = config;
|
|
this._refCards = [];
|
|
this.renderCard();
|
|
}
|
|
|
|
async renderCard() {
|
|
const config = this._config;
|
|
const promises = config.cards.map((config) =>
|
|
this._createCardElement(config),
|
|
);
|
|
this._refCards = await Promise.all(promises);
|
|
|
|
// Style cards
|
|
this._refCards.forEach((card) => {
|
|
if (card.updateComplete) {
|
|
card.updateComplete.then(() => this._styleCard(card));
|
|
} else {
|
|
this._styleCard(card);
|
|
}
|
|
});
|
|
|
|
// Create the card
|
|
const card = document.createElement('ha-card');
|
|
const cardContent = document.createElement('div');
|
|
card.header = config.title;
|
|
card.style.overflow = 'visible';
|
|
cardContent.style.display = 'flex';
|
|
// height: 100% is only appropriate for horizontal stacks, where the card
|
|
// should fill the grid-allocated row height. For vertical stacks, height
|
|
// must be content-driven (auto) — otherwise child cards that also set
|
|
// height: 100% (e.g. hui-horizontal-stack-card after HA 2025.x) will
|
|
// resolve their 100% against our container height and inflate to fill it.
|
|
if (config.horizontal) {
|
|
card.style.height = '100%';
|
|
cardContent.style.height = '100%';
|
|
}
|
|
|
|
this._refCards.forEach((refCard) => cardContent.appendChild(refCard));
|
|
|
|
if (config.horizontal) {
|
|
// Proportional widths from grid_options.columns (native stack gives equal
|
|
// flex: 1 1 0 to all; we use columns as relative flex weights).
|
|
this._refCards.forEach((refCard, i) => {
|
|
const cols = config.cards[i]?.grid_options?.columns ?? 1;
|
|
refCard.style.flex = `${cols} ${cols} 0`;
|
|
refCard.style.minWidth = '0';
|
|
});
|
|
} else {
|
|
cardContent.style.flexDirection = 'column';
|
|
// Proportional heights from grid_options.rows; cards without rows are
|
|
// content-sized (flex: 0 0 auto).
|
|
this._refCards.forEach((refCard, i) => {
|
|
const rows = config.cards[i]?.grid_options?.rows;
|
|
refCard.style.flex = rows ? `${rows} ${rows} 0` : '0 0 auto';
|
|
});
|
|
}
|
|
|
|
card.appendChild(cardContent);
|
|
|
|
const shadowRoot = this.shadowRoot || this.attachShadow({ mode: 'open' });
|
|
while (shadowRoot.hasChildNodes()) {
|
|
shadowRoot.removeChild(shadowRoot.lastChild);
|
|
}
|
|
shadowRoot.appendChild(card);
|
|
|
|
// Calculate card size
|
|
this._cardSize.resolve();
|
|
}
|
|
|
|
async _createCardElement(cardConfig) {
|
|
const helpers = await window.loadCardHelpers();
|
|
const element =
|
|
cardConfig.type === 'divider'
|
|
? helpers.createRowElement(cardConfig)
|
|
: helpers.createCardElement(cardConfig);
|
|
|
|
element.hass = this._hass;
|
|
element.addEventListener(
|
|
'll-rebuild',
|
|
(ev) => {
|
|
ev.stopPropagation();
|
|
this._createCardElement(cardConfig).then(() => {
|
|
this.renderCard();
|
|
});
|
|
},
|
|
{ once: true },
|
|
);
|
|
return element;
|
|
}
|
|
|
|
set hass(hass) {
|
|
this._hass = hass;
|
|
if (this._refCards) {
|
|
this._refCards.forEach((card) => {
|
|
card.hass = hass;
|
|
});
|
|
}
|
|
}
|
|
|
|
_styleCard(element) {
|
|
const config = this._config;
|
|
if (element.shadowRoot) {
|
|
if (element.shadowRoot.querySelector('ha-card')) {
|
|
let ele = element.shadowRoot.querySelector('ha-card');
|
|
ele.style.boxShadow = 'none';
|
|
ele.style.border = 'none';
|
|
if ('styles' in config) {
|
|
Object.entries(config.styles).forEach(([key, value]) =>
|
|
ele.style.setProperty(key, value),
|
|
);
|
|
}
|
|
} else {
|
|
let searchEles = element.shadowRoot.getElementById('root');
|
|
if (!searchEles) {
|
|
searchEles = element.shadowRoot.getElementById('card');
|
|
}
|
|
if (!searchEles) return;
|
|
searchEles = searchEles.childNodes;
|
|
for (let i = 0; i < searchEles.length; i++) {
|
|
if (searchEles[i].style) {
|
|
searchEles[i].style.margin = '0px';
|
|
}
|
|
this._styleCard(searchEles[i]);
|
|
}
|
|
}
|
|
} else {
|
|
if (
|
|
typeof element.querySelector === 'function' &&
|
|
element.querySelector('ha-card')
|
|
) {
|
|
let ele = element.querySelector('ha-card');
|
|
ele.style.boxShadow = 'none';
|
|
ele.style.borderRadius = '0';
|
|
ele.style.border = 'none';
|
|
if ('styles' in config) {
|
|
Object.entries(config.styles).forEach(([key, value]) =>
|
|
ele.style.setProperty(key, value),
|
|
);
|
|
}
|
|
}
|
|
let searchEles = element.childNodes;
|
|
for (let i = 0; i < searchEles.length; i++) {
|
|
if (searchEles[i] && searchEles[i].style) {
|
|
searchEles[i].style.margin = '0px';
|
|
}
|
|
this._styleCard(searchEles[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
_computeCardSize(card) {
|
|
if (typeof card.getCardSize === 'function') {
|
|
return card.getCardSize();
|
|
}
|
|
return customElements
|
|
.whenDefined(card.localName)
|
|
.then(() => this._computeCardSize(card))
|
|
.catch(() => 1);
|
|
}
|
|
|
|
async getCardSize() {
|
|
await this._cardSize.promise;
|
|
const sizes = await Promise.all(this._refCards.map(this._computeCardSize));
|
|
return sizes.reduce((a, b) => a + b, 0);
|
|
}
|
|
|
|
getGridOptions() {
|
|
return {
|
|
columns: 12,
|
|
rows: 'auto',
|
|
min_columns: 3,
|
|
};
|
|
}
|
|
|
|
static getConfigElement() {
|
|
return document.createElement('vertical-stack-in-card-editor');
|
|
}
|
|
|
|
static getStubConfig() {
|
|
return {
|
|
cards: [],
|
|
};
|
|
}
|
|
}
|
|
|
|
customElements.define('vertical-stack-in-card', VerticalStackInCard);
|
|
window.customCards = window.customCards || [];
|
|
window.customCards.push({
|
|
type: 'vertical-stack-in-card',
|
|
name: 'Vertical Stack In Card',
|
|
description: 'Group multiple cards into a single sleek card.',
|
|
preview: false,
|
|
documentationURL: 'https://github.com/ofekashery/vertical-stack-in-card',
|
|
});
|