Refactoring. New args function

This commit is contained in:
Thomas Lovén 2019-01-22 21:45:06 +01:00
parent 148cc93376
commit 2ca0b84d1f
2 changed files with 289 additions and 241 deletions

149
README.md
View File

@ -29,43 +29,75 @@ That's all. You don't need to do anything else.
## Developer instructions
`card-tools` defines a global object `window.cardTools` which contains some helpful functions and stuff.
*BREAKING CHANGES IN VERSION 0.3* Please read changelog below
`card-tools` defines a global object `cardTools` which contains some helpful functions and stuff.
To make sure `card-tools` is installed, add the following line to the start of the `setConfig()` function of your custom card:
To make sure `card-tools` is loaded before your plugin, wait for `customElements.whenDefined("card-tools")` to resolve.
Example:
```js
if(!window.cardTools) throw new Error(`Can't find card-tools. See https://github.com/thomasloven/lovelace-card-tools`);
customElements.whenDefined('card-tools').then(() => {
// YOUR CODE GOES IN HERE
class MyPlugin extends cardTools.litElement {
setConfig(config) {
this.name = config.name;
}
render() {
return cardTools.litHtml()`
${this.name}
`;
}
}
customElements.define("my-plugin", MyPlugin);
}); // END OF .then(() => {
setTimeout(() => {
if(customElements.get('card-tools')) return;
customElements.define('my-plugin', class extends HTMLElement{
setConfig() { throw new Error("Can't find card-tools. See https://github.com/thomasloven/lovelace-card-tools");}
});
}, 2000);
```
The following functions and variables are defined:
The `setTimeout` block at the end will make your element display an error message if `card-tools` is not found. Make sure the element name is the same in both `customElements.define()` calls.
The following functions are defined:
| Name | v >= | Description |
| --- | --- | --- |
| `window.cardTools.v` | 0.1 | Current `card-tools` version |
| `window.cardTools.checkVersion(v)` | 0.1 | Check that the current `card-tools` version is at least `v` |
| `window.cardTools.hass()` | 0.1 | Returns A `hass` state object. Useful for plugins that are *not* custom cards. If you need it, you'll know it |
| `window.cardTool.fireEvent(event, detail)` | 0.1 | Fire lovelace event `event` with options `detail` |
| `window.cardTools.LitElement` | 0.1 | A reference to the LitElement base class. |
| `window.cardTools.litHtml` | 0.1 | A reference to the litHtml template function (requires Home Assistant 0.84 or later) |
| `window.cardTools.createCard(config)` | 0.1 | Creates and sets up a lovelace card based on `config` |
| `window.cardTools.createElement(config)` | 0.1 | Creates and sets up a `picture-elements` element based on `config` |
| `window.cardTools.createEntityRow(config)` | 0.1 | Creates and sets up an `entities` row based on `config` |
| `window.cardTools.deviceID` | 0.1 | Kind of unique and kind of persistent identifier for the browser viewing the page |
| `window.cardTools.moreInfo(entity)` | 0.1 | Brings up the `more-info` dialog for the specified `entity` id |
| `window.cardTools.longPress(element)` | 0.1 | Bind `element` to the long-press handler of lovelace |
| `window.cardTools.parseTemplate(text, [error])` | 0.2 | Parse a simple state template and return results |
| `cardTools.v()` | 0.1 | Current `card-tools` version |
| `cardTools.checkVersion(v)` | 0.1 | Check that the current `card-tools` version is at least `v` |
| `cardTools.hass()` | 0.1 | Returns A `hass` state object. Useful for plugins that are *not* custom cards. If you need it, you'll know it |
| `cardTool.fireEvent(event, detail)` | 0.1 | Fire lovelace event `event` with options `detail` |
| `cardTools.litElement()` | 0.1 | A reference to the LitElement base class. |
| `cardTools.litHtml()` | 0.1 | A reference to the litHtml template function (requires Home Assistant 0.84 or later) |
| `cardTools.createCard(config)` | 0.1 | Creates and sets up a lovelace card based on `config` |
| `cardTools.createElement(config)` | 0.1 | Creates and sets up a `picture-elements` element based on `config` |
| `cardTools.createEntityRow(config)` | 0.1 | Creates and sets up an `entities` row based on `config` |
| `cardTools.deviceID()` | 0.1 | Kind of unique and kind of persistent identifier for the browser viewing the page |
| `cardTools.moreInfo(entity)` | 0.1 | Brings up the `more-info` dialog for the specified `entity` id |
| `cardTools.longPress(element)` | 0.1 | Bind `element` to the long-press handler of lovelace |
| `cardTools.hasTemplate(text)` | 0.2 | Check if `text` contains a simple state template |
| `cardTools.parseTemplate(text, [error])` | 0.2 | Parse a simple state template and return results |
| `cardTools.args()` | 0.3 | Returns URL parameters of the script from `resources:` |
> Another way to use the `card-tools` is to just copy the function you want, and paste it into your card. It requires a bit of more work, but may be more user friendly.
### v and checkVersion
This variable and function are just there to make sure the user has the right version of `card-tools`. I may add more functions later, and then you can make sure that those are supported by the version the user has.
Those functions are just there to make sure the user has the right version of `card-tools`. I may add more functions later, and then you can make sure that those are supported by the version the user has.
I recommend adding a check as soon as possible, such as in the `setConfig()` function of a custom card/element/entity row.
```js
setConfig(config) {
if(!window.cardTools) throw new Error(`Can't find card-tools. See https://github.com/thomasloven/lovelace-card-tools`);
window.cardTools.checkVersion(0.1);
cardTools.checkVersion(0.1);
...
```
@ -77,44 +109,18 @@ This is provided for plugins that *aren't* cards, elements or entity rows. For t
```js
...
greeting.innerHTML = `Hi there, ${window.cardTools.hass().user.name}`;
window.cardTools.hass().connection.subscribeEvents((event) => {console.log("A service was called in Home Assistant")}, 'call-service');
greeting.innerHTML = `Hi there, ${cardTools.hass().user.name}`;
cardTools.hass().connection.subscribeEvents((event) => {console.log("A service was called in Home Assistant")}, 'call-service');
```
### fireEvent
This is mainly used as a helper for some other functions of `cardTools`, but it could be useful to fire a lovelace event sometime, such as `"config-refresh"` perhaps? Explore!
### LitElement and litHtml
### litElement and litHtml
Currently, the Home Assistant frontend is being converted to LitElement based elements, rather than Polymer based, since those are faster and easier to use. If you wish to make your element LitElement based, those may help.
**Important!** Since `card-tools` may be loaded after your element, it's not advisable to make your card class extend `window.cardTools.LitElement`.
Instead, a reference to `LitElement` must be extracted from somewhere else:
```js
var LitElement = LitElement || Object.getPrototypeOf(customElements.get('hui-error-entity-row'));
class LitCard extends LitElement {
...
}
customelements.define('lit-card', LitCard);
```
Unfortunately, this method may break if the definition of `hui-error-entity-row` changes in the future, but should - in that case - only need minor modifications.
`litHtml` can be used safely, though:
```js
render() {
return window.cardTools.litHtml`
<ha-card .header="${this._config.title}">
Hello, world!
</ha-card>
`;
```
### createCard, createElement, createEntityRow
Currently, custom elements can be used in three places in Lovelace; as cards, as elements in a `picture-elements` card or as rows in an `entities` card.
@ -122,14 +128,14 @@ Currently, custom elements can be used in three places in Lovelace; as cards, as
Those functions creates a card, element or row safely and cleanly from a config object. They handle custom elements and automatically picks the most suitable row for an entity. In short, it's mainly based on - and works very similar to - how Lovelace handles those things natively.
```js
const myElement = window.cardTools.createElement({
const myElement = cardTools.createElement({
type: "state-icon",
entity: "light.bed_light",
hold_action: {action: "toggle"},
});
```
> There's also a `window.cardTools.createThing(thing, config)` which is a helper function for those three. You'll probably never need to access it directly, but it might be good to know that it's there...
> There's also a `cardTools.createThing(thing, config)` which is a helper function for those three. You'll probably never need to access it directly, but it might be good to know that it's there...
### deviceID
@ -137,7 +143,7 @@ This can be used to uniquely identify the device connected to Lovelace. Or actua
It generates a random number, and stores it in the browsers local storage. That means it will stay around for quite a while.
It's kind of hard to explain, but as an example I use this to make browsers usable as media players in [lovelace-player](https://githb.com/thomasloven/lovelace-player). In short, a `media`-`deviceID` pair is sent to every browser currently viewing the lovelace UI, but only if the `deviceId` matches `window.cardTools.deviceID` is the `media` played. That way, I can make a sound play only on my ipad, even if I have the same page open on my computer.
It's kind of hard to explain, but as an example I use this to make browsers usable as media players in [lovelace-player](https://githb.com/thomasloven/lovelace-player). In short, a `media`-`deviceID` pair is sent to every browser currently viewing the lovelace UI, but only if the `deviceId` matches `cardTools.deviceID()` is the `media` played. That way, I can make a sound play only on my ipad, even if I have the same page open on my computer.
I'm sure this can have lots of uses.
@ -149,9 +155,9 @@ This can be used to open the more-info dialog for an entity.
```js
render() {
return window.cardTools.litHtml`
return cardTools.litHtml`
<paper-button
@click="${window.cardTools.moreInfo("light.bed_light");}"
@click="${cardTools.moreInfo("light.bed_light");}"
>
Click me!
</paper-button>
@ -167,9 +173,9 @@ Once an element has been bound to longpress, it will be able to receive `ha-clic
```js
render() {
return window.cardTools.litHtml`
return cardTools.litHtml`
<paper-button
@click="${window.cardTools.moreInfo("light.bed_light");}"
@click="${cardTools.moreInfo("light.bed_light");}"
@ha-click="${console.log('I was clicked')}"
@ha-hold="${console.log('I was held')}"
>
@ -179,13 +185,13 @@ render() {
}
firstUpdated() {
window.cardTools.longpress(this.shadowRoot.querySelector('paper-button'));
cardTools.longpress(this.shadowRoot.querySelector('paper-button'));
}
```
### parseTemplate
### hasTemplate and parseTemplate
This lets you parse a user specified template like `[[ light.bed_light.state ]]` and return the result.
`cardTools.parseTemplate` lets you parse a user specified template like `[[ light.bed_light.state ]]` and return the result.
Two things are important:
@ -205,3 +211,30 @@ Next is one of:
The function replaces any template found in the supplied string with the requested value, or an error message on failure.
The optional argument `error` is a string that replaces the default error message.
`cardTools.hasTemplate` just checks if a string contains a simple state template.
### args
Lovelace plugins are imported by placing a script URL in the `resources` section of `ui-lovelace.yaml` or the Raw editor. This URL can be followed by query parameters.
```
resources:
- url: /local/my-plugin.js?height=5&flag&width=10&
type: js
```
If called from `my-plugin.js` `cardTools.args()` will return the javascript object `{height: 5, flag: undefined, width: 10}`.
## Changelog
*0.2*
- Added `parseTemplate()` function
*0.3*
- `LitElement` renamed to `litElement`.
- `cardTools.litElement()`, `cardTools.litHtml` and `cardtools.deviceID()` are now functions.
- Updated recommendation for how to check if `card-tools` exists.
- Added `hasTemplate()` to documentation.
- Added `args()` function.
---

View File

@ -1,39 +1,38 @@
if (!window.cardTools){
const version = 0.2;
const CUSTOM_TYPE_PREFIX = "custom:";
customElements.define('card-tools',
class {
static CUSTOM_TYPE_PREFIX() { return "custom:"}
static version() { return "0.3"}
let cardTools =
{};
static v() {return version};
cardTools.v =
version;
cardTools.checkVersion =
(v) => {
if (version < v) {
static checkVersion(v) {
if (this.version() < v) {
throw new Error(`Old version of card-tools found. Get the latest version of card-tools.js from https://github.com/thomasloven/lovelace-card-tools`);
}
};
}
cardTools.LitElement =
Object.getPrototypeOf(customElements.get('hui-error-entity-row'));
static litElement() {
return Object.getPrototypeOf(customElements.get('hui-error-entity-row'));
}
cardTools.litHtml =
cardTools.LitElement.prototype.html;
static litHtml() {
return this.litElement().prototype.html;
}
cardTools.hass =
() => {
static hass() {
return document.querySelector('home-assistant').hass;
};
}
cardTools.fireEvent =
(ev, detail) => {
static fireEvent(ev, detail, entity=null) {
ev = new Event(ev, {
bubbles: true,
cancelable: false,
composed: true,
});
ev.detail = detail || {};
if(entity) {
entity.dispatchEvent(ev);
} else {
document
.querySelector("home-assistant")
.shadowRoot.querySelector("home-assistant-main")
@ -43,9 +42,9 @@ if (!window.cardTools){
.shadowRoot.querySelector("ha-app-layout #view hui-view")
.dispatchEvent(ev);
}
}
cardTools.createThing =
(thing, config) => {
static createThing(thing, config) {
const _createThing = (tag, config) => {
const element = document.createElement(tag);
try {
@ -73,8 +72,8 @@ if (!window.cardTools){
delete config.error;
return _createError(err, config);
}
if(tag.startsWith(CUSTOM_TYPE_PREFIX))
tag = tag.substr(CUSTOM_TYPE_PREFIX.length);
if(tag.startsWith(this.CUSTOM_TYPE_PREFIX()))
tag = tag.substr(this.CUSTOM_TYPE_PREFIX().length);
else
tag = `hui-${tag}-${thing}`;
@ -93,20 +92,21 @@ if (!window.cardTools){
// Remove error if element is defined later
customElements.whenDefined(tag).then(() => {
clearTimeout(timer);
cardTools.fireEvent("rebuild-view");
this.fireEvent("ll-rebuild", {}, element);
});
return element;
};
}
cardTools.createCard =
(config) => { return cardTools.createThing("card", config); };
static createCard(config) {
return this.createThing("card", config);
}
cardTools.createElement =
(config) => { return cardTools.createThing("element", config); };
static createElement(config) {
return this.createThing("element", config);
}
cardTools.createEntityRow =
(config) => {
static createEntityRow(config) {
const SPECIAL_TYPES = new Set([
"call-service",
"divider",
@ -137,20 +137,19 @@ if (!window.cardTools){
if(!config || typeof config !== "object" || (!config.entity && !config.type)) {
Object.assign(config, {error: "Invalid config given"});
return cardTools.createThing("", config);
return this.createThing("", config);
}
const type = config.type || "default";
if(SPECIAL_TYPES.has(type) || type.startsWith(CUSTOM_TYPE_PREFIX))
return cardTools.createThing("row", config);
if(SPECIAL_TYPES.has(type) || type.startsWith(this.CUSTOM_TYPE_PREFIX()))
return this.createThing("row", config);
const domain = config.entity.split(".", 1)[0];
Object.assign(config, {type: DEFAULT_ROWS[domain] || "text"});
return cardTools.createThing("entity-row", config);
};
return this.createThing("entity-row", config);
}
cardTools.deviceID =
(() => {
static deviceID() {
const ID_STORAGE_KEY = 'lovelace-player-device-id';
if(window['fully'] && typeof fully.getDeviceId === "function")
return fully.getDeviceId();
@ -162,34 +161,30 @@ if (!window.cardTools){
localStorage[ID_STORAGE_KEY] = `${s4()}${s4()}-${s4()}${s4()}`;
}
return localStorage[ID_STORAGE_KEY];
})();
}
cardTools.moreInfo =
(entity) => {
cardTools.fireEvent("hass-more-info", {entityId: entity});
};
static moreInfo(entity) {
this.fireEvent("hass-more-info", {entityId: entity});
}
cardTools.longpress =
(element) => {
static longpress(element) {
customElements.whenDefined("long-press").then(() => {
const longpress = document.body.querySelector("long-press");
longpress.bind(element);
});
return element;
};
}
cardTools.hasTemplate =
(text) => {
static hasTemplate(text) {
return /\[\[\s+.*\s+\]\]/.test(text);
};
}
cardTools.parseTemplate =
(text, error) => {
static parseTemplate(text, error) {
const _parse = (str) => {
try {
str = str.replace(/^\[\[\s+|\s+\]\]$/g, '')
const parts = str.split(".");
let v = cardTools.hass().states[`${parts[0]}.${parts[1]}`];
let v = this.hass().states[`${parts[0]}.${parts[1]}`];
parts.shift();
parts.shift();
parts.forEach(item => v = v[item]);
@ -200,9 +195,29 @@ if (!window.cardTools){
}
text = text.replace(/(\[\[\s.*?\s\]\])/g, (str, p1, offset, s) => _parse(str));
return text;
};
window.cardTools = cardTools;
customElements.define('card-tools', class {});
console.info("%cCARD-TOOLS IS INSTALLED", "color: green; font-weight: bold");
}
static args() {
var url = document.currentScript.src
url = url.substr(url.indexOf("?")+1)
let args = {};
url.split("&").forEach((a) => {
if(a.indexOf("=")) {
var parts = a.split("=");
args[parts[0]] = parts[1]
} else {
args[a] = true;
}
});
return args;
}
});
// Global definition of cardTools
cardTools = customElements.get('card-tools');
console.info(`%cCARD-TOOLS IS INSTALLED
%cDeviceID: ${customElements.get('card-tools').deviceID()}`,
"color: green; font-weight: bold",
"");