Skip to content

Commit b5ad847

Browse files
authored
Merge pull request #1214 from tomvanswam/feature/v3.2.0
Feature/v3.2.0
2 parents d0e1b98 + 9ceb85d commit b5ad847

10 files changed

Lines changed: 533 additions & 38 deletions

File tree

.hass_dev/www/stack-in-card.js

Lines changed: 208 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
console.log(
2+
`%cvertical-stack-in-card\n%cVersion: ${'1.0.1'}`,
3+
'color: #1976d2; font-weight: bold;',
4+
''
5+
);
6+
7+
class VerticalStackInCard extends HTMLElement {
8+
constructor() {
9+
super();
10+
}
11+
12+
setConfig(config) {
13+
this._cardSize = {};
14+
this._cardSize.promise = new Promise(
15+
(resolve) => (this._cardSize.resolve = resolve)
16+
);
17+
18+
if (!config || !config.cards || !Array.isArray(config.cards)) {
19+
throw new Error('Card config incorrect');
20+
}
21+
this._config = config;
22+
this._refCards = [];
23+
this.renderCard();
24+
}
25+
26+
async renderCard() {
27+
const config = this._config;
28+
const promises = config.cards.map((config) =>
29+
this._createCardElement(config)
30+
);
31+
this._refCards = await Promise.all(promises);
32+
33+
// Style cards
34+
this._refCards.forEach((card) => {
35+
if (card.updateComplete) {
36+
card.updateComplete.then(() => this._styleCard(card));
37+
} else {
38+
this._styleCard(card);
39+
}
40+
});
41+
42+
// Create the card
43+
const card = document.createElement('ha-card');
44+
const cardContent = document.createElement('div');
45+
card.header = config.title;
46+
card.style.overflow = 'hidden';
47+
this._refCards.forEach((card) => cardContent.appendChild(card));
48+
if (config.horizontal) {
49+
cardContent.style.display = 'flex';
50+
cardContent.childNodes.forEach((card) => {
51+
card.style.flex = '1 1 0';
52+
card.style.minWidth = 0;
53+
});
54+
}
55+
card.appendChild(cardContent);
56+
57+
const shadowRoot = this.shadowRoot || this.attachShadow({ mode: 'open' });
58+
while (shadowRoot.hasChildNodes()) {
59+
shadowRoot.removeChild(shadowRoot.lastChild);
60+
}
61+
shadowRoot.appendChild(card);
62+
63+
// Calculate card size
64+
this._cardSize.resolve();
65+
}
66+
67+
async _createCardElement(cardConfig) {
68+
const helpers = await window.loadCardHelpers();
69+
const element =
70+
cardConfig.type === 'divider'
71+
? helpers.createRowElement(cardConfig)
72+
: helpers.createCardElement(cardConfig);
73+
74+
element.hass = this._hass;
75+
element.addEventListener(
76+
'll-rebuild',
77+
(ev) => {
78+
ev.stopPropagation();
79+
this._createCardElement(cardConfig).then(() => {
80+
this.renderCard();
81+
});
82+
},
83+
{ once: true }
84+
);
85+
return element;
86+
}
87+
88+
set hass(hass) {
89+
this._hass = hass;
90+
if (this._refCards) {
91+
this._refCards.forEach((card) => {
92+
card.hass = hass;
93+
});
94+
}
95+
}
96+
97+
_styleCard(element) {
98+
const config = this._config;
99+
if (element.shadowRoot) {
100+
if (element.shadowRoot.querySelector('ha-card')) {
101+
let ele = element.shadowRoot.querySelector('ha-card');
102+
ele.style.boxShadow = 'none';
103+
ele.style.borderRadius = '0';
104+
ele.style.border = 'none';
105+
if ('styles' in config) {
106+
Object.entries(config.styles).forEach(([key, value]) =>
107+
ele.style.setProperty(key, value)
108+
);
109+
}
110+
} else {
111+
let searchEles = element.shadowRoot.getElementById('root');
112+
if (!searchEles) {
113+
searchEles = element.shadowRoot.getElementById('card');
114+
}
115+
if (!searchEles) return;
116+
searchEles = searchEles.childNodes;
117+
for (let i = 0; i < searchEles.length; i++) {
118+
if (searchEles[i].style) {
119+
searchEles[i].style.margin = '0px';
120+
}
121+
this._styleCard(searchEles[i]);
122+
}
123+
}
124+
} else {
125+
if (
126+
typeof element.querySelector === 'function' &&
127+
element.querySelector('ha-card')
128+
) {
129+
let ele = element.querySelector('ha-card');
130+
ele.style.boxShadow = 'none';
131+
ele.style.borderRadius = '0';
132+
ele.style.border = 'none';
133+
if ('styles' in config) {
134+
Object.entries(config.styles).forEach(([key, value]) =>
135+
ele.style.setProperty(key, value)
136+
);
137+
}
138+
}
139+
let searchEles = element.childNodes;
140+
for (let i = 0; i < searchEles.length; i++) {
141+
if (searchEles[i] && searchEles[i].style) {
142+
searchEles[i].style.margin = '0px';
143+
}
144+
this._styleCard(searchEles[i]);
145+
}
146+
}
147+
}
148+
149+
_computeCardSize(card) {
150+
if (typeof card.getCardSize === 'function') {
151+
return card.getCardSize();
152+
}
153+
return customElements
154+
.whenDefined(card.localName)
155+
.then(() => this._computeCardSize(card))
156+
.catch(() => 1);
157+
}
158+
159+
async getCardSize() {
160+
await this._cardSize.promise;
161+
const sizes = await Promise.all(this._refCards.map(this._computeCardSize));
162+
return sizes.reduce((a, b) => a + b, 0);
163+
}
164+
165+
static async getConfigElement() {
166+
// Ensure the hui-stack-card-editor is loaded.
167+
let cls = customElements.get('hui-vertical-stack-card');
168+
if (!cls) {
169+
const helpers = await window.loadCardHelpers();
170+
helpers.createCardElement({ type: 'vertical-stack', cards: [] });
171+
await customElements.whenDefined('hui-vertical-stack-card');
172+
cls = customElements.get('hui-vertical-stack-card');
173+
}
174+
const configElement = await cls.getConfigElement();
175+
176+
// Patch setConfig to remove non-VSIC config options.
177+
const originalSetConfig = configElement.setConfig;
178+
configElement.setConfig = (config) =>
179+
originalSetConfig.call(configElement, {
180+
type: config.type,
181+
title: config.title,
182+
cards: config.cards || [],
183+
});
184+
185+
return configElement;
186+
}
187+
188+
static getStubConfig() {
189+
return {
190+
cards: [],
191+
};
192+
}
193+
}
194+
195+
customElements.define('vertical-stack-in-card', VerticalStackInCard);
196+
window.customCards = window.customCards || [];
197+
window.customCards.push({
198+
type: 'vertical-stack-in-card',
199+
name: 'Vertical Stack In Card',
200+
description: 'Group multiple cards into a single sleek card.',
201+
preview: false,
202+
documentationURL: 'https://github.com/ofekashery/vertical-stack-in-card',
203+
});

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "compass-card",
3-
"version": "3.1.2'",
3+
"version": "3.2.0'",
44
"description": "Lovelace compass-card",
55
"keywords": [
66
"home-assistant",

src/compass-card.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import style from './style';
1313
import { CARD_VERSION, COMPASS_ABBREVIATIONS, COMPASS_POINTS, DEFAULT_ICON_VALUE, ICON_VALUES, UNAVAILABLE } from './const';
1414

1515
import { localize } from './localize/localize';
16-
import { getHeader, getCompass, getIndicatorSensors, getValueSensors, getBoolean, findValues, isNumeric } from './utils/objectHelpers';
16+
import { getHeader, getCompass, getIndicatorSensors, getValueSensors, getBoolean, findValues, isNumeric, resolveAttrPath } from './utils/objectHelpers';
1717

1818
declare global {
1919
interface Window {
@@ -308,13 +308,29 @@ export class CompassCard extends LitElement {
308308
*/
309309

310310
private svgCompass(directionOffset: number): SVGTemplateResult {
311+
const bg = this.getBackgroundImage(this.compass.circle);
312+
const imageRotate = this.compass.circle.offset_background ? directionOffset : 0;
313+
const cx = 76;
314+
const cy = 76;
315+
const r = 62;
316+
const imgSize = r * 2;
317+
const imgX = cx - r;
318+
const imgY = cy - r;
319+
311320
return svg`
312321
<svg viewbox="0 0 152 152" preserveAspectRatio="xMidYMid meet" class="compass-svg" style="--compass-card-svg-scale:${this.svgScale}%">
313322
<defs>
314-
<pattern id="image" x="0" y="0" patternContentUnits="objectBoundingBox" height="100%" width="100%">
315-
<image x="0" y="0" height="1" width="1" href="${this.getBackgroundImage(this.compass.circle)}" preserveAspectRatio="xMidYMid meet"></image>
316-
</pattern>
323+
<!-- clip the image to the circle so the GIF can animate -->
324+
<clipPath id="imageClip">
325+
<circle cx="${cx}" cy="${cy}" r="${r}" />
326+
</clipPath>
317327
</defs>
328+
329+
${
330+
bg
331+
? svg`<image href="${bg}" x="${imgX}" y="${imgY}" width="${imgSize}" height="${imgSize}" preserveAspectRatio="xMidYMid slice" clip-path="url(#imageClip)" transform="rotate(${imageRotate}, ${cx}, ${cy})" />`
332+
: ''
333+
}
318334
${this.getVisibility(this.compass.circle) ? this.svgCircle(this.compass.circle.offset_background ? directionOffset : 0) : ''}
319335
<g class="indicators" transform="rotate(${directionOffset},76,76)" stroke-width=".5">
320336
${this.compass.north.show ? this.svgIndicatorNorth() : ''}
@@ -328,12 +344,10 @@ export class CompassCard extends LitElement {
328344
}
329345

330346
private svgCircle(directionOffset: number): SVGTemplateResult {
331-
return svg`<circle class="circle" cx="76" cy="76" r="62" stroke="${this.getColor(this.compass.circle)}" stroke-width="${this.compass.circle.stroke_width}" fill="${this.circleFill()}" fill-opacity="
332-
${this.compass.circle.background_opacity}" stroke-opacity="1.0" transform="rotate(${directionOffset},76,76)" />`;
333-
}
334-
335-
private circleFill(): string {
336-
return this.getBackgroundImage(this.compass.circle) === '' ? 'white' : 'url(#image)';
347+
// if we used a background image we want the image visible, so don't fill the circle
348+
// otherwise fall back to a solid fill (keeps previous behavior)
349+
const fill = this.getBackgroundImage(this.compass.circle) === '' ? 'white' : 'none';
350+
return svg`<circle class="circle" cx="76" cy="76" r="62" stroke="${this.getColor(this.compass.circle)}" stroke-width="${this.compass.circle.stroke_width}" fill="${fill}" fill-opacity="${this.compass.circle.background_opacity}" stroke-opacity="1.0" transform="rotate(${directionOffset},76,76)" />`;
337351
}
338352

339353
private svgIndicators(): SVGTemplateResult[] {
@@ -509,11 +523,11 @@ export class CompassCard extends LitElement {
509523

510524
private getValue(entity: CCEntity): CCValue {
511525
if (entity.is_attribute) {
512-
const entityStr = entity.sensor.slice(0, entity.sensor.lastIndexOf('.'));
526+
const entityStr = entity.sensor.split('.').slice(0, 2).join('.');
513527
const entityObj = this.entities[entityStr];
514528
if (entityObj && entityObj.attributes) {
515-
const attribStr = entity.sensor.slice(entity.sensor.lastIndexOf('.') + 1);
516-
const value = entityObj.attributes[attribStr] || UNAVAILABLE;
529+
const attribStr = entity.sensor.split('.').slice(2).join('.');
530+
const value = resolveAttrPath(entityObj.attributes, attribStr) || UNAVAILABLE;
517531
return { value: isNumeric(value) ? Number(value).toFixed(entity.decimals) : value, units: entity.units };
518532
}
519533
return { value: UNAVAILABLE, units: entity.units };

src/const.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { localize } from './localize/localize';
22

3-
export const CARD_VERSION = '3.0.0';
3+
export const CARD_VERSION = '3.2.0';
44
export const ICONS = {
55
compass: 'mdi:compass',
66
};

src/localize/languages/uk.json

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"common": {
3+
"version": "Версія",
4+
"description": "Показує компас з індикатором напрямку значення об'єкта",
5+
"invalid_configuration": "Неправильна конфігурація",
6+
"no_entity": "Об'єкт не налаштовано",
7+
"offset_not_a_number": "Зміщення напрямку не є числом",
8+
"invalid": "помилка",
9+
"on": "Увімк.",
10+
"off": "Вимк."
11+
},
12+
"editor": {
13+
"name": "Ім'я",
14+
"optional": "Не обов'язково",
15+
"entity": "Об'єкт",
16+
"required": "Обов'язково",
17+
"primary": "Напрямок",
18+
"secondary": "Додатково",
19+
"indicator": "Індикатор",
20+
"direction": "Напрямок",
21+
"offset": "Зміщення",
22+
"show": "Показати",
23+
"abbreviations": "Скорочення",
24+
"toggle": "Увімкнути",
25+
"language": "Мова",
26+
"primary entity description": "Об'єкт напрямку",
27+
"secondary entity description": "Додатковий об'єкт",
28+
"language description": "Мова скорочень напрямку",
29+
"offset description": "Зміщення напрямку"
30+
},
31+
"directions": {
32+
"north": "Північ",
33+
"east": "Схід",
34+
"south": "Південь",
35+
"west": "Захід",
36+
"N": "Пн",
37+
"NNE": "ПнПнСх",
38+
"NE": "ПнСх",
39+
"ENE": "СхПнСх",
40+
"E": "Сх",
41+
"ESE": "СхПдСх",
42+
"SE": "ПдСх",
43+
"SSE": "ПдПдСх",
44+
"S": "Пд",
45+
"SSW": "ПдПдЗх",
46+
"SW": "ПдЗх",
47+
"WSW": "ЗхПдЗх",
48+
"W": "Зх",
49+
"WNW": "ЗхПнЗх",
50+
"NW": "ПнЗх",
51+
"NNW": "ПнПнЗх"
52+
}
53+
}

src/localize/localize.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import * as sk from './languages/sk.json';
1818
import * as sl from './languages/sl.json';
1919
import * as tw from './languages/tw.json';
2020
import * as ru from './languages/ru.json';
21+
import * as uk from './languages/uk.json';
2122

2223
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2324
export const languages: any = {
@@ -41,6 +42,7 @@ export const languages: any = {
4142
sl: sl,
4243
tw: tw,
4344
ru: ru,
45+
uk: uk,
4446
};
4547
export const COMPASS_LANGUAGES = [...Object.keys(languages), ''].sort();
4648

0 commit comments

Comments
 (0)