Complete overhaul

This commit is contained in:
2019-11-11 16:48:36 +01:00
parent 83e2b16af0
commit 8eeea0e700
10 changed files with 4663 additions and 373 deletions

102
src/layout.js Normal file
View File

@@ -0,0 +1,102 @@
function defaultLayout(cards, cols, config) {
function shortestCol() {
// Find the shortest column or the first one less than min_height
let i = 0;
for(let j=0; j<cols.length; j++){
if(cols[j].length < config.min_height)
return j;
if(cols[j].length < cols[i].length)
i = j;
}
return i;
}
cards.forEach((c) => {
if(!c) return; // Gap. Skip
const col = cols[shortestCol()];
col.appendChild(c);
col.length += c.getCardSize ? c.getCardSize() : 1;
});
}
function horizontalLayout(cards, cols, config) {
let i = 0;
cards.forEach((c) => {
i += 1;
if(!c) return; // Gap. Skip
const col = cols[(i-1)%cols.length];
col.appendChild(c);
col.length += c.getCardSize ? c.getCardSize() : 1;
})
}
function verticalLayout(cards, cols, config) {
let i = 0;
cards.forEach((c) => {
if(!c) { // Gap. Skip to next column
i += 1;
return;
}
const col = cols[(i)%cols.length];
col.appendChild(c);
col.length += c.getCardSize ? c.getCardSize() : 1;
})
}
export function buildLayout(cards, width, config) {
const parseValue = (val) => {
// Accepts e.g. `5` , `5px` or `50%`.
if(typeof(val) === "string")
if(val.endsWith("%"))
return Math.floor(width*parseInt(val)/100);
return parseInt(val)
}
let colnum = 0;
if(typeof(config.column_width) === "object") {
// If many widths are given, keep adding collumns as long as there is any
// horizontal space left
let calcWidth = width;
while(calcWidth > 0) {
// If there are not enough values, use the last one
let w = config.column_width[colnum];
if(w === undefined) w = config.column_width.slice(-1)[0];
calcWidth -= parseValue(w);
colnum += 1;
}
colnum = Math.max(colnum-1, 1);
} else {
colnum = Math.floor(width / parseValue(config.column_width));
}
colnum = Math.max(colnum, config.min_columns);
colnum = Math.min(colnum, config.max_columns);
let cols = [];
for (let i = 0; i < colnum; i++) {
const newCol = document.createElement("div");
newCol.classList.add("column");
newCol.length = 0;
cols.push(newCol);
}
switch(config.layout) {
case 'horizontal':
horizontalLayout(cards, cols, config);
break;
case 'vertical':
verticalLayout(cards, cols, config);
break;
case 'auto':
default:
defaultLayout(cards, cols, config);
}
// Remove empty columns
cols = cols.filter((c) => c.childElementCount > 0);
return cols;
}

211
src/main.js Normal file
View File

@@ -0,0 +1,211 @@
import { LitElement, html, css } from "card-tools/src/lit-element";
// import "card-tools/src/card-maker";
import { hass } from "card-tools/src/hass";
import {buildLayout} from "./layout";
class LayoutCard extends LitElement {
static get properties() {
return {
hass: {},
_config: {},
};
}
async setConfig(config) {
this._config = {
min_height: 5,
column_width: 300,
max_width: config.column_width || "500px",
min_columns: config.column_num || 1,
max_columns: 100,
...config,
}
this.cards = [];
this.columns = [];
}
async firstUpdated() {
window.addEventListener('resize', () => this.place_cards());
window.addEventListener('hass-open-menu', () => setTimeout(() => this.place_cards(), 100));
window.addEventListener('hass-close-menu', () => setTimeout(() => this.place_cards(), 100));
window.addEventListener('location-changed', () => {
if(location.hash === "")
setTimeout(() => this.place_cards(), 100)
});
}
async updated(changedproperties) {
if(!this.cards.length) {
// Build cards and layout
this.cards = await this.build_cards();
this.place_cards();
}
if(changedproperties.has("hass") && this.hass && this.cards) {
// Update the hass object of every card
this.cards.forEach((c) => {
if(!c) return;
c.hass = this.hass;
});
}
}
async build_card(c) {
if(c === "break")
return null;
const el = document.createElement("card-maker");
el.config = c;
el.hass = hass();
// Cards are initially placed in the staging area
// That places them in the DOM and lets us read their getCardSize() function
this.shadowRoot.querySelector("#staging").appendChild(el);
return new Promise((resolve, reject) => el.updateComplete.then(() => resolve(el)));
}
async build_cards() {
// Clear out any cards in the staging area which might have been built but not placed
const staging = this.shadowRoot.querySelector("#staging");
while(staging.lastChild)
staging.removeChild(staging.lastChild);
return Promise.all(
(this._config.entities || this._config.cards)
.map((c) => this.build_card(c))
);
}
place_cards() {
const width = this.shadowRoot.querySelector("#columns").clientWidth;
this.columns = buildLayout(
this.cards,
width,
this._config
);
this.format_columns();
this.requestUpdate();
}
format_columns() {
const renderProp = (name, property, index, unit="px") => {
// Check if the config option is specified
if (this._config[property] === undefined) return "";
let retval = `${name}: `;
const prop = this._config[property];
if (typeof(prop) === "object")
// Get the last value if there are not enough
if(prop.length > index)
retval += `${prop[index]}`;
else
retval += `${prop.slice(-1)}`;
else
retval += `${prop}`;
// Add unit (px) if necessary
if(!retval.endsWith("px") && !retval.endsWith("%")) retval += unit;
return retval + ";"
}
// Set element style for each column
for(const [i, c] of this.columns.entries()) {
const styles = [
renderProp("max-width", "max_width", i),
renderProp("min-width", "min_width", i),
renderProp("width", "column_width", i),
renderProp("flex-grow", "flex_grow", i, ""),
]
c.style.cssText = ''.concat(...styles);
}
}
getCardSize() {
if(this.columns)
return Math.max.apply(Math, this.columns.map((c) => c.length));
}
isPanel() {
let el = this.parentElement;
let steps = 10;
while(steps--) {
if(el.localName === "hui-panel-view") return true;
if(el.localName === "div") return false;
el = el.parentElement;
}
return false;
}
render() {
return html`
<div id="columns"
class="
${this._config.rtl ? "rtl": " "}
${this.isPanel() ? "panel": " "}
"
style="
${this._config.justify_content ? `justify-content: ${this._config.justify_content};` : ''}
">
${this.columns.map((col) => html`
${col}
`)}
</div>
<div id="staging"></div>
`;
}
static get styles() {
return css`
:host {
padding: 0 4px;
display: block;
margin-bottom: 0!important;
}
#columns {
display: flex;
flex-direction: row;
justify-content: center;
margin-top: -8px;
}
#columns.rtl {
flex-direction: row-reverse;
}
#columns.panel {
margin-top: 0;
}
.column {
flex-basis: 0;
flex-grow: 1;
overflow-x: hidden;
}
card-maker>* {
display: block;
margin: 4px 4px 8px;
}
card-maker:first-child>* {
margin-top: 8px;
}
card-maker:last-child>* {
margin-bottom: 4px;
}
#staging {
visibility: hidden;
height: 0;
}
`;
}
}
customElements.define("layout-card", LayoutCard);