diff --git a/config/babel.js b/config/babel.js index d8974a3f65..1c073245dc 100644 --- a/config/babel.js +++ b/config/babel.js @@ -3,7 +3,7 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => { throw Error("latestBuild not defined for babel loader config"); } return { - test: /\.m?js$/, + test: /\.m?js$|\.ts$/, use: { loader: "babel-loader", options: { @@ -12,6 +12,7 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => { require("@babel/preset-env").default, { modules: false }, ], + require("@babel/preset-typescript").default, ].filter(Boolean), plugins: [ // Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2}) diff --git a/gallery/webpack.config.js b/gallery/webpack.config.js index 155d92e3a2..d92eb482d7 100644 --- a/gallery/webpack.config.js +++ b/gallery/webpack.config.js @@ -56,6 +56,9 @@ module.exports = { }, }), ].filter(Boolean), + resolve: { + extensions: [".ts", ".js", ".json"], + }, output: { filename: "[name].js", chunkFilename: chunkFilename, diff --git a/hassio/webpack.config.js b/hassio/webpack.config.js index 4d29b651df..8ce545eac9 100644 --- a/hassio/webpack.config.js +++ b/hassio/webpack.config.js @@ -42,6 +42,9 @@ module.exports = { exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/], }), ].filter(Boolean), + resolve: { + extensions: [".ts", ".js", ".json"], + }, output: { filename: "[name].js", chunkFilename, diff --git a/package.json b/package.json index 900337d586..a7c3450adc 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "version": "1.0.0", "scripts": { "build": "script/build_frontend", - "lint": "eslint src hassio/src gallery/src test-mocha && polymer lint", + "lint": "eslint src hassio/src gallery/src test-mocha && tslint -c tslint.json 'src/**/*.ts' 'hassio/src/**/*.ts' 'gallery/src/**/*.ts' 'test-mocha/**/*.ts' && polymer lint", "mocha": "node_modules/.bin/mocha --opts test-mocha/mocha.opts", "test": "npm run lint && npm run mocha", "docker_build": "sh ./script/docker_run.sh build $npm_package_version", @@ -91,10 +91,12 @@ "devDependencies": { "@babel/core": "^7.1.2", "@babel/plugin-external-helpers": "^7.0.0", + "@babel/plugin-proposal-class-properties": "7.0.0", "@babel/plugin-proposal-object-rest-spread": "^7.0.0", "@babel/plugin-syntax-dynamic-import": "^7.0.0", "@babel/plugin-transform-react-jsx": "^7.0.0", "@babel/preset-env": "^7.1.0", + "@babel/preset-typescript": "7.0.0", "@gfx/zopfli": "^1.0.9", "babel-eslint": "^10", "babel-loader": "^8.0.4", @@ -133,6 +135,10 @@ "reify": "^0.17.3", "require-dir": "^1.0.0", "sinon": "^6.3.4", + "tslint": "^5.11.0", + "tslint-config-prettier": "^1.15.0", + "tslint-eslint-rules": "^5.4.0", + "typescript": "3.1.3", "wct-browser-legacy": "^1.0.1", "web-component-tester": "^6.8.0", "webpack": "^4.19.1", diff --git a/src/mixins/lit-localize-mixin.js b/src/mixins/lit-localize-mixin.js deleted file mode 100644 index 1433d8b5ca..0000000000 --- a/src/mixins/lit-localize-mixin.js +++ /dev/null @@ -1,52 +0,0 @@ -import { dedupingMixin } from "@polymer/polymer/lib/utils/mixin.js"; -import { LocalizeBaseMixin } from "./localize-base-mixin"; - -export const HassLocalizeLitMixin = dedupingMixin( - (superClass) => - class extends LocalizeBaseMixin(superClass) { - static get properties() { - return { - hass: {}, - localize: {}, - }; - } - - connectedCallback() { - super.connectedCallback(); - - let language; - let resources; - if (this.hass) { - language = this.hass.language; - resources = this.hass.resources; - } - this.localize = this.__computeLocalize(language, resources); - } - - updated(changedProperties) { - super.updated(changedProperties); - - if (!changedProperties.has("hass")) { - return; - } - - let oldLanguage; - let oldResources; - if (changedProperties.hass) { - oldLanguage = changedProperties.hass.language; - oldResources = changedProperties.hass.resources; - } - - let language; - let resources; - if (this.hass) { - language = this.hass.language; - resources = this.hass.resources; - } - - if (oldLanguage !== language || oldResources !== resources) { - this.localize = this.__computeLocalize(language, resources); - } - } - } -); diff --git a/src/mixins/lit-localize-mixin.ts b/src/mixins/lit-localize-mixin.ts new file mode 100644 index 0000000000..51c408310a --- /dev/null +++ b/src/mixins/lit-localize-mixin.ts @@ -0,0 +1,67 @@ +import { + Constructor, + LitElement, + PropertyDeclarations, + PropertyValues, +} from "@polymer/lit-element"; +import { HomeAssistant } from "../types"; +import { + LocalizeBaseMixin, + LocalizeFunc, + LocalizeMixin, +} from "./localize-base-mixin"; + +export const HassLocalizeLitMixin = ( + superClass: Constructor +): Constructor => + // @ts-ignore + class extends LocalizeBaseMixin(superClass) { + protected hass: HomeAssistant; + protected localize: LocalizeFunc; + + static get properties(): PropertyDeclarations { + return { + hass: {}, + localize: {}, + }; + } + + public connectedCallback(): void { + super.connectedCallback(); + + let language; + let resources; + if (this.hass) { + language = this.hass.language; + resources = this.hass.resources; + } + this.localize = this.__computeLocalize(language, resources); + } + + public updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + + if (!changedProperties.has("hass")) { + return; + } + + let oldLanguage; + let oldResources; + const hass = changedProperties.get("hass") as HomeAssistant; + if (hass) { + oldLanguage = hass.language; + oldResources = hass.resources; + } + + let language; + let resources; + if (this.hass) { + language = this.hass.language; + resources = this.hass.resources; + } + + if (oldLanguage !== language || oldResources !== resources) { + this.localize = this.__computeLocalize(language, resources); + } + } + }; diff --git a/src/mixins/localize-base-mixin.js b/src/mixins/localize-base-mixin.js deleted file mode 100644 index 8b087143ed..0000000000 --- a/src/mixins/localize-base-mixin.js +++ /dev/null @@ -1,82 +0,0 @@ -import { dedupingMixin } from "@polymer/polymer/lib/utils/mixin.js"; -import IntlMessageFormat from "intl-messageformat/src/main.js"; - -/** -Adapted from Polymer app-localize-behavior. - -Copyright (c) 2016 The Polymer Project Authors. All rights reserved. -This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt -The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt -The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt -Code distributed by Google as part of the polymer project is also -subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt -*/ - -export const LocalizeBaseMixin = dedupingMixin( - (superClass) => - class extends superClass { - /** - * Returns a computed `localize` method, based on the current `language`. - */ - __computeLocalize(language, resources, formats) { - const proto = this.constructor.prototype; - - // Check if localCache exist just in case. - this.__checkLocalizationCache(proto); - - // Everytime any of the parameters change, invalidate the strings cache. - if (!proto.__localizationCache) { - proto.__localizationCache = { - messages: {}, - }; - } - proto.__localizationCache.messages = {}; - - return (key, ...args) => { - if (!key || !resources || !language || !resources[language]) { - return ""; - } - - // Cache the key/value pairs for the same language, so that we don't - // do extra work if we're just reusing strings across an application. - const translatedValue = resources[language][key]; - - if (!translatedValue) { - return this.useKeyIfMissing ? key : ""; - } - - const messageKey = key + translatedValue; - let translatedMessage = - proto.__localizationCache.messages[messageKey]; - - if (!translatedMessage) { - translatedMessage = new IntlMessageFormat( - translatedValue, - language, - formats - ); - proto.__localizationCache.messages[messageKey] = translatedMessage; - } - - const argObject = {}; - for (let i = 0; i < args.length; i += 2) { - argObject[args[i]] = args[i + 1]; - } - - return translatedMessage.format(argObject); - }; - } - - __checkLocalizationCache(proto) { - // do nothing if proto is undefined. - if (proto === undefined) return; - - // In the event proto not have __localizationCache object, create it. - if (proto.__localizationCache === undefined) { - proto.__localizationCache = { - messages: {}, - }; - } - } - } -); diff --git a/src/mixins/localize-base-mixin.ts b/src/mixins/localize-base-mixin.ts new file mode 100644 index 0000000000..af19f90ae4 --- /dev/null +++ b/src/mixins/localize-base-mixin.ts @@ -0,0 +1,109 @@ +import { dedupingMixin } from "@polymer/polymer/lib/utils/mixin.js"; +import IntlMessageFormat from "intl-messageformat/src/main.js"; + +/** + * Adapted from Polymer app-localize-behavior. + * + * Copyright (c) 2016 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +/** + * Optional dictionary of user defined formats, as explained here: + * http://formatjs.io/guides/message-syntax/#custom-formats + * + * For example, a valid dictionary of formats would be: + * this.formats = { + * number: { USD: { style: 'currency', currency: 'USD' } } + * } + */ +interface FormatType { + [format: string]: any; +} +export interface FormatsType { + number: FormatType; + date: FormatType; + time: FormatType; +} + +export type LocalizeFunc = (key: string, ...args: any[]) => string; + +export interface LocalizeMixin { + localize: LocalizeFunc; +} + +export const LocalizeBaseMixin = (superClass) => + class extends superClass { + /** + * Returns a computed `localize` method, based on the current `language`. + */ + public __computeLocalize( + language: string, + resources: string, + formats?: FormatsType + ): LocalizeFunc { + const proto = this.constructor.prototype; + + // Check if localCache exist just in case. + this.__checkLocalizationCache(proto); + + // Everytime any of the parameters change, invalidate the strings cache. + if (!proto.__localizationCache) { + proto.__localizationCache = { + messages: {}, + }; + } + proto.__localizationCache.messages = {}; + + return (key, ...args) => { + if (!key || !resources || !language || !resources[language]) { + return ""; + } + + // Cache the key/value pairs for the same language, so that we don't + // do extra work if we're just reusing strings across an application. + const translatedValue = resources[language][key]; + + if (!translatedValue) { + return this.useKeyIfMissing ? key : ""; + } + + const messageKey = key + translatedValue; + let translatedMessage = proto.__localizationCache.messages[messageKey]; + + if (!translatedMessage) { + translatedMessage = new (IntlMessageFormat as any)( + translatedValue, + language, + formats + ); + proto.__localizationCache.messages[messageKey] = translatedMessage; + } + + const argObject = {}; + for (let i = 0; i < args.length; i += 2) { + argObject[args[i]] = args[i + 1]; + } + + return translatedMessage.format(argObject); + }; + } + + public __checkLocalizationCache(proto) { + // do nothing if proto is undefined. + if (proto === undefined) { + return; + } + + // In the event proto not have __localizationCache object, create it. + if (proto.__localizationCache === undefined) { + proto.__localizationCache = { + messages: {}, + }; + } + } + }; diff --git a/src/mixins/localize-mixin.js b/src/mixins/localize-mixin.js index d1a104d1d2..263e97bcb5 100644 --- a/src/mixins/localize-mixin.js +++ b/src/mixins/localize-mixin.js @@ -1,5 +1,5 @@ import { dedupingMixin } from "@polymer/polymer/lib/utils/mixin.js"; -import { LocalizeBaseMixin } from "./localize-base-mixin.js"; +import { LocalizeBaseMixin } from "./localize-base-mixin"; /** * Polymer Mixin to enable a localize function powered by language/resources from hass object. * diff --git a/src/panels/lovelace/cards/hui-glance-card.js b/src/panels/lovelace/cards/hui-glance-card.ts similarity index 67% rename from src/panels/lovelace/cards/hui-glance-card.js rename to src/panels/lovelace/cards/hui-glance-card.ts index c2ca66f5f5..5154848ea6 100644 --- a/src/panels/lovelace/cards/hui-glance-card.js +++ b/src/panels/lovelace/cards/hui-glance-card.ts @@ -1,4 +1,4 @@ -import { LitElement, html } from "@polymer/lit-element"; +import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element"; import { classMap } from "lit-html/directives/classMap.js"; import { repeat } from "lit-html/directives/repeat"; @@ -12,11 +12,89 @@ import "../../../components/entity/state-badge.js"; import "../../../components/ha-card.js"; import "../../../components/ha-icon.js"; -import EventsMixin from "../../../mixins/events-mixin.js"; -import { HassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin.js"; +import { fireEvent } from "../../../common/dom/fire_event.js"; +import { HassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin"; +import { HomeAssistant } from "../../../types.js"; +import { LovelaceCard, LovelaceConfig } from "../types.js"; -class HuiGlanceCard extends HassLocalizeLitMixin(EventsMixin(LitElement)) { - renderStyle() { +interface EntityConfig { + name: string; + icon: string; + entity: string; + tap_action: "toggle" | "call-service" | "more-info"; + service?: string; + service_data?: object; +} + +interface Config extends LovelaceConfig { + show_name?: boolean; + show_state?: boolean; + title?: string; + column_width?: string; + theming?: "primary"; + entities: EntityConfig[]; +} + +class HuiGlanceCard extends HassLocalizeLitMixin(LitElement) + implements LovelaceCard { + static get properties(): PropertyDeclarations { + return { + hass: {}, + }; + } + protected hass: HomeAssistant; + protected config: Config; + protected configEntities: EntityConfig[]; + + public getCardSize() { + return 3; + } + + public setConfig(config: Config) { + this.config = config; + this.style.setProperty( + "--glance-column-width", + config.column_width || "20%" + ); + + if (config.theming) { + if (typeof config.theming !== "string") { + throw new Error("Incorrect theming config."); + } + this.classList.add(`theme-${config.theming}`); + } + + this.configEntities = processConfigEntities(config.entities); + if (this.hass) { + this.requestUpdate(); + } + } + + protected render() { + if (!this.config) { + return html``; + } + const { title } = this.config; + const states = this.hass.states; + const entities = this.configEntities.filter( + (conf) => conf.entity in states + ); + + return html` + ${this.renderStyle()} + +
+ ${repeat( + entities, + (entityConf) => entityConf.entity, + (entityConf) => this.renderEntity(entityConf) + )} +
+
+ `; + } + + private renderStyle() { return html`