Files
2026-08-24 21:55:33 +08:00

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',
});